C 语言位域(bit filed)

位域也叫做位段(bit field),使用位域能够节省结构体数据内存的占用。接下来,我们从以下几个方面来讲解下位域:

  1. 位域的作用
  2. 位域的语法
  3. 位域的存储

以下代码运行环境为: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 个位大小。

我们通过分析发现:

  1. 假设:年龄 age 一般最小值为 0, 最大值 100 左右即可,7 个比特位能够表示的最大值为 127,完全可以表示我们的年龄。不需要使用 4 字节,32 位来表示,浪费内存。
  2. 假设:分数 score 最小值是 0,最大值是 120。我们只需要 7 个位就能表示最大值 120,不需要使用 32 个位来表示。
  3. 假设:我们最多可能会有 15 个班级, grade 占用 4 个位就可以了,无需占用 32个位来表示。
  4. 性别 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 行代码为例,来讲解下位域各部分的规则和含义:

  1. unsigned int 叫做位域类型说明符。
    1. C 标准支持 signed int、unsigned int,很多编译器在这两个类型基础上扩展支持:char、unsigned char、short、unsigned short、_Bool 作为位域类型说明符。
    2. 位域类型说明符中 signed 表示该位域字段可正、可负,unsigned 表示该字段智能存储正数。
    3. 位域长度不能超过该类型的大小。即:如果位域类型说明符是 char 类型,则该位域长度不能超过 8 位;如果位域类型说明符是 int 类型,则该位域长度不能超过 32 位,以此类推…
  2. age 叫做位域名,通过该名字访问该位域字段。位域的名字可以省略,此时不能访问该位域字段,但是仍然占用内存空间。
  3. 冒号后面的数字表示该位域的长度,单位是:bit(位),其最大值不能大于类型说明符的最大位长度。最小值可以为 0,这种情况下该位于名字必须为空。

2.2 位域使用语法

  1. 位域不能取地址。这是由于地址是以字节为单位编号,而位域字段不能保证占用完整的字节。
  2. 位域不能是数组形式。
  3. 位域的值不能超过其表示的范围,否则是未定义的。

请看下面的示例代码:

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. 位域的存储

位域成员在存储的时候,有以下几种规则:

  1. 如果结构体中所有的位域成员的类型说明符相同,并且能够在一个存储单元中存储(类型说明符的大小为一个存储单元),这些成员会存储在同一个存储单元中。
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 字节。

最后,需要注意的是:位域的内存分配与内存对齐的实现方式依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的。

至此,关于位域的内容讲解完毕,希望对你有所帮助!

未经允许不得转载:一亩三分地 » C 语言位域(bit filed)
评论 (0)

7 + 2 =