1 位域的定义
1.1 位域的存储
位域是 C++ 语言的一种重要的数据结构,它的主要作用是压缩存储结构,使之更紧凑,并能节省内存资源。位域的定义使用 struct 关键字,但是和定义 struct 不同的是,它需要为每个分量(属性)指定长度,但是每个分量可以和数据结构中其他正常的元素一样使用和初始化。一个典型的位域定义是这样的:
struct BitFields
{
unsigned int a : 4;
unsigned int b : 4;
unsigned int c : 4;
unsigned int d : 4;
};
BitFields bf{4,3,2,1}; //初始化 四个 分量
unsigned int 是存储单元的类型,C++ 的位域只支持 int、unsigned int 和 signed int 三种存储单元类型。在这个例子中,a、b、c、d 四个分量共占用 16 个位,所以它们被分配在同一个 32 位的 unsigned int 存储单元中。在这个 32 位的存储单元中,a、b、c、d 四个分量按照定义的顺序从逻辑低位开始存放,如图(1)所示:
图(1)位域的分量存储关系
图(1)展示的是给 a、b、c、d 四个分量分别赋值 4、3、2、1 的时候这个位域在整个 unsigned int 存储单元中的存储情况,这个是 C++ 语言的逻辑位置,至于这个位域在物理内存中的存放序列,则取决于系统是大端字节序还是小端字节序。在我的 intel CPU 上,这个 unsigned int 位段在内存中的存储情况依次是(从低地址位置开始):0x34,0x12,0x00,0x00。
1.2 分隔和跨存储单元
位域并不要求所有的比特都连续使用,一些分量之间可以分隔一些不使用的比特,没有命名的分量位置即被认为是分隔或占位,比如:
struct BitFields
{
unsigned int a : 16;
unsigned int : 4;
unsigned int b : 8;
unsigned int c : 4;
};
BitFields bf{4,3,2};
给 a 分配 16 个比特,然后隔 4 个比特,给 b 分配 8 个比特,给 c 分配 4 个比特,其存储关系如图(2)所示:
图(2)位域的无名分量存储
作为间隔的无名分量自动被置 0,如果你和我一样是 intel 的 CPU,则这个位域在内存中的存储情况依次是(从低地址位置开始):0x04,0x00,0x30,0x20。
位域定义时,有两种情况会导致位域占用多个存储单元,一种情况是分量的长度超过一个存储单元的限制,比如一个 unsigned int 就是 32 位,如果分量超过 32 位,则会将新增一个存储单元。当一个位域需要跨存储单元存放时,有一点需要注意,即一个分量不会被跨存储单元存放。也就是说,如果一个分量的长度超过了上一个存储单元的剩余位置,它会被整体安排在下一个存储单元,上一个存储单元中剩余的位置将不会被使用(作为无名分量,置 0)。比如:
struct BitFields
{
unsigned int a : 16;
unsigned int : 4;
unsigned int b : 8;
unsigned int c : 8;//超过 32 比特,被放在下一个存储单元的低位
};
BitFields bf{4,3,2};
在这个例子中,分量 c 会被安排在下一个存储单元中的低位,如图(3)所示:
图(3)位域的跨单元存储
这个位域的大小会变成 8 个字节,对于小端字节序的 intel CPU,这个位域在内存中的存储情况依次是(从低地址位置开始):0x04,0x00,0x30,0x00,0x02,0x00,0x00,0x00。
另一种占用多个存储单元的情况是在定义位域时变化存储单元类型,比如:
struct BitFields
{
unsigned int a : 16;
int b;
unsigned int c : 4;
};
虽然分量 a 只用了第一个存储单元的 16 个比特,但是 b 是数据结构的一个正常成员,需要分配一个完整的存储单元,所以分量 c 就被分配给了一个新的存储单元,整个 BitFields 共占用 3 个存储单元,两个 unsigned int ,一个 singed int。
2 位域的大小和对齐
位域作为一种特殊的 struct,也可以用 sizeof 操作符取大小,当然,也存在对齐问题。使用位域的数据结构在计算大小和对齐的时候,不是按照分量计算的,而是按照实际存储单元计算的。比如这个数据结构的定义:
struct BFStruct
{
unsigned int a : 16;
double b;
unsigned int c : 4;
};
assert(std::alignment_of_v<BFStruct> == 8);
因为这个数据结构中有个 double 类型,所以 BFStruct 的对齐大小是 8 (struct 的对齐是按照最大的那个成员的对齐方式对齐的,关于对齐的详细内容,请戳这篇《C++ 的对齐 alignof 和 alignas》),整个数据结构的大小是 24 字节。在内存中的情况是 a 占用 4 个字节,然后空 4 个字节填充对齐,然后是 b,占用 8 个字节,然后是 c,占用四个字节,最后是 4 个字节的填充。如果把 b 的类型换成 int,则数据结构的大小就会变成 12 字节,因为数据结构的整体变成了 4 字节对齐:
struct BFStruct
{
unsigned int a : 16;
int b;
unsigned int c : 4;
};
assert(std::alignment_of_v<BFStruct> == 4);
3 位域与联合
位域可以理解为是 struct 中的某个存储单元的特殊分配形式,在第一节已经介绍过在 struct 中位域分量和其他正常存储的数据结构成员可以同时存在。作为一种特殊形式的 struct,位域也可以和联合(union)一起使用,共享相同的存储空间。
struct BitFields
{
unsigned int a : 16;
unsigned int : 4;
unsigned int b : 8;
};
union TestUnion
{
BitFields bf;
unsigned int ak;
};
TestUnion tu;
tu.bf = {4,2};
assert(tu.ak == 0x00200004);
4 C++ 20 的扩展
C++ 20 对位域的分量初始化定义了一种新的形式,就是在定义位域的分量的时候指定分量的默认值:
struct BitFields
{
unsigned int a : 16 {4};
unsigned int b : 4 {3};
unsigned int c : 8 {2};
};
BitFields bf;
assert(bf.a == 4);
assert(bf.b == 3);
assert(bf.c == 2);
不仅可以省掉一两行初始化代码,在一些特殊场合获取有特殊的用途,总之,挺好。
5 注意事项
5.1 说明
旧的 C 语言资料会有位域长度的限制,即位域分量不能跨字节存储,也就是说,一个位域分量的最大长度不能超过 8 比特。现代 C++ 编译器已经没有这个限制了,超过 8 比特和跨字节定义位域分量都没有问题。还有一点就是虽然 C++ 允许在 int、unsigned int 和 signed int 三种存储单元类型上使用位域分量,但是有的编译器会进行扩展,比如支持 char、long、long long 等等。
5.2 初始化
使用位域时,一些注意事项需要明确。首先使用位域最好是采用对分量赋值的方式初始化,不要用 memset 之类的粗暴方式。主要原因是对分量扩展存储单元的规则理解不透,会导致大小计算错误。
5.3 分量溢出
对位域的分量赋值时,需要注意溢出的问题。比如一个 4 个比特的分量,如果赋值为 17,则设个分量的实际值是 1,因为溢出部分被截断了。现代 C++ 编译器都能正确处理溢出,不会污染到其他分量,但是是否会给出编译告警则取决于编译器,据我所知,大部分编译器对此都很漠然。
5.4 关于 bitset
C++ 的 STL 提供了 std::bitset,对于用单个的 bit 位表示开关量的场合,应考虑使用 std::bitset。
关注作者的算法专栏
https://blog.csdn.net/orbit/category_10400723.html
关注作者的出版物《算法的乐趣(第二版)》
https://www.ituring.com.cn/book/3180