在C语言中指定枚举类型的大小

60

已经阅读了这个相关问题,但是想要更具体的解答。

  • 有没有一种方法可以明确告诉编译器你希望枚举的宽度是多少?
  • 如果有的话,你怎么做呢?我知道在C#中如何指定,那在C语言中是否也是这样做的呢?
  • 这样做是否值得?当枚举值传递给一个函数时,它会被作为一个整型值传递吗?
10个回答

30

我相信如果你使用GCC,就会有一个标志。

-fshort-enums


+1 酷!这正是我想要的,就是强制编译器方面的。 - Will
23
或者选择性地 enum __attribute__((__packed__)) my_enum {...}; - Nikolai Fetissov
3
@Nikolai N Fetissov: 我会尝试这个方法。我知道在结构体中可以使用 __packed__ 属性,但不知道在枚举中也可以使用。 - Will
这里有更多的见解。https://dev59.com/Km445IYBdhLWcg3wZJiw#54527229 - Gabriel Staples
正如GCC文档所述,使用标志-fshort-enums是危险的,因为它会影响所有枚举类型。最好使用__attribute__((__packed__)) - Roland Illig

26

是否有办法告诉编译器要将枚举定义为特定的宽度?

一般情况下不能。标准的C语言不支持。

这么做是否值得呢?

这要看具体情况。如果你是想传递参数给函数,那么不值得这样做(见下文)。如果是想在构建来自枚举类型的聚合时节省内存,那么这样做可能值得考虑。但是,在C语言中,你可以在聚合中使用大小合适的整数类型而不是枚举类型。在C语言中(与C++相反),枚举类型和整数类型几乎总是可以互换使用。

当枚举值被传递给函数时,它是否会作为一个int大小的值传递?

现代的大多数编译器都会将所有参数作为给定硬件平台上自然字长大小的值进行传递。例如,在64位平台上,许多编译器都会将所有参数作为64位值传递,而不管它们的实际大小,即使在该平台上类型 int 的大小为32位(因此,在这种平台上它通常不会作为“int大小”的值传递)。因此,试图优化枚举大小以用于参数传递是没有意义的。


4
如果你在谈论函数传递参数,那么不值得这样做(见下文)。并非总是如此。如果使用8位处理器,则自然大小为8位,但int和enum至少为16位(不带-fshort-enums),因此您至少需要2个寄存器,这可能会减慢函数调用的速度。 - 12431234123412341234123

14

即使你在编写严格的C代码,结果也会依赖于编译器。通过使用本主题中提供的策略,我得到了一些有趣的结果...

enum_size.c

#include <stdio.h>

enum __attribute__((__packed__)) PackedFlags {
    PACKED = 0b00000001,
};

enum UnpackedFlags {
    UNPACKED = 0b00000001,
};

int main (int argc, char * argv[]) {
  printf("packed:\t\t%lu\n", sizeof(PACKED));
  printf("unpacked:\t%lu\n", sizeof(UNPACKED));
  return 0;
}

$ gcc enum_size.c
$ ./a.out
packed:         4
unpacked:       4

$ gcc enum_size.c -fshort_enums
$ ./a.out
packed:         4
unpacked:       4

$ g++ enum_size.c
$ ./a.out
packed:         1
unpacked:       4

$ g++ enum_size.c -fshort_enums
$ ./a.out
packed:         1
unpacked:       1

在我上面的例子中,直到我开始使用C++编译器,我才意识到__attribute__((__packed__))修饰符没有产生任何好处。
编辑:
@technosaurus的怀疑是正确的。
通过检查sizeof(enum PackedFlags)的大小而不是sizeof(PACKED),我看到了我所期望的结果...
  printf("packed:\t\t%lu\n", sizeof(enum PackedFlags));
  printf("unpacked:\t%lu\n", sizeof(enum UnpackedFlags));

我现在从gcc看到了预期结果:

$ gcc enum_size.c
$ ./a.out
packed:         1
unpacked:       4

$ gcc enum_size.c -fshort_enums
$ ./a.out
packed:         1
unpacked:       1

3
尝试使用sizeof (enum PackedFlags)以避免枚举被扩展。您将在使用sizeof('a')时获得类似的体验。 - technosaurus
@technosaurus 没错!为什么C编译器会发生这种情况,而C++编译器不会呢? - Zak

12

如果枚举是结构体的一部分,也有另一种方法:

enum whatever { a,b,c,d };

struct something {
   char  :0;
   enum whatever field:CHAR_BIT;
   char  :0;
};

如果枚举字段周围有普通字段,则可以省略:0;。 如果之前有另一个位域,则:0将强制对其后面的字段进行字节对齐到下一个字节。


1
完全没有任何一种 gcc 的标识组合可以编译它,只会出现 error: expected specifier-qualifier-list before ‘:’ token 的错误。零大小的位域是真实存在的,而且它们必须是匿名的,但它们仍然需要有一个类型。我猜想,一个零大小/匿名的位域意味着“在下一个分配单元的开始处开始下一个非零大小的位域”,其中它的类型指定对齐到哪个单元。我猜这引发了一个问题,为什么它需要一个类型,当接下来的非零大小的位域已经有一个类型了。但要问委员会这个问题可能需要时间机器。 - underscore_d
这应该被标记为正确的解决方案。所有其他答案都可以进行优化,而使用位域是有保证的。 - Twifty
我认为这不能用来定义一个可以在结构体外部使用的枚举,对吗? - dykeag
不行。因为它是位集语法的属性,所以只能在 struct 内部使用。 - Patrick Schlüter
2
C11 6.7.2.1p5表示_位域必须具有限定或未限定版本的_Bool、signed int、unsigned int或某些实现定义类型_,这意味着如果实现允许将enum作为位域类型,则您的答案才有效。C11 6.7.2.1p11表示_实现可以分配任何足够大以容纳位域的可寻址存储单元_,它不必是底层类型的倍数(这是我之前假设的)。 - Roland Illig

12

从C23开始,在标准C中终于可以实现以下功能:

您可以在enum关键字后面(或者在名称标签后面,如果有命名)放置一个冒号和整数类型,以指定枚举的固定底层类型,这将设置枚举类型的大小和范围。

这样做真的值得吗?当将枚举值传递给函数时,它是否会被作为int大小的值传递?

在x86_64上,整数的类型不影响它是否在寄存器中传递(只要它适合单个寄存器)。然而,堆上数据的大小对于缓存性能非常重要。


2
参考: https://zh.cppreference.com/w/c/language/enum - Ryan Haining

11
在某些情况下,这可能会有所帮助:
typedef uint8_t command_t;
enum command_enum
{
    CMD_IDENT                = 0x00,     //!< Identify command
    CMD_SCENE_0              = 0x10,     //!< Recall Scene 0 command
    CMD_SCENE_1              = 0x11,     //!< Recall Scene 1 command
    CMD_SCENE_2              = 0x12,     //!< Recall Scene 2 command
};

/* cmdVariable is of size 8 */
command_t cmdVariable = CMD_IDENT; 

一方面,类型command_t的大小为1(8位),可用于变量和函数参数类型。 另一方面,您可以使用默认为int类型的枚举值进行赋值,但编译器会在分配给command_t类型变量时立即将它们转换。
此外,如果您做了不安全的事情,例如定义并使用CMD_16bit = 0xFFFF,编译器将通过以下消息警告您:

警告:大整数隐式截断为无符号类型[-Woverflow]

编辑:如果您正在搜索此主题,也许是时候开始在嵌入式系统中使用C++并将部分代码切换到C++并使用class enum

1
这里的"编译器"指的是GCC,其他编译器可能发出类似的警告,也可能不会。 - Roland Illig
实际上,这将是大小为1,因为 sizeof(uint8_t) == 1 ,如果存在的话。虽然它只有8位。 - SamB

10

你可以通过定义适当的值来强制枚举至少具有某个大小。例如,如果你希望将枚举存储为与int相同大小,即使所有值都适合于char,你可以这样做:

typedef enum {
    firstValue = 1,
    secondValue = 2,

    Internal_ForceMyEnumIntSize = MAX_INT
} MyEnum;

注意,行为可能取决于实现。

正如你所指出的那样,将这样的值传递给函数将导致其被扩展为int类型,但如果你在数组或结构体中使用自己的类型,则大小将很重要。如果你真的关心元素大小,应该使用像int8_tint32_t等类型。


