在C/C++中,计算一个联合体的大小

52

C/C++中的联合(union)大小是多少?它是其中最大数据类型的大小吗?如果是,那么如果联合中较小的数据类型处于激活状态,编译器如何计算移动堆栈指针的方式?

8个回答

69

union总是占用与最大成员相同的空间。无论当前使用的是什么。

union {
  short x;
  int y;
  long long z;
}

上述的 union 的实例始终需要至少使用一个 long long 来进行存储。

附注:正如Stefano所指出的那样,任何类型(union, struct, class)实际占用的空间确实取决于编译器执行对齐等其他问题。出于简单起见,我没有详细说明这些问题,只是想告诉大家 union 考虑了最大的项目大小。 重要的是要知道,实际大小确实取决于对齐方式


当使用long double时,sizeof可能返回较大的值。 long double占用10字节,但Intel建议将其对齐到16字节。 - dreamlax
长双精度浮点数...嗯,这取决于编译器。我认为PowerPC编译器使用128位的长双精度浮点数。 - Mehrdad Afshari
是的,抱歉我想说x86上的长双精度。 - dreamlax

37
标准答案在C++标准的第9.5节中回答了所有问题,或者在C99标准的第6.5.2.3段第5款(或C11标准的第6段,或C18标准的第6.7.2.1条第16款)中回答:

联合体中最多只能有一个数据成员处于活动状态,也就是说,在联合体中最多只能存储一个数据成员的值。[注意:为了简化使用union而作出一项特殊保证:如果POD-union包含共享公共初始序列(9.2)的多个POD-struct,并且如果这种POD-union类型的对象包含POD-struct之一,则允许检查任何POD-struct成员的公共初始序列;请参见9.2。]联合体的大小足以容纳其数据成员中最大的那个。每个数据成员都被分配一个像它是一个结构体的唯一成员的方式。

这意味着每个成员共享同一内存区域。至多有一个成员处于活动状态,但你不能确定哪一个。你需要在其他地方自己存储关于当前活动成员的信息。将这样的标志与联合体一起存储(例如使用一个带有整数类型标志和一个联合体数据存储的结构体)将给你一个所谓的“带判别联合”:一个知道当前“活动类型”的联合。

一种常见的用法是在词法分析器中,可以有不同的标记,但根据标记,需要存储不同的信息(将line放入每个结构中以显示共同的初始序列):

struct tokeni {
    int token; /* type tag */
    union {
        struct { int line; } noVal;
        struct { int line; int val; } intVal;
        struct { int line; struct string val; } stringVal;
    } data;
};

标准允许您访问每个成员的line,因为这是每个成员的公共初始序列。

存在编译器扩展,允许访问所有成员,无论哪一个当前存储了它的值。这允许在每个成员之间以不同类型重新解释存储的位,从而实现高效的重组。例如,以下内容可用于将浮点变量分解为2个无符号短整型:

union float_cast { unsigned short s[2]; float f; };

当编写低级别的代码时,这会非常有用。如果编译器不支持该扩展,但您仍然使用它,那么您编写的代码结果将未定义。因此,如果您使用此技巧,请确保您的编译器支持它。


3
在我看来,这是一个糟糕的标准语言的可怕例子 - 实际上,整个关于工会的部分似乎有点不足。为什么要引入“活跃”的概念呢? - anon
2
GCC至少明确支持交叉读取联合成员。如果成员在3.10/15中某种方式相关或者具有相同的布局兼容性,我相信即使它不是“活动”的成员,你仍然可以读取其他成员。 - Johannes Schaub - litb
正是“激活”位让我感到困惑。如果 9.5\1 以“最多的值”开头,那么就没有必要引入这个模糊的“激活”概念。但这应该(如果需要的话)放在 comp.lang.c++.std 上,而不是在一个可怕的 SO 评论框中!所以我在这个主题上签退了。 - anon
1
哈哈,好的。开个线程,让我们玩得开心 :p - Johannes Schaub - litb
1
@anon,“active”在这里是必不可少的,因为联合中数据的类型取决于最后存储的内容。仅仅因为您不理解标准并不意味着它是错误的。 - Jim Balter

