如何在C语言中将枚举名称转换为字符串

127

在C语言中,是否有将枚举器名称转换为字符串的可能性?

10个回答

248

一种方法是让预处理器来完成工作。这还可以确保您的枚举和字符串保持同步。

#define FOREACH_FRUIT(FRUIT) \
        FRUIT(apple)   \
        FRUIT(orange)  \
        FRUIT(grape)   \
        FRUIT(banana)  \

#define GENERATE_ENUM(ENUM) ENUM,
#define GENERATE_STRING(STRING) #STRING,

enum FRUIT_ENUM {
    FOREACH_FRUIT(GENERATE_ENUM)
};

static const char *FRUIT_STRING[] = {
    FOREACH_FRUIT(GENERATE_STRING)
};

预处理器完成后,您将得到:

enum FRUIT_ENUM {
    apple, orange, grape, banana,
};

static const char *FRUIT_STRING[] = {
    "apple", "orange", "grape", "banana",
};

然后你可以这样做:
printf("enum apple as a string: %s\n",FRUIT_STRING[apple]);

如果用例仅仅是打印枚举名称,那么请添加以下宏:
#define str(x) #x
#define xstr(x) str(x)

然后执行:
printf("enum apple as a string: %s\n", xstr(apple));

在这种情况下,两级宏可能看起来是多余的,但由于C语言中字符串化的工作方式,有时是必要的。例如,假设我们想要将#define与枚举一起使用:
#define foo apple

int main() {
    printf("%s\n", str(foo));
    printf("%s\n", xstr(foo));
}

输出将会是:
foo
apple

这是因为 str 会将输入的 foo 转换成字符串,而不是将其扩展为 apple。使用 xstr 可以先进行宏展开,然后再将结果转换为字符串。
更多信息请参见 Stringification

非常简单,就像你所描述的那样。我只需要在头文件中将枚举类型转换为字符串和字符串转换为枚举类型。当处理枚举时,我不想创建实现文件(对于C++/Objective-C)。 - p0lAris
你可以迭代FRUIT_STRING数组并进行字符串比较。如果找到匹配项,则索引是ENUM的值,假设ENUM不是稀疏的。 - Terrence M
9
如果您不想在命名空间中混杂苹果和橙子... 您可以使用 #define GENERATE_ENUM(ENUM) PREFIX##ENUM, 作为前缀。 - jsaak
当apple是int值而不是字面文本“apple”时,包含str(apple)的最后一行将无法工作。相反,如果传递了一个名为i的变量,它将打印“枚举苹果为字符串:i”。这是有道理的,因为它在预处理器级别上运行。 - hookenz
6
对于看到这篇文章的读者,使用宏列表来枚举程序中的各种项目的方法通常被非正式地称为“X宏”。 - Lundin
显示剩余2条评论

35
在这种情况下:

您拥有以下内容:

enum fruit {
    apple, 
    orange, 
    grape,
    banana,
    // etc.
};

我喜欢将这个放在枚举定义的头文件中:

static inline char *stringFromFruit(enum fruit f)
{
    static const char *strings[] = { "apple", "orange", "grape", "banana", /* continue for rest of values */ };

    return strings[f];
}

4
我真的看不出这有什么帮助。你可以详细解释一下,让它更明显一些吗? - David Heffernan
3
好的,那么这有什么帮助呢?你是说输入 enumToString(apple) 比输入 "apple" 更简单吗?好像现在并没有任何类型安全保障。除非我漏掉了什么,否则你在这里的建议就是毫无意义的,只会使代码变得难以理解。 - David Heffernan
2
好的,我现在明白了。在我看来,这个宏是无用的,我建议你删除它。 - David Heffernan
2
评论谈论宏。它在哪里? - Sandeep
4
这也不方便维护。如果我插入一个新的枚举,就必须记住在数组中正确位置上进行复制。 - Fabio
显示剩余7条评论

32

你不需要依靠预处理器来确保枚举和字符串同步。在我看来,使用宏往往会使代码更难阅读。

使用枚举和字符串数组

enum fruit                                                                   
{
    APPLE = 0, 
    ORANGE, 
    GRAPE,
    BANANA,
    /* etc. */
    FRUIT_MAX                                                                                                                
};   

const char * const fruit_str[] =
{
    [BANANA] = "banana",
    [ORANGE] = "orange",
    [GRAPE]  = "grape",
    [APPLE]  = "apple",
    /* etc. */  
};

注意:fruit_str数组中的字符串不必按枚举项的顺序声明。

如何使用它