1
这差不多是我想的。我倾向于定义一些枚举,它们实际上不需要超过8或16位,当我不必浪费空间时,我很讨厌这种情况。 - Will
1
如果优化器删除了您未使用的“Internal_ForceMyEnumIntSize”,则此方法可能无法正常工作。 - EBlake
@Patrick 不,它只是枚举的另一个成员。它本来可以被称为thirdValue、lastValue或maxValue。MAX_INT 被 C 定义为 int 类型的最大值。 - Kristopher Johnson
1
@EBlake在stackoverflow.com上表示,类型“应能够表示枚举中所有成员的值。”它没有提到允许优化器删除成员,因此我不明白为什么会被允许。我也看不出有什么好处?枚举器只是用于命名整数常量的替代方式;如果代码中未使用枚举器成员,则它不会出现在二进制文件中,因此没有任何东西可以删除或优化。 - underscore_d
在默认情况下,枚举类型始终被存储为 int,因此示例不是很相关。此外,使用 0xFF 而不是 MAX_INT 不会给出一个 8 位的枚举大小,而是像你解释的那样“至少 8 位”。并且在 GCC 中,默认情况下这将是一个 32 位的 int。 - Julien
显示剩余2条评论

5

@Nyx0uf 说道,GCC有一个标志可以设置:

-fshort-enums

仅为枚举类型分配其需要的字节数,以满足声明的可能值范围。具体来说,枚举类型等价于具有足够空间的最小整数类型。

警告:使用-fshort-enums开关会导致GCC生成的代码与未使用该开关生成的代码不兼容。将其用于符合非默认应用程序二进制接口的要求。

来源:https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

阅读以下文章以获取一般见识:https://www.embedded.fm/blog/2016/6/28/how-big-is-an-enum
有趣的是...注意我在下面用黄色标记的那一行!添加一个名为ARM_EXCEPTION_MAKE_ENUM_32_BIT的枚举条目,并将其值设置为0xffffffff,即stdint.h中的UINT32_MAX(请参见此处此处),会强制使这个特定的Arm_symbolic_exception_name枚举具有uint32_t整数类型。这就是ARM_EXCEPTION_MAKE_ENUM_32_BIT条目的唯一目的!它之所以有效,是因为uint32_t是可以包含此枚举中所有枚举值的最小整数类型——即:08(包括)以及0xffffffff或十进制2^32-1=4294967295

enter image description here

关键词:ARM_EXCEPTION_MAKE_ENUM_32_BIT 枚举的目的以及为什么需要它?Arm_symbolic_exception_name 0xffffffff 枚举条目的用途。

2

目前我无法回答你的前两个问题,因为我正在尝试找到一种好的方法来解决这个问题。如果我发现自己喜欢的策略,也许我会编辑这篇文章。但是这并不直观。

但我想指出一些迄今未提到的事情,并回答第三个问题:

当编写将从非C语言调用的C API时,“值得做”。任何直接链接到C代码的内容都需要正确理解C代码API中所有结构体、参数列表等的内存布局。不幸的是,C类型如int,或更糟糕的是枚举类型,大小相对较不可预测(由编译器、平台等决定),因此,了解包含枚举类型的任何内容的内存布局可能会很棘手,除非您使用的编程语言编译器也是C编译器,并且具有某种语言机制来利用该知识。当API使用可预测大小的C类型,如uint8_tuint16_tuint32_tuint64_tvoid*uintptr_t等以及由这些可预测大小的类型组成的结构/联合时,编写无问题的C库绑定会更容易。

因此,当内存布局和对齐问题可能存在时,我会关心枚举类型的大小。但是,除非您有某些增加机会成本的特殊情况(例如,在像小型MCU这样的内存受限系统上具有大量枚举类型值的数组/列表),否则我不会过于担心优化问题。

不幸的是,像-fshort-enums这样的情况并没有得到帮助,因为该功能是供应商特定的,且不太可预测(例如,另一个系统必须通过近似GCC算法来“猜测”枚举类型的大小)。如果有任何东西,它将允许人们以一种打破其他语言绑定(或未使用相同选项编译的其他C代码)的常见假设的方式编译C代码,预期结果是参数或结构成员被写入或从内存中错误的位置读取,导致内存损坏。


1
让我感到不悦的是,标准似乎对需要遵守 C 实现控制之外的约束条件的代码的概念持敌意。即使是识别一类使用常见结构布局算法的实现,该算法将每个内存放置在跟随所有先前对象并适合其对齐的最低偏移量处,也是如此简单。 - supercat

0

这取决于为枚举分配的值。

例如: 如果存储大于2^32-1的值,则为整个枚举分配的大小将更改为下一个大小。

将0xFFFFFFFFFFFF值存储到枚举变量中,如果尝试在32位环境中编译,则会发出警告(四舍五入警告)。 而在64位编译中,它将成功,并且分配的大小将为8字节。


我在clang 11中尝试了这个,编译器只是将值截断为32位。 - SO_fix_the_vote_sorting_bug

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