22

这取决于编译器和选项。

int main() {
  union {
    char all[13];
    int foo;
  } record;

printf("%d\n",sizeof(record.all));
printf("%d\n",sizeof(record.foo));
printf("%d\n",sizeof(record));

}

这将输出:

13 4 16

如果我没记错的话,这取决于编译器放置在分配空间中的对齐方式。因此,除非您使用某些特殊选项,否则编译器将在您的联合空间中放置填充。

编辑:使用gcc您需要使用pragma指令。

int main() {
#pragma pack(push, 1)
      union {
           char all[13];
           int foo;
      } record;
#pragma pack(pop)

      printf("%d\n",sizeof(record.all));
      printf("%d\n",sizeof(record.foo));
      printf("%d\n",sizeof(record));

}

这会输出

13 4 13

你也可以从反汇编中看到它(为了清晰起见,删除了一些printf)

  0x00001fd2 <main+0>:    push   %ebp             |  0x00001fd2 <main+0>:    push   %ebp
  0x00001fd3 <main+1>:    mov    %esp,%ebp        |  0x00001fd3 <main+1>:    mov    %esp,%ebp
  0x00001fd5 <main+3>:    push   %ebx             |  0x00001fd5 <main+3>:    push   %ebx
  0x00001fd6 <main+4>:    sub    $0x24,%esp       |  0x00001fd6 <main+4>:    sub    $0x24,%esp
  0x00001fd9 <main+7>:    call   0x1fde <main+12> |  0x00001fd9 <main+7>:    call   0x1fde <main+12>
  0x00001fde <main+12>:   pop    %ebx             |  0x00001fde <main+12>:   pop    %ebx
  0x00001fdf <main+13>:   movl   $0xd,0x4(%esp)   |  0x00001fdf <main+13>:   movl   $0x10,0x4(%esp)                                         
  0x00001fe7 <main+21>:   lea    0x1d(%ebx),%eax  |  0x00001fe7 <main+21>:   lea    0x1d(%ebx),%eax
  0x00001fed <main+27>:   mov    %eax,(%esp)      |  0x00001fed <main+27>:   mov    %eax,(%esp)
  0x00001ff0 <main+30>:   call  0x3005 <printf>   |  0x00001ff0 <main+30>:   call   0x3005 <printf>
  0x00001ff5 <main+35>:   add    $0x24,%esp       |  0x00001ff5 <main+35>:   add    $0x24,%esp
  0x00001ff8 <main+38>:   pop    %ebx             |  0x00001ff8 <main+38>:   pop    %ebx
  0x00001ff9 <main+39>:   leave                   |  0x00001ff9 <main+39>:   leave
  0x00001ffa <main+40>:   ret                     |  0x00001ffa <main+40>:   ret    

唯一不同之处在于main+13,编译器在栈上分配0xd而不是0x10。


谢谢 - 这样我就不用组装这个答案了! - Jonathan Leffler
3
好的,我会尽力进行翻译。内容如下:是的,我想我们都应该说“至少和最大的包含类型一样大”。 - anon
1
@Neil:编译器对齐是一个完全不同的问题。它也会发生在结构体中,并且还取决于你将联合体放置在结构体中的位置。虽然这当然是正确的,但我认为这只是让这个问题变得更复杂了。顺便说一下,我很注意将我的示例联合体对齐到8字节边界上 :-p - Mehrdad Afshari

11

联合体中没有所谓的活动数据类型。您可以自由读写联合体的任何“成员”:对于您来说,这是解释您获得的内容的问题。

因此,联合体的sizeof始终是其最大数据类型的sizeof。