printf("enum apple as a string: %s\n", fruit_str[APPLE]);

增加编译时检查

如果您担心忘记一个字符串,可以添加以下检查:

#define ASSERT_ENUM_TO_STR(sarray, max) \                                       
  typedef char assert_sizeof_##max[(sizeof(sarray)/sizeof(sarray[0]) == (max)) ? 1 : -1]

ASSERT_ENUM_TO_STR(fruit_str, FRUIT_MAX);

如果枚举项的数量与数组中字符串的数量不匹配,编译时将报告错误。


太棒了!你可以使用这个替代ASSERT_ENUM_TO_STR宏,只需包含#include <assert.h>: static_assert((sizeof(fruit_str)/sizeof(fruit_str[0]) == (FRUIT_MAX)), "fruit enum has different elements than fruit_str[]"); - dAm2K
这是一个可怕的解决方案,每次更改枚举时都需要进行多个更改。 - Enerccio
\o/ 这应该是被接受的答案。 - Ezequiel Garcia

20

我找到了一个 C 预处理器技巧,可以做同样的工作,无需声明专门的字符串数组(来源:http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/c_preprocessor_applications_en)。

连续枚举量

在 Stefan Ram 发明了连续枚举量之后(不需要显式声明索引,例如 enum {foo=-1, foo1 = 1}),可以通过这种精巧的技巧实现:

#include <stdio.h>

#define NAMES C(RED)C(GREEN)C(BLUE)
#define C(x) x,
enum color { NAMES TOP };
#undef C

#define C(x) #x,    
const char * const color_name[] = { NAMES };

这将产生以下结果:

int main( void )  { 
    printf( "The color is %s.\n", color_name[ RED ]);  
    printf( "There are %d colors.\n", TOP ); 
}

颜色是红色。
有3种颜色。

非顺序枚举

由于我想将错误代码定义映射到字符串数组中,以便我可以将原始错误定义附加到错误代码中(例如"错误为3 (LC_FT_DEVICE_NOT_OPENED)"),因此我扩展了代码,使您可以轻松确定相应枚举值所需的索引:

#define LOOPN(n,a) LOOP##n(a)
#define LOOPF ,
#define LOOP2(a) a LOOPF a LOOPF
#define LOOP3(a) a LOOPF a LOOPF a LOOPF
#define LOOP4(a) a LOOPF a LOOPF a LOOPF a LOOPF
#define LOOP5(a) a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF
#define LOOP6(a) a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF
#define LOOP7(a) a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF
#define LOOP8(a) a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF
#define LOOP9(a) a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF


#define LC_ERRORS_NAMES \
    Cn(LC_RESPONSE_PLUGIN_OK, -10) \
    Cw(8) \
    Cn(LC_RESPONSE_GENERIC_ERROR, -1) \
    Cn(LC_FT_OK, 0) \
    Ci(LC_FT_INVALID_HANDLE) \
    Ci(LC_FT_DEVICE_NOT_FOUND) \
    Ci(LC_FT_DEVICE_NOT_OPENED) \
    Ci(LC_FT_IO_ERROR) \
    Ci(LC_FT_INSUFFICIENT_RESOURCES) \
    Ci(LC_FT_INVALID_PARAMETER) \
    Ci(LC_FT_INVALID_BAUD_RATE) \
    Ci(LC_FT_DEVICE_NOT_OPENED_FOR_ERASE) \
    Ci(LC_FT_DEVICE_NOT_OPENED_FOR_WRITE) \
    Ci(LC_FT_FAILED_TO_WRITE_DEVICE) \
    Ci(LC_FT_EEPROM_READ_FAILED) \
    Ci(LC_FT_EEPROM_WRITE_FAILED) \
    Ci(LC_FT_EEPROM_ERASE_FAILED) \
    Ci(LC_FT_EEPROM_NOT_PRESENT) \
    Ci(LC_FT_EEPROM_NOT_PROGRAMMED) \
    Ci(LC_FT_INVALID_ARGS) \
    Ci(LC_FT_NOT_SUPPORTED) \
    Ci(LC_FT_OTHER_ERROR) \
    Ci(LC_FT_DEVICE_LIST_NOT_READY)


