昨天偶然遇到有人问起如下的题目:

struct {
int a:1;
int b:2;
int c:3;
} test;

test.b = 2;
Q:test的内存里,16进制为多少

     因为很久没有写过位结构体的缘故,知识点有些生疏,仅能想起test内的成员a、b、c会将一个byte按标记位划分,回答得不好(看错位回答了0x2),让我们再复盘一下(以小端存储为例):

让我们画出test的2进制位
0 0 0 0 0 0 0 0
c c c b b a
给b成员赋值为2后
0 0 0 0 0 1 0 0
答案显而易见应该为0x4!!

       温故知新,下面将会详细梳理下位域的诞生及存储法则,存储法则是理解位域的重点!

一.位域产生的原因

     有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。例如开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。正是基于这种考虑,C语言又提供了一种叫做位域的数据结构。所谓"位域"是把一个字节中的二进制位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

      定义形式

struct 位域结构名 
{
位域列表
};
type [member_name] : width;

type:C语言标准还规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、signed int 和 unsigned int(int 默认就是signed int),到了 C99,_Bool 也被支持了,说到这里大家可能有疑问,在彭平时的学习开发中,肯定见过char、signed char、unsigned char以及enum类型的位域域名,但编译器在具体实现时都进行了扩展,额外支持了 char、signed char、unsigned char 以及 enum 类型,所以上面的代码虽然不符合C语言标准,但它依然能够被编译器支持

width:C语言标准规定,位域的宽度不能超过它所依附的数据类型的长度。通俗地讲,成员变量都是有类型的,这个类型限制了成员变量的最大长度

二.位域存储法则

C语言标准并没有规定位域的具体存储方式,不同的编译器有不同的实现,但它们都尽量压缩存储空间

1.法则一

当相邻成员的类型相同时,如果它们的位宽之和小于struct类型的bit大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的bit大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍

#include <stdio.h>

int main(){
struct {
unsigned m : 6;
unsigned n : 12;
unsigned p : 4;
} bs;
printf("%d\n", sizeof(bs));

return 0;
}

     m、n、p 的类型都是 unsigned int,在32位/64位系统上,int都是4byte,因此运行结构都是4,即bs大小为32bit,而位宽之和为22,小于32,所以在内存中它们会紧挨着存储。

sizeof(struct bs) 的大小之所以为 4,而不是 3,是因为要将内存对齐到 4 个字节,以便提高存取效率(空间换时间)

      假设将域名m的位宽设置为22,猜猜结构体的大小为变为多少?

C语言 - 位域(位域)详解_#include

     编写运行程序后,发现大小变为8!

     再给它写入数据看看会变成多少:

#include <stdio.h>
#include <stdint.h>
#include <string.h>

int main(){
union {
struct {
uint32_t m : 22;
uint32_t n : 12;
uint32_t p : 4;
} bs;

uint64_t d;
} my_union;

uint64_t *p = (uint64_t *)&my_union;

memset(&my_union, 0x00, sizeof(my_union));
printf("sizeof my_union.bs:%d,my_union:%d\n", sizeof(my_union.bs), sizeof(my_union));
printf("sizeof p:%d\n", sizeof(p));

my_union.bs.m = 0x111;
my_union.bs.n = 0x22;
printf("my_union.d:0x%lx\n", my_union.d); // 64bit需要加l!

return 0;
}

C语言 - 位域(位域)详解_#include_02

2.法则二

    当相邻成员的类型不同时,不同的编译器有不同的实现方案,GCC 会压缩存储,而 VC/VS 不会。

#include <stdio.h>
#include <stdint.h>
#include <string.h>

int main(){
union {
struct {
uint32_t m : 22;
uint8_t n:7;
uint32_t p : 4;
} bs;

uint64_t d;
} my_union;

uint64_t *p = (uint64_t *)&my_union;

memset(&my_union, 0x00, sizeof(my_union));
printf("sizeof my_union.bs:%d,my_union:%d\n", sizeof(my_union.bs), sizeof(my_union));
printf("sizeof p:%d\n", sizeof(p));

my_union.d = 0xf11223344;
printf("my_union.bs.m:0x%x\n", my_union.bs.m);
printf("my_union.bs.n:0x%x\n", my_union.bs.n);
printf("my_union.bs.p:0x%x\n", my_union.bs.p);

return 0;
}

C语言 - 位域(位域)详解_#include_03

3.法则三

    如果成员之间穿插着非位域成员,那么不会进行压缩,每个编译器下均为12Byte

#include <stdio.h>
#include <stdint.h>
#include <string.h>

int main()
{
union {
struct {
uint32_t m : 22;
uint32_t n;
uint32_t p : 4;
} bs;

uint64_t d;
} my_union;

uint64_t *p = (uint64_t *)&my_union;

memset(&my_union, 0x00, sizeof(my_union));
printf("sizeof my_union.bs:%d,my_union:%d\n", sizeof(my_union.bs), sizeof(my_union));
printf("sizeof p:%d\n", sizeof(p));

return 0;
}

C语言 - 位域(位域)详解_#include_04

4.总结&备注

       通过上面的分析,我们发现位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用&获取位域成员的地址是没有意义的,C语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号。

三.无名位域

       位域成员可以没有名称,只给出数据类型和位宽:

#include <stdio.h>
#include <stdint.h>
#include <string.h>

int main()
{
union {
struct {
uint32_t m : 22;
uint32_t :7; // 7bit不使用,留白
uint32_t p : 4;
} bs;

uint64_t d;
} my_union;

uint64_t *p = (uint64_t *)&my_union;

memset(&my_union, 0x00, sizeof(my_union));
printf("sizeof my_union.bs:%d,my_union:%d\n", sizeof(my_union.bs), sizeof(my_union));
printf("sizeof p:%d\n", sizeof(p));

return 0;
}

C语言 - 位域(位域)详解_位域_05

    无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。上面的例子中,如果没有位宽为7的无名成员,m、n 将会挨着存储,sizeof(struct bs) 的结果为4;有了这7位作为填充,m、n 将分开存储,sizeof(struct bs) 的结果为8。