位域也叫做位段(bit field),使用位域能够节省结构体数据内存的占用。接下来,我们从以下几个方面来讲解下位域:
- 位域的作用
- 位域的语法
- 位域的存储
以下代码运行环境为:win10 专业版 + vs2019 社区版。
1. 位域的作用
位域是一个结构体,使用 struct 关键字来进行定义。请先看下面普通结构体的定义语法:
struct Student { unsigned int age; unsigned int score; unsigned int grade; unsigned int gender; }; void test() { cout << sizeof(Student) << endl; // 输出:16 }
上述代码中,我们定义了一个 Student 结构体,其包含 4 个成员:age(年龄)、score(分数)、grade(年级)、gender(性别)。
其中每一个成员占用 4 个字节大小,那么整个 Student 占用 16 字节大小,64 个位大小。
我们通过分析发现:
- 假设:年龄 age 一般最小值为 0, 最大值 100 左右即可,7 个比特位能够表示的最大值为 127,完全可以表示我们的年龄。不需要使用 4 字节,32 位来表示,浪费内存。
- 假设:分数 score 最小值是 0,最大值是 120。我们只需要 7 个位就能表示最大值 120,不需要使用 32 个位来表示。
- 假设:我们最多可能会有 15 个班级, grade 占用 4 个位就可以了,无需占用 32个位来表示。
- 性别 gender 只需要表示两个值,即 1 个位就可以,也无需占用 32 位。
如果按照我们的分析,则 Student 结构体占用的内存大小将会大大减少。
问题是:我们给变量分配内存的最小单位是字节。但是,我们希望按照位(bit)为单位来分配内存。这个有办法实现吗?可以的,使用结构体位域。
简言之:位域能够使得结构体成员能够以位(bit)为单位分配内存。
2. 位域的语法
位域语法我们从两个方面来讲解:位域的定义语法、位域的使用语法
2.1 位域定义语法
位域由三个部分组成:位域类型说明符、位域名、位域长度。我们将 Student 结构体中的成员修改为位域形式,请看下面的示例代码:
struct Student { unsigned int age : 7; unsigned int score : 7; unsigned int grade : 4; unsigned int gender : 1; }; void test() { cout << sizeof(Student) << endl; // 输出:4 }
修改成位域形式之后,Student 的大小从 16 字节变成了 4 字节,节省了内存。
我们以第 3 行代码为例,来讲解下位域各部分的规则和含义:
- unsigned int 叫做位域类型说明符。
- C 标准支持 signed int、unsigned int,很多编译器在这两个类型基础上扩展支持:char、unsigned char、short、unsigned short、_Bool 作为位域类型说明符。
- 位域类型说明符中 signed 表示该位域字段可正、可负,unsigned 表示该字段智能存储正数。
- 位域长度不能超过该类型的大小。即:如果位域类型说明符是 char 类型,则该位域长度不能超过 8 位;如果位域类型说明符是 int 类型,则该位域长度不能超过 32 位,以此类推…
- age 叫做位域名,通过该名字访问该位域字段。位域的名字可以省略,此时不能访问该位域字段,但是仍然占用内存空间。
- 冒号后面的数字表示该位域的长度,单位是:bit(位),其最大值不能大于类型说明符的最大位长度。最小值可以为 0,这种情况下该位于名字必须为空。
2.2 位域使用语法
- 位域不能取地址。这是由于地址是以字节为单位编号,而位域字段不能保证占用完整的字节。
- 位域不能是数组形式。
- 位域的值不能超过其表示的范围,否则是未定义的。
请看下面的示例代码:
struct Student { unsigned int age : 7; unsigned int score : 7; unsigned int grade : 4; unsigned int gender : 1; }; void test() { Student student; // 1. 成员访问 student.age = 100; // 正确 student.age = 200; // 超出表示范围结果未定义 cout << student.age << endl; // 2. 不能对位域成员取地址 // cout << &(student.age) << endl; // 错误 }
3. 位域的存储
位域成员在存储的时候,有以下几种规则:
- 如果结构体中所有的位域成员的类型说明符相同,并且能够在一个存储单元中存储(类型说明符的大小为一个存储单元),这些成员会存储在同一个存储单元中。
struct Demo { int a : 5; int b : 7; int c : 9; }; void test() { cout << sizeof(Demo) << endl; // 输出:4 }
Demo 的位域成员 a、b、c 的类型说明符相同,并且共占用(5 + 7 + 9 = 21)个位, 21 个位一起存储在同一个 int 类型(32位)的存储单元中。
2. 如果结构体中所有的位域成员的类型说明符相同,但是,一个存储单元无法容纳所有的位域成员,则后面的位域存储在一个新的存储单元中。
struct Demo { int a : 5; int b : 25; int c : 9; }; void test() { cout << sizeof(Demo) << endl; // 输出:8 }
Demo 的位域成员 a 和 b 类型相同,其大小和为 30 位,小于存储单元的大小。所以,存储在同一个存储单元中。
c 占用 9 个位,在第一个存储单元中位置不足,所以,单独占用第二个存储单元。一个存储单元是最大的类型说明符大小(此处是 4),所以,Demo 共占用 8 个字节。
3. 如果存在匿名位域,下一个成员将会在新的存储单元中存储。
struct Demo { int a : 5; int : 0; // 无名位域 int b : 6; int : 0; // 无名位域 int c : 9; }; void test() { cout << sizeof(Demo) << endl; // 输出:12 }
Demo 的位域成员 a 存储在第一个存储单元中,由于其后面出现无名位域,导致位域成员 b 存储在第二个存储单元中。此时,后面又碰到了无名位域,导致位域 c 存储在第三个存储单元中。
虽然 3 个位域成员大小为 5 + 6 + 9 = 20 能够存储在同一个存储单元种,但是由于无名位域的原因,导致位域成员 a、b、c 占用 3 个存储单元,即 占用 12 字节的存储空间。
4. 如果相邻的位段的类型不同,下一个不同类型的成员需要存储在新的存储中。
struct Demo { int a : 5; int b : 6; char c : 7; char d : 7; }; void test() { cout << sizeof(Demo) << endl; // 输出:8 }
位域成员 a、b 同类型,并且大小和小于一个存储单元(位域成员中最大的类型),故而占用 4 字节内存空间。
位域成员 c 和 前面两个成员不同,故而需要占用一个新的存储单元,d 和 c 的类型相同,所以占用同一个存储单元。
所以,最终 Demo 占用 2 个存储单元,即 8 字节。
最后,需要注意的是:位域的内存分配与内存对齐的实现方式依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的。
至此,关于位域的内容讲解完毕,希望对你有所帮助!