引入问题

假设我们需要管理一些文件,其中除了文件的其他信息,我们还需要管理文件的权限,共有以下3种权限:

  • 是否可读—r
  • 是否可写—w
  • 是否可执行—x

每个文件的以上3种权限都不同,现在我们需要设置一个结构体来管理文件.

一种方法是使用3个unsigned int类型的变量作为标志来存储这些权限信息:

1
2
3
4
5
6
struct file_t {
char * file_name;
size_t file_size;
... // 其他文件信息
unsigned int r,w,x; // 分别记录3种权限
}

显然是可以的,但是一个unsigned int类型的成员就占用4字节空间(64位平台),3个成员就占用了12个字节,而实际上我们需要的仅仅是3个bit的标志位!

位段可以一定程度上节省空间,另一方面可以简化对位标志的操作.

C语言提供了位域(或译位段)来允许我们按位来定义成员并操作.

位域的声明和使用

位域是一种特殊的结构体,该结构体的成员是signed intunsigned int类型变量中的一组位(bit),我们仍然使用struct关键字来定义一个位域,并指定每一个成员所占用的位数.

1
2
3
4
5
struct permission {
unsigned int r : 1;
unsigned int w : 1;
unsigned int x : 1;
};

上述结构体位域有3个位域成员,分别为r,w,x,他们根据定义分别在结构体中占用1bit的空间,共占用3bit空间.

我们可以像访问结构体成员那样来访问各个位段成员,但是需要注意赋的值不能超过位段成员的存储范围,例如3bit(unsigned)的位段成员范围为[0,2^3-1].

例如如下代码说明文件file1的属性为只读,即仅可以读取,不能写入和执行:

1
2
3
4
struct permission file1;
file1.r = 1; // 允许读取
file1.w = 0; // 不允许写入
file1.x = 0; // 不允许执行

位域的兼容性与占用空间

兼容性与实现问题

位段有许多兼容性和实现问题:

  1. 位段成员如果未显式使用signed intunsigned int声明,则它究竟被解释为有符号还是无符号数是由编译器决定的.
  2. int被解释为有符号还是无符号也是实现问题.
  3. 位段成员的最大位长度,许多编译器将其限制在整型值的长度内.(注意32位和16位的整型值长度就不一样)
  4. 位段的成员在内存中的分布情况也是未定义的,取决于具体的实现.
  5. 当一个位段成员较大,无法放在前面所剩余的位中,编译器可能将其放在下一个内存位置对齐,也可能紧挨在前一个位段成员后面,造成重叠.
  6. 是否允许除int,unsigned int外类型的位域是未定义的.

第5点即:位域是否能越过分配单元边界,这是未定义的.

占用空间

位域整体是向整型类型对齐的. PS:可能有误

但是由于位域成员的各种基于实现的行为,无法说明位域成员的准确表现.不过我们有2种显式的填充和对齐方法:

使用未命名的字段来填充未命名的空间;使用未命名的0宽度的字段来迫使下一个字段向下一个整数对齐:

1
2
3
4
5
6
7
struct s{
unsigned int f1 : 1;
unsigned int : 2;
unsigned int f2 : 1;
unsigned int : 0;
unsigned int f3 : 1;
}

其中f1和f2之间有2bit的空位,f3将存储在(即对齐)下一个unsigned int的位置处(注意其在这一段内存中的位置仍旧是未定义的).

优缺点

位域的各种用途完全可以使用对应的位操作来替代,但是使用位域显然大大简化了操作—我们将他们视为结构体的成员.

尽管如此,位域在底层仍然无法避免对应的位偏移等操作,它只是对我们屏蔽了而已.

另一方面,位域有着一系列未定义的行为,亦即由实现决定,需要考虑其兼容性问题.

同时,如果需要的标志位的总长度很少,例如上面例子的3bit(r/w/x),可能并不能起到节省空间的作用—例如可以将这3bit的标志位压缩于一个unsigned char变量中.

总之,在使用位域之前,一定要把位域简化代码的优点和其移植性弱的缺点相权衡!


本章简单讲解了位域,除了一些底层的代码,一般使用并不多,了解即可.

——WAHAHA 2024.2.24 于火车上完成

开学了QWQ



上一篇:C语言教程-14_2-结构体与指针

下一篇:C语言教程-14_4-共用体