3
当然,你是错的...标准中的语言明确提到了主动数据类型。但是,sizeof是一个编译时操作,因此当然不依赖于活动数据类型。 - Jim Balter
2
@JimBalter - 你关于标准的说法是正确的。我的意思是,在C语言中,你无法查询一个union的_active datatype_。没有任何防止编码人员写入float并读取int(并获得垃圾数据)的机制。 - mouviciel
4
你说过,“联合体没有活动数据类型的概念”。你错了,承认吧。如果你试图声称你的意思与你写的完全不同以避免被证明错误是不行的。“没有什么可以防止编码者写一个浮点数并读取一个整数(然后得到垃圾)。”--当然,没有什么可以阻止它... C标准并没有阻止任何事情;它只告诉你这种行为是否被定义——它没有被定义。正如一再强调的那样,未定义行为包括任何事情,甚至包括核武器爆炸。对某些人来说,这会阻止他们编写UB。 - Jim Balter
如果没有明确指出是在谈论C还是C ++,则会得到-1分,因为它们在联合类型转换方面有根本区别。在C中允许重新解释对象字节表示https://dev59.com/-2gu5IYBdhLWcg3wBym1,但在C ++中不允许。在后者中,这是纯UB,或者如果您愿意接受(例如,在g ++中),则是实现定义的规则。 - underscore_d

2

这个大小至少是最大的组合类型的大小。没有“活动”类型的概念。


1
除了这个,是有的。 - underscore_d

1

你应该把union看作一个容器,其中包含最大的数据类型,并结合强制转换的快捷方式。当你使用较小的成员之一时,未使用的空间仍然存在,但它只是保持未使用状态。

在Unix下,你经常会看到它与ioctl()调用结合使用,所有ioctl()调用都将传递相同的结构体,其中包含所有可能的响应的union。例如,这个例子来自于/usr/include/linux/if.h,这个结构体被用于ioctl()的以太网接口的配置/查询状态,请求参数定义了哪个union部分实际上正在使用:

struct ifreq 
{
#define IFHWADDRLEN 6
    union
    {
        char    ifrn_name[IFNAMSIZ];        /* if name, e.g. "en0" */
    } ifr_ifrn;

    union {
        struct  sockaddr ifru_addr;
        struct  sockaddr ifru_dstaddr;
        struct  sockaddr ifru_broadaddr;
        struct  sockaddr ifru_netmask;
        struct  sockaddr ifru_hwaddr;
        short   ifru_flags;
        int ifru_ivalue;
        int ifru_mtu;
        struct  ifmap ifru_map;
        char    ifru_slave[IFNAMSIZ];   /* Just fits the size */
        char    ifru_newname[IFNAMSIZ];
        void *  ifru_data;
        struct  if_settings ifru_settings;
    } ifr_ifru;
};

0

在C/C++中,union的大小是多少?它是其内部最大数据类型的大小吗?

是的,union的大小是其最大成员的大小。

例如:

#include<stdio.h>

union un
{
    char c;
    int i;
    float f;
    double d;
};

int main()
{
    union un u1;
    printf("sizeof union u1 : %ld\n",sizeof(u1));
    return 0;
}

输出:
sizeof union u1 : 8
sizeof double d : 8

这里最大的成员是 double。两个成员都有大小为8。所以,正如sizeof正确告诉你的那样,联合体的大小确实为8
编译器如何计算移动堆栈指针的方式,如果联合中较小的数据类型是活动的?
这是由编译器内部处理的。假设我们正在访问联合体的一个数据成员,那么我们不能访问其他数据成员,因为我们只能访问联合体的单个数据成员,因为每个数据成员共享相同的内存。通过使用联合体,我们可以节省大量宝贵的空间。

-1
  1. 最大成员的大小。

  2. 这就是为什么联合体通常在一个结构体内部有一个标志,指示哪个是“活动”成员。

例子:

struct ONE_OF_MANY {
    enum FLAG { FLAG_SHORT, FLAG_INT, FLAG_LONG_LONG } flag;
    union { short x; int y; long long z; };
};

1
不正确。一个常见的用途是访问较大类型的较小部分。例如:联合体 U { int i; char c[4]; }; 可以用于(实现特定的)访问 4 字节整数的字节。 - anon
哦,没错...我没有注意到这种可能性。我一直都是使用字节位移和类似的方法来访问较大类型的部分。 - isekaijin
@anon - 这取决于你的编译器,可能是实现特定的或者只是未定义行为。如果可以避免,甚至依赖前者也是不好的实践。 - underscore_d

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