#define Cn(x,y) x=y,
#define Ci(x) x,
#define Cw(x)
enum LC_errors { LC_ERRORS_NAMES TOP };
#undef Cn
#undef Ci
#undef Cw
#define Cn(x,y) #x,
#define Ci(x) #x,
#define Cw(x) LOOPN(x,"")
static const char* __LC_errors__strings[] = { LC_ERRORS_NAMES };
static const char** LC_errors__strings = &__LC_errors__strings[10];
在这个例子中,C 预处理器将生成以下代码:
enum LC_errors { LC_RESPONSE_PLUGIN_OK=-10,  LC_RESPONSE_GENERIC_ERROR=-1, LC_FT_OK=0, LC_FT_INVALID_HANDLE, LC_FT_DEVICE_NOT_FOUND, LC_FT_DEVICE_NOT_OPENED, LC_FT_IO_ERROR, LC_FT_INSUFFICIENT_RESOURCES, LC_FT_INVALID_PARAMETER, LC_FT_INVALID_BAUD_RATE, LC_FT_DEVICE_NOT_OPENED_FOR_ERASE, LC_FT_DEVICE_NOT_OPENED_FOR_WRITE, LC_FT_FAILED_TO_WRITE_DEVICE, LC_FT_EEPROM_READ_FAILED, LC_FT_EEPROM_WRITE_FAILED, LC_FT_EEPROM_ERASE_FAILED, LC_FT_EEPROM_NOT_PRESENT, LC_FT_EEPROM_NOT_PROGRAMMED, LC_FT_INVALID_ARGS, LC_FT_NOT_SUPPORTED, LC_FT_OTHER_ERROR, LC_FT_DEVICE_LIST_NOT_READY, TOP };

static const char* __LC_errors__strings[] = { "LC_RESPONSE_PLUGIN_OK", "" , "" , "" , "" , "" , "" , "" , "" "LC_RESPONSE_GENERIC_ERROR", "LC_FT_OK", "LC_FT_INVALID_HANDLE", "LC_FT_DEVICE_NOT_FOUND", "LC_FT_DEVICE_NOT_OPENED", "LC_FT_IO_ERROR", "LC_FT_INSUFFICIENT_RESOURCES", "LC_FT_INVALID_PARAMETER", "LC_FT_INVALID_BAUD_RATE", "LC_FT_DEVICE_NOT_OPENED_FOR_ERASE", "LC_FT_DEVICE_NOT_OPENED_FOR_WRITE", "LC_FT_FAILED_TO_WRITE_DEVICE", "LC_FT_EEPROM_READ_FAILED", "LC_FT_EEPROM_WRITE_FAILED", "LC_FT_EEPROM_ERASE_FAILED", "LC_FT_EEPROM_NOT_PRESENT", "LC_FT_EEPROM_NOT_PROGRAMMED", "LC_FT_INVALID_ARGS", "LC_FT_NOT_SUPPORTED", "LC_FT_OTHER_ERROR", "LC_FT_DEVICE_LIST_NOT_READY", };

这导致以下实现能力:

LC_errors__strings[-1] ==> LC_errors__strings[LC_RESPONSE_GENERIC_ERROR] ==> "LC_RESPONSE_GENERIC_ERROR"


1
很好。这正是我正在寻找并使用它的东西。同样的错误 :) - mrbean
1
LOOP3到LOOP9可以分别用LOOP2到LOOP8来定义。 - Michael
@Michael:说得好。虽然这样做会使维护变得更加困难,但可以生成更短的代码。 - Maschina
很好。如果color_name在头文件中,请不要忘记将其定义为静态。 - DimP

14

没有直接实现这个功能的简单方法。但是 P99 有宏允许您自动创建此类型的函数:

 P99_DECLARE_ENUM(color, red, green, blue);

在头文件中

 P99_DEFINE_ENUM(color);

在一个编译单元(.c文件)中可以解决问题,在这个例子中,函数将会被称为color_getname


4
一种比Hokyo的“非顺序枚举”答案更简单的替代方法,基于使用指示器来实例化字符串数组:
#define NAMES C(RED, 10)C(GREEN, 20)C(BLUE, 30)
#define C(k, v) k = v,
enum color { NAMES };
#undef C

#define C(k, v) [v] = #k,    
const char * const color_name[] = { NAMES };

1
当索引为负数时(在错误枚举中非常常见),这将无法工作。 - DadiBit

2
我通常这样做:

我通常这样做:

#define COLOR_STR(color)                            \
    (RED       == color ? "red"    :                \
     (BLUE     == color ? "blue"   :                \
      (GREEN   == color ? "green"  :                \
       (YELLOW == color ? "yellow" : "unknown"))))   

这不是一个糟糕的答案。它清晰、简单易懂。如果你正在处理其他人需要快速阅读和理解你的代码的系统,那么清晰度非常重要。我不建议使用预处理器技巧,除非它们被彻底注释或在编码标准中进行了描述。 - nielsen

