DaemonCoder
从Nginx源码中学习C语言位域的使用
DaemonCoder | 2020-02-11 21:56:03

位域长什么样

如果你阅读过Nginx源码的话,可以发现大量的位域的使用。如 ngx_process_t 结构体,定义如下:
typedef struct {
ngx_pid_t pid;
int status;
ngx_socket_t channel[2];

ngx_spawn_proc_pt proc;
void *data;
char *name;

unsigned respawn:1;
unsigned just_spawn:1;
unsigned detached:1;
unsigned exiting:1;
unsigned exited:1;
} ngx_process_t;
Nginx的 master 进程在管理所有的 worker 进程时,会用一个数组来记录每个 worker 进程的相关数据,数组的每一项都是一个上述 ngx_process_t 结构体的实例。如果你对Nginx不太熟悉也没有关系,我们不关心Nginx实现的细节,本文我们只关注位域的使用。
仔细观察上述结构体的定义可以看到,最后几个成员变量的定义有些特殊,变量名之后还有一个冒号和一个数字,这个数字指定了变量在存储时占用的位数,这就是我们本文要说的位域(又叫位段)。结构体中的成员变量可以指定位域,指定位域的几个相邻的字段,可以被压缩存储。

为什么要用位域

再回到上述的例子,用到位域的几个字段(respawn、just_spawn、detached、exiting、exited),取值都只有两种情况:0和1。以 exited 字段为例,1表示已经退出,0表示没有退出。我们平时在写程序时,也会经常遇到这种场景,值只有很少的几种情况,而我们通常会把每个字段定义为 int 类型,或者像上面例子中的 unsigned 类型,这样每个字段都需要4字节来存储,明明一位就可以搞定的地方却用了32位,这是一种没必要的浪费。
前面我们也提到了,结构体中指定位域的几个相邻的字段,可以压缩存储。上面 ngx_process_t 结构体最后5个字段可以被存储在一个 unsigned 类型所占的内存中,总共占了32位,也就是4字节(为什么不是5位而是32位呢?这里是出于内存对齐的考虑)。如果不用位域,最后几个字段总共占用 5*32=160 位,也就是20字节。内存占用的差距一目了然。

位域只能使用在特写的类型上

使用位域时需要注意,C语言标准中规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、signed int 和 unsigned int,到了 C99,_Bool 也被支持了。但是编译器在具体实现时都进行了扩展,额外支持了 char、signed char、unsigned char 以及 enum 类型。所以对char等类型指定位域的代码虽然不符合C语言标准,但它依然能够被编译器支持。
这个是一个使用位域的示例:
// 未使用位域
printf("%lu\n", sizeof(struct {
    unsigned a;
    unsigned b;
    unsigned c;
}));    // 输出12,三个unsigned类型的大小

// 使用位域,大小被压缩成一个unsigned类型的大小
printf("%lu\n", sizeof(struct {
    unsigned a:1;
    unsigned b:1;
    unsigned c:1;
}));    // 输出4,一个unsigned类型的大小

位域指定的位数,不能超过类型本身的大小

如我们给一个 unsigned int 类型指定位域为64时,会在编译时报错:
struct {
unsigned a:64;
}
// error: width of bit-field 'a' (64 bits) exceeds width of its type (32 bits)

只有结构体中相邻的指定位域字段才可以被压缩存储

看下面的例子:
printf("%lu\n", sizeof(struct {
unsigned a:16;
unsigned b;
unsigned c:16;
})); // 输出:12
字段b没有用位域,所以a和c不能被压缩到一块存储,整体依旧是占用了12字节。

当共用的空间不足时,开启一个新的共用空间

看下面示例:
printf("%lu\n", sizeof(struct {
unsigned a:16;
unsigned b:1;
unsigned c:16;
})); // 输出:8
a、b、c 三个字段都是指定了位域,分别占用16位、1位、16位,前两个字段a和b会共用一个unsigned类型大小的空间,也就是32位,剩余32-16-1=15位空闲,字段c需要16位来存储,之前的剩余空间已经不足,所以会用一个新的unsigned大小来存储c,所以总共占用了两个unsigned大小,也就是8字节。

无名位域

使用位域时,可以用没有变量名的无名位域,示例如下:
printf("%lu\n", sizeof(struct {
unsigned a:16;
unsigned :14;
unsigned b:1;
unsigned c:16;
})); // 输出:8
上前一个示例相同,只不过用一个无名位域填充了第一个unsigned最后空闲的14位,这样b和c就共用了一块空间。
位域还可以指定位数为0,但是此时只能使用无名位域。


微信公共号:

头条号: