4

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

 2 years ago
source link: https://blog.51cto.com/u_15635173/5567541
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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语言 - 位域(位域)详解_c语言

     编写运行程序后,发现大小变为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语言 - 位域(位域)详解_c语言_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语言 - 位域(位域)详解_c语言_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语言 - 位域(位域)详解_位域_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语言 - 位域(位域)详解_#include_05

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK