request-free-img

彻底弄懂 C 语言结构体位域:贴近硬件的强大特性

在C语言中,结构体位域的操作能展示出C语言贴近硬件的特点。

然后它却令很多C语言新手望而却步,即使许多C语言老手也未必全懂。

今天我们就一起来彻底弄懂C语言的结构体位域字段。

结构体位域的基本语法示例

struct BitField {
    unsigned int a : 3; // a 占用 3 位
    unsigned int b : 5; // b 占用 5 位
    unsigned int c : 20; // c 占用 20 位
};

这是一个C语言中结构体位域的例子。

位域字段通常使用 int 或 unsigned int类型,再用冒号分隔,冒号后边指定字段占用的位宽。

在我们的示例中a占用3位,B占用5位,C占用20位。

位域的定义规则与限制

位宽必须小于等于类型所支持的位数,例如我们的示例中将C改成60位,这样是不允许的,编译会报错。

同时位域的字段值也不能超过指定位宽所能表示的值的范围。

在我们的示例中a的位宽是3位,因此它的最大值为 2^3 – 1 =7 ,也就是最大只能表示7。

位域的内存占用特点

位域字段会尽量填满当前的 unsigned int,在我们的示例 结构中,ABC三个字段一共只占用28位。

因此这些字段会存储在一个 32 位的存储单元中。

所以你别看这个结构体中有3个INT字段,其实它只是用一个INT的不同位来分别保存ABC的值。

不信,我们查看一下这个结构的总大小。

看到了吗,它只有4个字节大小,也就是一个INT类型的大小。

这正是位域有趣的地方。

位域在内存中的存储方式(小端模式)

那位域在内存中是如何存储的呢?

其实它在内存中的存储方式和存储一个INT值的方式是一样的。

我们这里以小端模式(Little Endian)来做为示例讲解。

为了让大家能更深入的理解,这里对小端模式做个简单的知识普及。

小端模式是现代计算机最常见的字节序方式。

小端模式的特点是对于一个多字节数据(如 int ),它的低字节存储在内存的低地址,高字节存储在高地址。

例如一个32位整数 0x12345678,它以小端模式在内存中的存储方式就是 0x78 0x56 0x34 0x12。

没错,它和你看到的是反过来的。

赋值示例与内存布局推导

好,现在回到我们的示例中。

假设我们给这个结构体的A,B, C都赋值为1。

我们来推导一下它在内存中的值。

A存储在这个INT的最低位,占用3个位宽,我们给他赋值1,所以它是 001。

B存储在INT的3-7位,占5个位宽,我们也给他赋了值1,所以它是00001。

C占用20个位宽,存储在INT的8-27位,我们也给他赋值1,所以它是这样 0000 0000 0000 0000 0001。

把A,B,C组合到一起就是这样 0000 0000 0000 0000 0001 0000 1001。

再转成16进制就是 0x00000109。

我们上边说过在小端模式下它在内存中的保存形式是和你看到的反过来的。

因此,理论上它会在内存中这样保存 09 01 00 00。

我们验证一下,运行这个程序。

再看查F变量的内存值,没错,就是09 01 00 00。

位域字段的读取原理

明白了写入,存储的原理。它位域字段值又是如何被读取的呢?

其实这最终只是编译器通过 掩码(mask) 和 移位操作 来访问这些位域字段。

例如我们访问f.a 编译器会确定 a 的起始位置和位宽,我们示例 中A在最低位,位宽是三位,那么就编译器会通过用0X7和结构体原始值做位与操作就可以得到A的值(raw & 0x7 ) = a。

而对于B的值,因为它位于第3-7位,所以需要清除不属于 b 的位,保留第 3-7 位。最后再右移 3 位就得到了B的值 (raw & 0xF8) >> 3 = b。

取C值也是类似。

位域的实际应用场景

那结构体位域这么复杂,到底有什么用呢?

第一种用途,硬件寄存器操作。

硬件寄存器通常由若干位字段组成,每个位或几位有特定含义。使用位域刚好可以直接操作这些位。

struct Register {
    unsigned int data : 5; // 数据字段,占5位
    unsigned int mode : 2; // 模式字段,占2位
    unsigned int flag : 1; // 标志字段,占1位
};

第二种是通信协议解析。

很多通信协议使用固定的位结构,比如IP头、TCP头等,用位域来解析这些协议字段再好不过了。

例如我们的TCP协议头的结构体大至是这样的:

struct TCPHeader {
    uint16_t sourcePort; // 源端口号
    uint16_t destPort; // 目标端口号
    uint32_t sequenceNumber; // 序列号
    uint32_t ackNumber; // 确认号
    uint8_t dataOffset : 4; // 数据偏移(高4位)
    uint8_t reserved : 3; // 保留字段(中3位)
    uint8_t flags : 9; // 控制位(URG, ACK, PSH, RST, SYN, FIN 等)
    uint16_t windowSize; // 窗口大小
    uint16_t checksum; // 校验和
    uint16_t urgentPointer; // 紧急指针
    // 可选字段和填充不包括在此基础定义中,可以动态处理
};

其次位域也可以用在内存有限的嵌入式设备,或者在需要模拟底层位操作的场景中。

使用位域的注意事项

然而位域的存储顺序和对齐方式可能因平台或编译器不同而有所不同。

并且访问位域字段需要额外的位操作,使得访问效率会略低于普通成员变量。

使用时务必要多加注意。


更多问题探讨,请关注公众号:程序员角