2

没有对枚举进行验证的这种函数有些危险。建议使用switch语句。另一个优点是,这可以用于具有定义值的枚举,例如对于标志,其中值为1、2、4、8、16等。

还要将所有枚举字符串放在一个数组中:

static const char * allEnums[] = {
    "Undefined",
    "apple",
    "orange"
    /* etc */
};

在头文件中定义索引:

#define ID_undefined       0
#define ID_fruit_apple     1
#define ID_fruit_orange    2
/* etc */

这样做可以更容易地生成不同版本,例如,如果您想用其他语言制作国际版程序。

在头文件中使用宏:

#define CASE(type,val) case val: index = ID_##type##_##val; break;

制作一个带有switch语句的函数,它应该返回一个const char *,因为这些字符串是静态常量:
const char * FruitString(enum fruit e){

    unsigned int index;

    switch(e){
        CASE(fruit, apple)
        CASE(fruit, orange)
        CASE(fruit, banana)
        /* etc */
        default: index = ID_undefined;
    }
    return allEnums[index];
}

如果在Windows系统上进行编程,则ID_值可以是资源值。

(如果使用C++,则所有函数都可以具有相同的名称。

string EnumToString(fruit e);

)


0
为了避免整数和枚举成员名称之间的混淆,在我的调试过程中,我决定使用以下命令:
enum {CC, CV, STOP, DESCONECTADO}; unsigned char mode;
Serial.print(" mode = "); Serial.println((mode == 0) ? "CC" : (mode == 1) ? "CV" : (mode == 2) ? "STOP" : "DESCONECTADO");
这样打印出来的是状态单词而不是整数值。

0

我决定编写一个函数,通过复制枚举并在 Vim 中使用正则表达式来更新其主体。我使用 switch-case,因为我的枚举不是紧凑的,所以我们有最大的灵活性。我将正则表达式作为注释保留在代码中,因此只需复制粘贴即可。

我的枚举(缩短版,真实的枚举要大得多):

enum opcode
{
    op_1word_ops = 1024,
    op_end,

    op_2word_ops = 2048,
    op_ret_v,
    op_jmp,

    op_3word_ops = 3072,
    op_load_v,
    op_load_i,

    op_5word_ops = 5120,
    op_func2_vvv,
};

在复制枚举之前的函数:

const char *get_op_name(enum opcode op)
{
    // To update copy the enum and apply this regex:
    // s/\t\([^, ]*\).*$/\t\tcase \1:    \treturn "\1";
    switch (op)
    {
    }

    return "Unknown op";
}

我将枚举的内容粘贴到 switch 括号内:
const char *get_op_name(enum opcode op)
{
    // To update copy the enum and apply this regex:
    // s/\t\([^, ]*\).*$/\t\tcase \1:    \treturn "\1";
    switch (op)
    {
    op_1word_ops = 1024,
    op_end,

    op_2word_ops = 2048,
    op_ret_v,
    op_jmp,

    op_3word_ops = 3072,
    op_load_v,
    op_load_i,

    op_5word_ops = 5120,
    op_func2_vvv,
    }

    return "Unknown op";
}

然后,在Vim中使用Shift-V选择行,按下:,然后粘贴(在Windows上使用Ctrl-V)正则表达式s/\t\([^, ]*\).*$/\t\tcase \1: \treturn "\1";,然后按回车键。
const char *get_op_name(enum opcode op)
{
    // To update copy the enum and apply this regex:
    // s/\t\([^, ]*\).*$/\t\tcase \1:    \treturn "\1";
    switch (op)
    {
        case op_1word_ops:      return "op_1word_ops";
        case op_end:        return "op_end";

        case op_2word_ops:      return "op_2word_ops";
        case op_ret_v:      return "op_ret_v";
        case op_jmp:        return "op_jmp";

        case op_3word_ops:      return "op_3word_ops";
        case op_load_v:     return "op_load_v";
        case op_load_i:     return "op_load_i";

        case op_5word_ops:      return "op_5word_ops";
        case op_func2_vvv:      return "op_func2_vvv";
    }

    return "Unknown op";
}

正则表达式跳过第一个\t字符,然后将其后的每个既不是,也不是 的字符放入\1中,并匹配行的其余部分以删除所有内容。然后,使用\1作为枚举标签,以case <label>: return "<label>";格式重新制作行。请注意,它在此帖子中看起来排列不良,仅因为StackOverflow使用4个空格缩进,而我在Vim中使用8个空格缩进,因此您可能需要编辑样式的正则表达式。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接