C语言之位域
一、位域的概念
位域又称位段,在C语言中,位域是一种特殊的结构体成员,它允许我们在结构体中以位为单位来指定成员的存储空间,而不是以字节为单位。这种方式可以更有效地利用内存,特别是在需要处理大量的布尔值或者需要对硬件寄存器进行精确控制的场景中。
二、位域的定义和声明
1.基本语法
位域的定义与普通结构体类似,只需要在结构体的成员后面跟上**:
和一个整数**,这个整数表示该成员所占位数。如下例:
// Win32
#include <stdio.h>
// 定义一个包含位域的结构体
struct Test {
unsigned int a : 3; // a 占 3 位
unsigned int b : 2; // b 占 2 位
unsigned int c : 1; // c 占 1 位
};
int main() {
struct Test bf;
bf.a = 5; // 5 的二进制表示为 101,在 3 位中可以正常存储
bf.b = 3; // 3 的二进制表示为 11,在 2 位中可以正常存储
bf.c = 1; // 1 的二进制表示为 1,在 1 位中可以正常存储
printf("a = %d, b = %d, c = %d\n", bf.a, bf.b, bf.c);
printf("sizeof(bf) = %lu\n", sizeof(bf));
return 0;
}
PS E:\code\c_code> cd "e:\code\c_code\" ; if ($?) { gcc test2.c -o test2 } ; if ($?) { .\test2 }
a = 5, b = 3, c = 1
sizeof(bf) = 4
2. 位域的类型
位域的类型必须是整型类型和字符类型(在 C99 及以后的标准中,也可以使用_Bool
类型)。通常建议使用unsigned int
类型,以避免符号位带来的问题。
// Win32
#include <stdio.h>
// 定义一个包含位域的结构体
struct Test {
unsigned char a : 3; // a 占 3 位
unsigned char b : 2; // b 占 2 位
unsigned char c : 1; // c 占 1 位
};
int main() {
struct Test bf;
bf.a = 5; // 5 的二进制表示为 101,在 3 位中可以正常存储
bf.b = 3; // 3 的二进制表示为 11,在 2 位中可以正常存储
bf.c = 1; // 1 的二进制表示为 1,在 1 位中可以正常存储
printf("a = %d, b = %d, c = %d\n", bf.a, bf.b, bf.c);
printf("sizeof(bf) = %lu\n", sizeof(bf));
return 0;
}
PS E:\code\c_code> cd "e:\code\c_code\" ; if ($?) { gcc test2.c -o test2 } ; if ($?) { .\test2 }
a = 5, b = 3, c = 1
sizeof(bf) = 1
三、位域的内存分布
1. 位域的存储顺序
位域在内存中的存储顺序取决于编译器和机器的字节序。一般来说,位域是从右向左存储的,即先分配的位域在低地址位。
2. 位域的对齐
位域的对齐规则也与编译器有关。通常情况下,位域会尽量在一个存储单元(如一个int类型的存储单元)内连续存储,如果一个存储单元不足以容纳所有位域,那么剩下的位域会被存储到下一个存储单元中。
// Win32
#include <stdio.h>
// 定义一个包含位域的结构体
typedef struct{
unsigned char a : 3; // a 占 3 位
unsigned char b : 5;
} tch1; // 这时一个字节的存储单元足够存储 a 和 b
typedef struct{ // unsiged char 储存单元为一个字节
unsigned char a : 3; // a 占 3 位
unsigned char b : 6; // b 占 6 位
} tch2; // 这时一个字节的存储单元不足,所有 b 都会存到下一个字节中
int main() {
printf("sizeof(tch1) = %lu\n", sizeof(tch1)); // 1
printf("sizeof(tch2) = %lu\n", sizeof(tch2)); // 2
return 0;
}
3. 位域成员不能跨字节存储数据
在大多数情况下,位域不会跨字节存储数据。当一个字节剩余的位数不足以存储下一个位域成员时,编译器会自动将下一个位域成员存储到下一个字节中。 当然,当位域成员存储有效数据超过指定位数时,会截取丢弃超过部分。 例子如下:
// Win32
#include <stdio.h>
// 定义一个包含位域的结构体
typedef struct{
unsigned char a : 5;// a 占 5 位
unsigned char b : 4;// b 占 4 位
} tch; // 2个 unsigned char 类型的储存单元,2字节
int main() {
tch bf={0};
bf.a = 95; // 95 的二进制表示为 1011111, 有效数据超过五位,所以只取低五位11111,即31
bf.b = 15; // 15 的二进制表示为 1111,在 4 位中可以正常存储,由于位域默认不能跨字节,所以b占用了第二个字节的前4位
char *p = (char *)&bf;
printf("sizeof(bf) = %lu\n", sizeof(bf));
printf("bf.a = %d, bf.b = %d\n", bf.a, bf.b);
printf("内存中第一个字节的二进制表示:");
for(int i = 7; i>=0;i--){
printf("%d",((*p)>>i)&1); // 输出一字节的每一位值,从高地址开始。
}
printf("\n");
printf("内存中第二个字节的二进制表示:");
p++;
for(int i = 7; i>=0;i--){
printf("%d",((*p)>>i)&1);
}
return 0;
}
PS E:\code\c_code> cd "e:\code\c_code\" ; if ($?) { gcc test3.c -o test3 } ; if ($?) { .\test3 }
test3.c: 在函数‘main’中:
test3.c:11:12: 警告:unsigned conversion from ‘int’ to ‘unsigned char:5’ changes value from ‘95’ to ‘31’ [-Woverflow]
11 | bf.a = 95; // 95 的二进制表示为 1011111, 有效数据超过五位,所以只取低五位11111,即31
| ^~
sizeof(bf) = 2
bf.a = 31, bf.b = 15
内存中第一个字节的二进制表示:00011111
内存中第二个字节的二进制表示:00001111
4. 位域成员不能跨类型存储数据
当存在多类型位域成员时,位域的存储,与编译器有关,GUN上的gcc会将所有位域成员进来放在一个较大类型的空间;
有些编译器就不会,会将不同类型成员单独分配空间。
// Win32
// gcc
#include <stdio.h>
// 定义一个包含位域的结构体
typedef struct{
unsigned char a : 5;
unsigned char b : 7;
unsigned int c : 10;
} test; // unsigned int 是较大类型空间,这里将所有位域成员都放在 unsigned int 类型的空间中。
int main() {
test bf = {0};
bf.a = 5; // 5 的二进制表示为 101,在 5 位中可以正常存储
bf.b = 3; // 3 的二进制表示为 11,在 7 位中可以正常存储
bf.c = 0x3f; // 0x3f 的二进制表示为 00111111,在 10 位中可以正常存储
printf("sizeof(bf) = %d\n", sizeof(bf));
printf("a = %d, b = %d, c = %d\n", bf.a, bf.b, bf.c);
unsigned int *p = (unsigned int *)&bf;
printf("按位从高位向低位32位二进制值:\n");
for (int i = 31; i >= 0; i--) { // 从高位到低位输出32 位
printf("%d", (*p >> i) & 1);
if (i % 8 == 0) {
printf("\n");
}
}
return 0;
}
PS E:\code\c_code> cd "e:\code\c_code\" ; if ($?) { gcc test3.c -o test3 } ; if ($?) { .\test3 }
sizeof(bf) = 4
a = 5, b = 3, c = 63
按位从高位向低位32位二进制值:
00000000
00011111
10000011
00000101
四、无名位域
顾名思义,无名位域就是没有名字的位域。无名位域里面不存放有效数据,也难以访问数据,它只是起到一个占位填充的作用,只是帮助我们管理位域成员在这个类型的空间的存放位置。
struct B{
unsigned x:3;
unsigned :5; // 这就是无名位域, 没有它,x与y会存在一个字节中。
unsigned y:5;
unsigned :3; // 这就是无名位域
unsigned z:7;
}