12. 联合体的大小:联合体大小计算规则、成员对齐对联合体大小的影响
联合体(union)在C语言里是个很有意思的存在。它和结构体长得像,但内存布局完全是两码事。结构体是“大家都有房间住”,联合体是“大家挤一个房间”。
那问题来了——这个“房间”到底该有多大?这就是我们今天要聊的核心:联合体的大小计算规则,以及成员对齐对它造成的影响。
12.1 联合体大小的基本规则
联合体的所有成员共享同一块内存。所以它的大小,至少得能装下最大的那个成员。这是最朴素的直觉,对吧?
但事情没这么简单。我当年刚学C语言时,写过一个联合体:
union Data {
char c;
int i;
double d;
};
我当时想:char占1字节,int占4字节,double占8字节。那这个联合体应该是8字节吧?
嗯,结果是对的。但为什么?因为double是8字节,它最大。联合体的大小就是最大成员的大小——这是第一条规则。
核心规则1:联合体的大小至少等于其最大成员的大小。
但等等,这只是“至少”。实际大小往往比最大成员还要大一点。为什么?因为对齐。
12.2 对齐对联合体大小的影响
联合体也有对齐要求。它的对齐方式,是所有成员中对齐要求最严格的那个。说白了,联合体的对齐值等于所有成员对齐值的最大值。
我举个例子你就明白了:
union Example {
char arr[5]; // 对齐值1,大小5
int i; // 对齐值4,大小4
};
最大成员是arr,5字节。但联合体的对齐要求是4(因为int的对齐值是4)。所以联合体的大小必须是4的倍数。5不是4的倍数,向上取整到8。
所以这个联合体的大小是8字节,而不是5字节。
个人经验:我在做嵌入式通信协议解析时,经常用联合体来解析数据包。有一次我定义了一个联合体来解析一个5字节的报文,结果sizeof返回8,导致我缓冲区分配多了3字节。虽然没出bug,但浪费了宝贵的RAM。从那以后,我每次定义联合体都会手动算一下对齐后的实际大小。
12.3 联合体大小计算三步法
我总结了一个三步法,你照着做就不会错:
- 找最大成员:找出所有成员中占用字节数最大的那个。
- 找对齐值:找出所有成员中对齐要求最严格的那个值。
- 向上取整:将最大成员的大小向上取整到对齐值的整数倍。
咱们用代码验证一下:
#include <stdio.h>
#include <stddef.h>
union U1 {
char a[5];
int b;
};
union U2 {
short c[3];
double d;
};
union U3 {
char e;
int f[2];
long long g;
};
int main() {
printf("U1: %zu (预期8)\n", sizeof(union U1));
printf("U2: %zu (预期16)\n", sizeof(union U2));
printf("U3: %zu (预期8)\n", sizeof(union U3));
return 0;
}
运行结果:
U1: 8 (预期8)
U2: 16 (预期16)
U3: 8 (预期8)
为什么会这样?
- U1:最大成员是char[5](5字节),对齐值是int的4。5向上取整到4的倍数 → 8。
- U2:最大成员是short[3](6字节),对齐值是double的8。6向上取整到8的倍数 → 16。
- U3:最大成员是int[2](8字节)和long long(8字节),都是8。对齐值是long long的8。8已经是8的倍数 → 8。
注意:不同编译器、不同平台的对齐规则可能略有差异。比如有些嵌入式编译器默认使用1字节对齐(packed),那联合体的大小就是最大成员的大小,没有填充字节。我在做ARM Cortex-M项目时就遇到过这种情况,当时排查了好久才发现是编译器对齐设置的问题。
12.4 联合体中的结构体成员
联合体里可以嵌套结构体。这时候计算大小就要把结构体当成一个整体来看。
struct Packed {
char a;
int b;
short c;
};
union Container {
struct Packed p;
double d;
};
先算struct Packed的大小。假设在默认对齐下:
- char a 在偏移0
- int b 对齐值4,从偏移4开始,占4字节
- short c 对齐值2,从偏移8开始,占2字节
- 结构体总大小10字节,对齐值4,向上取整到12
所以struct Packed的大小是12字节。联合体Container的最大成员是struct Packed(12字节)和double(8字节),取12。对齐值是double的8和struct Packed的4中的最大值,也就是8。12向上取整到8的倍数 → 16。
所以union Container的大小是16字节。
避坑指南:我曾经在定义网络协议报文时,把一个结构体和一个uint64_t放在联合体里。结构体里有个uint32_t字段,我以为联合体大小就是结构体的大小(假设12字节)。结果sizeof返回16,因为uint64_t的对齐要求是8。后来我加了个__attribute__((packed))才把大小压下来。嗯,嵌入式开发里这种细节特别容易踩坑。
12.5 联合体大小与位域
联合体里也可以有位域成员。位域的大小计算稍微特殊一点。
union BitFieldUnion {
struct {
unsigned int a : 3;
unsigned int b : 5;
unsigned int c : 8;
} bits;
unsigned int value;
};
这个联合体的大小是多少?
位域结构体bits的总位数是3+5+8=16位,也就是2字节。但unsigned int的对齐值是4,所以位域结构体的大小是4字节(向上取整到4的倍数)。联合体的最大成员是bits(4字节)和value(4字节),都是4。对齐值也是4。所以联合体大小是4字节。
你看,位域虽然只用了2字节,但联合体还是占了4字节。这就是对齐在作怪。
12.6 知识体系总结
我把联合体大小的核心逻辑画了张图,方便你理解:
这张图把计算流程串起来了。你写代码的时候,脑子里过一遍这三步,基本不会出错。
12.7 实用建议
最后给你几个我在项目中积累的经验:
- 明确对齐要求:跨平台代码里,用
sizeof而不是硬编码大小。不同平台的对齐规则可能不同。 - 考虑使用packed:在嵌入式或网络协议场景下,如果内存是稀缺资源,可以考虑用
__attribute__((packed))或#pragma pack(1)来取消对齐填充。 - 注意位域的对齐:位域成员的对齐行为在不同编译器下可能有差异,写可移植代码时要小心。
- 用sizeof验证:我习惯在代码里加个
static_assert来验证联合体的大小是否符合预期。比如:static_assert(sizeof(union MyUnion) == 8, "Unexpected union size");
一句话总结:联合体的大小 = 向上取整(最大成员大小,最严格对齐值)。记住这个公式,联合体大小计算就不再是难题。