C++ 的位域(BitFields)

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王晓华-吹泡泡的小猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值