4. 位域与内存对齐:位域的定义、位域的内存分配、位域与对齐的冲突、位域的跨平台问题
位域这东西,说白了就是C语言里一个「省着用」的机制。你想想看,有时候我们只需要一个标志位,比如「开关状态」,用int存太浪费了。位域就是让你按bit来分配空间。
我个人习惯是,在嵌入式驱动、协议解析这类场景里,位域用得特别多。但用不好,坑也不少。今天咱们就把它掰开揉碎了讲清楚。
4.1 位域的定义
位域的定义语法很简单,就是在结构体成员后面加个冒号和位数。比如:
struct DeviceStatus {
unsigned int power_on : 1; // 1 bit
unsigned int mode : 2; // 2 bits
unsigned int error : 3; // 3 bits
unsigned int reserved : 2; // 2 bits
};
这里每个成员占多少位,由冒号后面的数字决定。注意,位域成员的类型必须是整型或枚举类型,我一般用 unsigned int,避免符号位带来的麻烦。
unsigned int 而不是 int。否则符号扩展会让你怀疑人生。
4.2 位域的内存分配
位域怎么分配内存?嗯,这里有个关键点:编译器会尽量把相邻的位域塞进同一个存储单元。这个存储单元的大小,通常是 unsigned int 的大小(32位系统上是4字节)。
举个例子:
struct Example {
unsigned int a : 3;
unsigned int b : 5;
unsigned int c : 10;
};
这三个成员加起来一共18位,不到32位。所以编译器会把它们塞进同一个4字节单元里。整个结构体大小就是4字节。
但如果你这样写:
struct Example2 {
unsigned int a : 3;
unsigned int b : 30; // 超过剩余位数
};
a占3位,剩下29位。b需要30位,放不下了。这时候编译器会另起一个存储单元。所以 sizeof(Example2) 是8字节。
4.3 位域与对齐的冲突
这里就是坑最多的地方了。位域和内存对齐,有时候会「打架」。
我记得有一次做网络协议解析,定义了一个位域结构体:
struct PacketHeader {
unsigned int version : 4;
unsigned int type : 4;
unsigned int length : 16;
unsigned int flags : 8;
};
我以为它只占4字节。结果一打印 sizeof,发现是8字节。为什么?
因为编译器在 length 后面做了对齐。虽然 version 和 type 只占8位,但 length 是16位,它需要按2字节对齐。编译器会在 type 后面填充一些位,让 length 对齐到2字节边界。
说白了,位域的对齐规则,和普通成员一样。每个位域成员的对齐要求,取决于它的基础类型(比如 unsigned int 是4字节对齐)。
4.4 位域的跨平台问题
跨平台,这才是位域最大的痛点。我总结了一下,主要有三个问题:
- 存储单元大小不同:32位系统上存储单元是4字节,64位系统上可能是8字节。同一个位域结构体,在不同平台上大小不一样。
- 位序不同:大端和小端系统,位域的排列顺序是反的。比如
a : 1; b : 2;在小端系统上,a在低位,b在高位;大端系统上正好相反。 - 对齐规则不同:不同编译器对位域的对齐策略有差异。有的编译器会严格对齐,有的会尽量压缩。
举个例子,你在x86上写了一个位域结构体,放到ARM上跑,结果可能完全不一样。我当年在做一个跨平台项目时,就因为这个bug排查了整整两天。
下面这张图,帮你理清位域的核心知识点:
#pragma pack 或者手动操作。
最后总结一下:位域是个好东西,但用之前一定要想清楚你的场景。单平台、内存紧张、可读性优先,那就用。跨平台、需要精确控制布局,那就别偷懒,手动移位吧。
嗯,今天就聊到这儿。位域这块,多写几个例子,多踩几次坑,自然就熟了。