struct位域

struct位域

位域可以将成员变量拆分成bit的粒度. 用法一般是:

1
identifier(optional) attr(optional) : size

例如以下:

1
2
3
4
5
6
7
struct BITS{
    uint32_t d1 : 4;
    uint32_t d2 : 4;
    uint32_t d3 : 4;
    uint32_t d4 : 4;
    uint32_t : 16;
};

BITS的成员d1/d2/d3/d4各占4bits, 最后还有16bit的保留位. 所以sizeof(BITS)的大小是4.

稍微改动一下, 去掉16bit的保留位:

1
2
3
4
5
6
struct BITS{
    uint32_t d1 : 4;
    uint32_t d2 : 4;
    uint32_t d3 : 4;
    uint32_t d4 : 4;
};

现在BITS的大小是2吗? 不是的, sizeof(BITS)还是4.

再改动, 保留20bit空间:

1
2
3
4
5
6
7
struct BITS{
    uint32_t d1 : 4;
    uint32_t d2 : 4;
    uint32_t d3 : 4;
    uint32_t d4 : 4;
    uint32_t : 20;
};

现在BITS的大小是4吗? 不是的, sizeof(BITS)是8.

以上, BITS声明的位域数和不足uint32_t占位时, BITS占位是sizeof(uint32_t), 超过时, 这是sizeof(uint32_t)的整数倍. 这一点和struct保持一致.

再来改一笔, 把uint32_t改成uint8_t:

1
2
3
4
5
6
struct BITS{
    uint8_t d1 : 4;
    uint8_t d2 : 4;
    uint8_t d3 : 4;
    uint8_t d4 : 4;
};

现在BITS的大小是4吗? 不是的, sizeof(BITS)是2.

如果把某个uint8_t改成uint16_t, sizeof(BITS)依然是2, 如果改成uint32_t, 则sizeof(BITS)是4.

有以下结论:

struct位域占用的大小总是其最大标识符的整数倍.

我们可以单独写入或者读出每个成员:

1
2
3
bit.d1 = 1;
bit.d2 = 2;
bit.d3 = bit.d1 | bit.d2;

如果成员比较多则比较麻烦, 这时候可以使用union.

union初始化位域

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
union BITS{
    struct {
        uint32_t d1 : 4;
        uint32_t d2 : 4;
        uint32_t d3 : 4;
        uint32_t d4 : 4;
        uint32_t : 16;
    }u;
    uint32_t data;
};

我们可以直接初始化所有位域:

1
2
BITS bits;
bits.data = 0x0;

也可以通过data一次性设置所有位域:

1
bits.data = 0x12345678;

但是datad1-d4的对应关系如何? 则需要考虑系统的小大端.

小大端模式

小端: 低位Byte存低地址, 高位Byte存高地址;

大端: 低位Byte存高地址, 高位Byte存低地址;

可用以下代码判断:

1
2
3
4
5
union MODE
{
    uint16_t i = 1;
    uint8_t small;
};

如果是小端, 则small为true, 否则small为false.

如果是小端存储:

1
bits.data = 0x12345678;

输出d1-d4则是:

1
2
3
4
d1=8;
d2=7;
d3=6;
d4=5;

如果是大端存储, 输出d1-d4则是:

1
2
3
4
d1=1;
d2=2;
d3=3;
d4=4;

了解小大端存储方式对开发有一定的帮助, 比如下面一个例子:

设计一个debug接口, 用户set一个属性, 系统通过获取这个属性可以支持不同的debug模式, 但是因为某些原因, 只允许设置一个属性, 值是32位.

此时我们就可以用上面的BITS. 获取prop:

1
2
3
4
5
6
BITS prop;
prop.data = getprop("debug.prop");
int32_t debug_mode_value[MODE1] = prop.d1;
int32_t debug_mode[MODE2] = prop.d2;
int32_t debug_mode[MODE3] = prop.d3;
int32_t debug_mode[MODE4] = prop.d4;

仅用一个prop, 就可以支持同时设置多个debug属性.

我们来看一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
union BITS{
    struct {
        uint8_t d1 : 4;
        uint8_t d2 : 4;
        uint8_t d3 : 4;
        uint8_t d4 : 4;
    }u;
    uint32_t data;
};
BITS bits;
bits.data = 0x87654321;
//低16bits  0100, 0011, 0010, 0001

在小端存储系统中, 输出d1-d4是:

1
1 2 3 4

我们改动一下:

1
2
3
4
5
6
struct {
    uint8_t d1 : 4;
    uint8_t d2 : 2;
    uint8_t d3 : 4;
    uint8_t d4 : 4;
}u;

预计输出是:

1
2
//0001 //01   //1100    //0000
1      2      12        0

但是实际输出依然是:

1
1 2 3 4

原因是d1/d2已经占用6bit, 再加d3是10bit超过了uint8_t的8bit, 所以d1/d2补齐2bit按照8bit对齐.

我们可以在改动一下验证这个结论:

1
2
3
4
5
6
struct {
    uint8_t d1 : 4;
    uint8_t d2 : 2;
    uint8_t d3 : 2;
    uint8_t d4 : 4;
}u;

现在d1-d4的输出是:

1
2
//0001 //10 //00 //0011
1      2    0    3

符合预期d1-d3正好占8bit, 所以不会有补齐对齐操作.

位域如何实现的

通过编译器翻译后的汇编代码, 我们可以基本知道其原理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
union BITS{
    struct {
        uint8_t d1 : 4;
        uint8_t d2 : 2;
        uint8_t d3 : 2;
        uint8_t d4 : 4;
    }u;
    uint32_t data;
};

BITS bits;
bits.data = 0x87654321;

int d1 = bits.u.d1;
int d2 = bits.u.d2;
int d3 = bits.u.d3;
int d4 = bits.u.d4;

汇编后:

将值0x87654321赋值给bits.data, 这里比较好理解.

1
mov     DWORD PTR [rbp-28], -2023406815

接下来时获取d1的值:

1
2
3
4
movzx   eax, BYTE PTR [rbp-28]
and     eax, 15
movzx   eax, al
mov     DWORD PTR [rbp-4], eax

从首地址拿数据, 然后与0xFF(15)按位与.

再获取d2的数据:

1
2
3
4
5
movzx   eax, BYTE PTR [rbp-28]
shr     al, 4
and     eax, 3
movzx   eax, al
mov     DWORD PTR [rbp-8], eax

d1的区别在于, 右移4bit, 然后与0x3按位与, 这时是提取2bit.

再获取d3:

1
2
3
4
movzx   eax, BYTE PTR [rbp-28]
shr     al, 6
movzx   eax, al
mov     DWORD PTR [rbp-12], eax

有点不同, 为什么没有按位与的操作了? 因为这里是取的BYTE, 右移6bit就可以得到高位的2bit了.

d4则和d1类似, 只不过取值地址需要+1:

1
2
3
4
movzx   eax, BYTE PTR [rbp-27]
and     eax, 15
movzx   eax, al
mov     DWORD PTR [rbp-16], eax

所以, 位域操作在逻辑上和位操作是类似的, 也是通过移位和与或运算得到.

结论

综上, 总结struct位域:

  • struct大小是最大标识符的整数倍
  • union赋值struct位域需要考虑小大端
  • 位域不能横跨两个标识符, 此时需要补齐对齐
  • 位域也是通过移位和与或运算得到