LV150-位域
一、位域简介
1. 什么是位域
计算机的内存是以字节为单位,为变量分配内存,也是以字书为单位。但是,实际上,有些信息的存储,并不需要占用一个字节,只需要 1 个或几个二进制位就够了。
比如:人的性别,只有两种可能的取值男和女,我们就可以用 0 表示男, 1 表示女,这样使用 1 个二进制位就够了。多数情况,我们会选择 char 类型,占用 1 个字节,但是如果可以用一个位来表示,那有何必浪费 8 个位呢,毕竟在一些内存很小的 CPU 中,内存空间是非常珍贵的。
为了节省存储空间, C 语言中又提供了一种数据结构,称为 位域 或 位段,
所谓位域是把 一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
【位域的本质】本质其实就是一种结构类型,只不过成员是按照二进制位进行分配的。
【注意】位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用 & 获取位域成员的地址是没有意义的,C 语言也禁止这样做。地址是字节( Byte )的编号,而不是位( Bit )的编号。
2. 怎么定义?
作为一种数据类型,位域的定义与结构体很类似,都是使用关键字 struct ,一般格式如下:
struct bitField_name
{
data_type member_name1: len1;
data_type member_name2: len2;
...
data_type member_nameN: lenN;
};各部分说明如下:
| struct | 位域定义的关键字,表明这是一个位域的定义 |
| structure_name | 位域解耦股的名称,与普通变量名要求一样 |
| data_type | 位域内成员的数据类型。 |
| member_nameN | 位域内成员的名称(成员变量名),命名规则与变量相同 |
| lenN | 位域内成员的长度(以 bit 为单位) |
(1)位域更像是定义在结构体内部的一种特殊的成员,在定义结构体的时候可以直接定义位域成员。例如,
struct data{
unsigned m;
unsigned n: 4;
unsigned char ch: 6;
};: 后面的数字用来限定成员变量占用的位数。成员 m 没有限制,根据数据类型即可推算出它占用 4 个字节( Byte )的内存。成员 n 、 ch 被 : 后面的数字限制,不能再根据数据类型计算长度,它们分别占用 4 、 6 位( Bit )的内存。
(2)位域的占用位数不能超过 8 个二进制位,同时 位域也不允许跨字节。
3. 匿名位域
在使用位域时,如果有需要可以选择跳过某些位不使用,其方法是在结构体类型中定义位域成员时,只指定成员占用的二进制位数,而不定义成员名,这种可以叫 匿名位域,或者无名位域。由于被跳过的这些位域成员没有名字,因此在程序中也无法进行引用。没有域名的位域匿名位域一般用来作填充或者调整成员位置。例如,
struct data{
int m: 12;
int : 20; /* 该位域成员不能使用 */
int n: 4;
};如果没有位宽为 20 的无名成员, m 、 n 将会挨着存储, sizeof(struct data) 的结果为 4 ;有了这 20 位作为填充, m 、 n 将分开存储, sizeof(struct data) 的结果为 8 ,具体为什么,后边再分析。
Tips:特别的,我们可以根据需要定义二进制位数为 0 的匿名位域成员,目的是指定某些位域从一个新的内存单元开始存放。
4. 数据类型
4.1 有哪些?
C 语言标准还规定,只有有限的几种数据类型可以用于位域。C99 规定 int、unsigned int 和 bool 可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。
| 类型分类 | 具体类型 |
|---|---|
| 有符号整数 | signed char, short, int, long, long long |
| 无符号整数 | unsigned char, unsigned short, unsigned int, unsigned long, unsigned long long |
| 字符类型 | char(具体是否有符号取决于实现) |
| 布尔类型 | _Bool(C99 及以后) |
4.2 使用实例
一个示例(这里是在 ubuntu18.04 上测试,gcc 版本为 7.5.0):
#include <stdio.h>
#include <stdint.h> // C99 标准整数类型
// 定义一个包含所有可用位域类型的结构体
struct AllBitFields {
// 有符号整数类型
signed char sc : 3; // 范围: -4 ~ 3
short si : 4; // 范围: -8 ~ 7
int i : 4; // 范围: -8 ~ 7
long li : 5; // 范围: -16 ~ 15
long long lli : 6; // 范围: -32 ~ 31
// 无符号整数类型
unsigned char uc : 4; // 范围: 0 ~ 15
unsigned short us : 5; // 范围: 0 ~ 31
unsigned int ui : 6; // 范围: 0 ~ 63
unsigned long ul : 7; // 范围: 0 ~ 127
unsigned long long ull : 8; // 范围: 0 ~ 255
// char 类型
char c : 3; // 范围取决于实现,通常 -4 ~ 3 或 0 ~ 7
// 布尔类型 (C99)
_Bool flag1 : 1;
_Bool flag2 : 1;
};
int main() {
struct AllBitFields bf = {0};
// ==== ==== == 测试有符号整数位域 == ==== ====
bf.sc = 3; // 最大正值
bf.si = 7; // 最大正值
bf.i = 7; // 最大正值
bf.li = 15; // 最大正值
bf.lli = 31; // 最大正值
// ==== ==== == 测试无符号整数位域 == ==== ====
bf.uc = 15; // 最大值 2^4-1
bf.us = 31; // 最大值 2^5-1
bf.ui = 63; // 最大值 2^6-1
bf.ul = 127; // 最大值 2^7-1
bf.ull = 255; // 最大值 2^8-1
// ==== ==== == 测试 char 位域 == ==== ====
bf.c = 5;
// ==== ==== == 测试布尔位域 == ==== ====
bf.flag1 = 1;
bf.flag2 = 0;
// ==== ==== == 打印输出 == ==== ====
printf("=== 有符号整数位域 ===\n");
printf("signed char (3位): %d\n", bf.sc);
printf("short (4位): %d\n", bf.si);
printf("int (4位): %d\n", bf.i);
printf("long (5位): %d\n", bf.li);
printf("long long (6位): %d\n", bf.lli);
printf("\n=== 无符号整数位域 ===\n");
printf("unsigned char (4位): %u\n", bf.uc);
printf("unsigned short (5位): %u\n", bf.us);
printf("unsigned int (6位): %u\n", bf.ui);
printf("unsigned long (7位): %lu\n", bf.ul);
printf("unsigned long long (8位): %llu\n", bf.ull);
printf("\n=== char 位域 ===\n");
printf("char (3位): %d\n", bf.c);
printf("\n=== 布尔位域 ===\n");
printf("flag1: %d\n", bf.flag1);
printf("flag2: %d\n", bf.flag2);
printf("\n=== 结构体大小 ===\n");
printf("sizeof(struct AllBitFields) = %zu 字节\n",
sizeof(struct AllBitFields));
return 0;
}可以得到如下打印信息:
sumu@virtual-machine:~/hk/alpha$ gcc main.c -Wall
main.c: In function ‘main’:
main.c:64:39: warning: format ‘%lu’ expects argument of type ‘long unsigned int’, but argument 2 has type ‘int’ [-Wformat=]
printf("unsigned long (7位): %lu\n", bf.ul);
~~^ ~~~~~
%u
main.c:65:43: warning: format ‘%llu’ expects argument of type ‘long long unsigned int’, but argument 2 has type ‘int’ [-Wformat=]
printf("unsigned long long (8位): %llu\n", bf.ull);
~~~^ ~~~~~~
%u
sumu@virtual-machine:~/hk/alpha$ ./a.out
=== 有符号整数位域 ===
signed char (3位): 3
short (4位): 7
int (4位): 7
long (5位): 15
long long (6位): 31
=== 无符号整数位域 ===
unsigned char (4位): 15
unsigned short (5位): 31
unsigned int (6位): 63
unsigned long (7位): 127
unsigned long long (8位): 255
=== char 位域 ===
char (3位): -3 ← 我们写入 5 时,实际存储的是 101,读取时按有符号理解就是 -3
=== 布尔位域 ===
flag1: 1
flag2: 0
=== 结构体大小 ===
sizeof(struct AllBitFields) = 8 字节5. 优势与缺点
- 这种数据结构的好处:
(1)可以使数据单元节省储存空间,当程序需要成千上万个数据单元时,这种方法就显得尤为重要。
(2)位段可以很方便的访问一个整数值的部分内容从而可以简化程序源代码。
- 位域也有它的缺点:
缺点在于,其内存分配与内存对齐的实现方式依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的。不同的 IDE 在处理位段这种数据结构的时候确实会有很大的不同。比如 vs 和 DEV-C++。
二、位域变量
位域也是一种数据类型,所以自然也可以用于定义变量,位域变量的定义与结构体一样。
1. 怎么定义?
1.1 方式一
- 先定义位域数据类型,再声明位域变量
/* 定义位域数据类型 */
struct data
{
unsigned int a: 1;
unsigned int b: 3;
unsigned int c: 4;
}
struct data d1, d2;定义了两个位域变量 d1 和 d2 ,它们都是 data 位域类型,都由 3 个成员组成。
【注意】
(1)关键字 struct 不可以省略,若没有这个关键字,则系统不认为 data 是位域类型。
(2) data 就像一个“模板”,定义出来的变量都 具有相同的性质。
1.2 方式二
- 定义位域数据类型的同时声明位域变量
/* 定义位域数据类型同时声明位域变量 */
struct data
{
unsigned int a: 1;
unsigned int b: 3;
unsigned int c: 4;
}d1, d2;与方式一类似,定义了两个位域变量 d1 和 d2 ,它们都是 data 位域类型,都由 3 个成员组成。
【说明】如果只需要 d1 、 d2 两个变量,后面不需要再使用位域结构名称定义其他变量,那么在定义时也可以不给出位域结构名称。
/* 定义结位域数据类型同时声明位域变量 */
struct
{
unsigned int a: 1;
unsigned int b: 3;
unsigned int c: 4;
}d1, d2;2. 怎么使用?
位域成员的使用和结构体成员的使用方式相同,可以对位域变量成员进行引用和赋值,这些操作都是通过成员运算符 . 完成的。
- 引用位域变量成员的方式如下:
位域变量名.位域名1
位域变量名.位域名2
...
位域变量名.位域名N- 对位域变量成员赋值的方式如下:
位域变量名.位域名1 = value1;
位域变量名.位域名2 = value2;
...
位域变量名.位域名N = valueN;【注意】赋值时不要超出位域定义时的位数。
2.1 赋值方式一
- 先定义位域变量,再赋值
#include <stdio.h>
/* 定义位域数据类型 */
struct data
{
unsigned int a: 1;
unsigned int b: 3;
unsigned int c: 4;
};
int main(int argc, char *argv[])
{
struct data d1; /* 定义位域变量 */
d1.a = 1; /* 0000 0001 */
d1.b = 6; /* 0000 0110 */
d1.c = 13;/* 0000 1101 */
printf("%d\t%d\t%d\n", d1.a, d1.b, d1.c);
return 0;
}在终端执行以下命令:
gcc test.c -Wall # 编译程序
./a.out # 执行可执行文件会看到有如下信息输出:
1 6 132.2 赋值方式二
- 先定义位域,然后在定义位域变量的时候进行赋值
#include <stdio.h>
/* 定义位域数据类型 */
struct data
{
unsigned int a: 1;
unsigned int b: 3;
unsigned int c: 4;
};
int main(int argc, char *argv[])
{
struct data d1 = {1, 6, 13}; /* 定义位域变量 1:0000 0001 6:0000 0110 13:0000 1101*/
printf("%d\t%d\t%d\n", d1.a, d1.b, d1.c);
return 0;
}在终端执行以下命令:
gcc test.c -Wall # 编译程序
./a.out # 执行可执行文件会看到有如下信息输出:
1 6 132.3 赋值方式三
- 定义位域结构体时直接对结构体变量赋值
#include <stdio.h>
/* 定义位域数据类型 */
struct data
{
unsigned int a: 1;
unsigned int b: 3;
unsigned int c: 4;
}d1 = {1, 6, 13};
/* 定义位域变量 1:0000 0001 6:0000 0110 13:0000 1101*/
int main(int argc, char *argv[])
{
printf("%d\t%d\t%d\n", d1.a, d1.b, d1.c);
return 0;
}在终端执行以下命令:
gcc test.c -Wall # 编译程序
./a.out # 执行可执行文件会看到有如下信息输出:
1 6 133. 嵌套在其他数据结构中
直接看实例:
#include <stdio.h>
/* 定义共用体 */
union x_bit
{
char m;
/* 定义位域数据类型 */
struct data
{
unsigned int a: 2;
unsigned int b: 4;
unsigned int c: 2;
}d;
};
int main(int argc, char *argv[])
{
union x_bit x;/* 定义共用体变量*/
x.d.a = 2;/* 10 */
x.d.b = 5;/* 0101*/
x.d.c = 1;/* 01 */
/* 01 0101 10 --> 0101 0110*/
printf("x.d.a = %d(%#x)\n", x.d.a, x.d.a);
printf("x.d.b = %d(%#x)\n", x.d.b, x.d.b);
printf("x.d.c = %d(%#x)\n", x.d.c, x.d.c);
printf("x.m = %d(%#x)\n", x.m, x.m);
return 0;
}在终端执行以下命令:
gcc test.c -Wall # 编译程序
./a.out # 执行可执行文件会看到有如下信息输出:
x.d.a = 2(0x2)
x.d.b = 5(0x5)
x.d.c = 1(0x1)
x.m = 86(0x56)这里的 CPU 是小端模式,低位存放在低的地址,所以组合方式就是 01 0101 10 合并为一个字节就是 0101 0110 ,也就是 0x56 换算为十进制就是 86 了。
三、位域的长度
1. 长度计算
当我们定义了一个位域后,用它定义出来的位域变量占多大空间呢?我们可以使用 sizeof 函数来求得所定义位域数据类型的长度。一般格式如下:
sizeof( struct bitField_name)
/* 或者使用结构体变量来求 */
struct bitField_name bitField_variable;
sizeof(bitField_variable)【注意】我们无法使用该函数求位域内成员的长度,即便位域成员长度超过一个字节,也无法使用,一般会报如下错误:
test.c:18:47: error: cannot take address of bit-field ‘a’我们当然可以手动计算位域的大小,但是 比较复杂,且依赖编译器实现。
2. 使用实例
2.1 示例 1:简单位域
#include <stdio.h>
struct A {
unsigned int a : 3; // 占 3 位
unsigned int b : 5; // 占 5 位
unsigned int c : 8; // 占 8 位
unsigned int d : 16; // 占 16 位
};
int main(int argc, char *argv[]) {
printf("struct A: %zu bytes\n", sizeof(struct A));
return 0;
}分析过程如下:
假设 int 为 32 位 (4字节)
a: 3位 → 可以放在第一个int的低3位
b: 5位 → 第一个int的低8位可以容纳(3+5=8)
c: 8位 → 第一个int剩余24位,可以放下
(3+5+8=16,还剩16位)
d: 16位 → 第一个int剩余16位,正好放下!
(3+5+8+16=32)
结果:1个int = 4字节2.2 示例 2:跨边界
#include <stdio.h>
struct B {
unsigned int a : 10; // 占 10 位
unsigned int b : 20; // 占 20 位
unsigned int c : 2; // 占 2 位
};
int main(int argc, char *argv[]) {
printf("struct B: %zu bytes\n", sizeof(struct B));
return 0;
}分析过程如下:
a: 10位 → int[0] 的低10位
b: 20位 → int[0] 剩余22位不够存20位吗?
→ 实际可以!(22 >= 20)
c: 2位 → int[0] 剩余2位,正好放下!
结果:1个int = 4字节2.3 示例 3
#include <stdio.h>
/* 定义位域数据类型 */
struct data
{
unsigned int a: 1;
unsigned int b: 3;
unsigned int c: 4;
};
int main(int argc, char *argv[])
{
struct data d1 = {1, 6, 13};/* 定义位域变量 1:0000 0001 6:0000 0110 13:0000 1101*/
printf("sizeof(unsigned int) = %ld\n", sizeof(unsigned int));
printf("sizeof(d1) = %ld\n", sizeof(d1));
printf("sizeof(struct data) = %ld\n", sizeof(struct data));
printf("%d\t%d\t%d\n", d1.a, d1.b, d1.c);
return 0;
}在终端执行以下命令:
gcc test.c -Wall # 编译程序
./a.out # 执行可执行文件会看到有如下信息输出:
sizeof(unsigned int) = 4
sizeof(d1) = 4
sizeof(struct data) = 4
1 6 13具体的那我们就接着往后看。存储方式,我们下节会通过实例来说明。
四、在内存中的存储
1. 非法操作?
当位域数据类型定义了一个位域变量的时候,这个位域以及其成员变量在内存中是怎样的呢?我么是否可以取出位域成员的地址来分析呢?
在 C 语言中,我们可以得到某个字节的内存地址,我们具备了操作任意内存字节的能力;但是在内存空间稀缺的年代,仅仅控制到字节级别还不足以满足 C 程序员的需求,为此 C 语言中又出现了 bit 级别内存的“有限操作能力” —— 位域。这里所谓的“有限”指的是机器的最小粒度寻址单位是字节,我们无法像获得某个字节地址那样得到某个 bit 的地址,因此我们仅能通过字节的运算来设置和获取某些 bit 的值。在 C 语言中,尝试获得一个 bit field 的地址是非法操作。
2. 内存低地址向高地址分配
2.1 分配规则
位域的存储都是由内存低地址向高地址分配,即从低地址字节的低位 bit 开始向高地址字节的高位 bit 分配空间。为了方便,这里将存储到同一个标准数据类型的所有成员称为一个域,同一个域内的成员称为域成员。如下面的结构体中 a1 和 a2 属于同一个域,a1 和 a2 是这个域的域成员;b1、b2、b3、b4 属于同一个域,b1、b2、b3、b4 是这个域的域成员。
有如下 C/C++结构体 struct st1 及变量 data:
struct st1
{
unsigned char a1 : 2;
unsigned char a2 : 6;
unsigned short b1 : 3;
unsigned short b2 : 4;
unsigned short b3 : 5;
unsigned short b4 : 4;
};
struct st1 data ={
.a1 = 0x1,
.a2 = 0x3,
.b1 = 0x4,
.b2 = 0x8,
.b3 = 0xb,
.b4 = 0x3
};结构体成员.a1 和.a2 属于同一个域,域大小为 8bit。.b1、.b2、.b3、.b4 属于同一个域,域大小为 16bit。域的定义一般要遵循如下原则:域大小可以是 8/16/32/64bit,域之间不可交叉,域的所有成员总大小必须等于所属域大小,必要时加入保留(reserved)位来填补。
- 在大端系统中,结构体变量及其成员的存储情况如下:
.a1 .a2 .b1 .b2 .b3 .b4
bit [00:01] [02:07] [08:10] [11:14] [15:19] [20:23]
data 01 000011 100 1000 01011 0011
.a1[0:1] = 0b01(0x1);
.a2[0:5] = 0b000011(0x3);
.b1[0:2] = 0b100(0x4);
.b2[0:3] = 0b1000(0x8);
.b3[0:4] = 0b01011(0xb);
.b4[0:3] = 0b0011(0x3);- 在小端系统中,各个结构体成员的存储情况如下:
.a1 .a2 .b1 .b2 .b3 .b4
bit [01:00] [07:02] [10:08] [14:11] [19:15] [23:20]
data 01 000011 100 1000 01011 0011
.a1[1:0] = 0b01(0x1);
.a2[5:0] = 0b000011(0x3);
.b1[2:0] = 0b100(0x4);
.b2[3:0] = 0b1000(0x8);
.b3[4:0] = 0b01011(0xb);
.b4[3:0] = 0b0011(0x3)2.2 示例
下面是一个实例(GLM5.0 写的,大模型还是好使哈):
#include <stdio.h>
#include <stdint.h>
#include <string.h>
// ==== ==== ==== ==== ==== 测试 1: 1 字节内的位域分配 ==== ==== ==== ==== ====
typedef struct {
unsigned int b0 : 1; // bit0 (最低位)
unsigned int b1 : 1;
unsigned int b2 : 1;
unsigned int b3 : 1;
unsigned int b4 : 1;
unsigned int b5 : 1;
unsigned int b6 : 1;
unsigned int b7 : 1; // bit7 (最高位)
} BitField1Byte;
// ==== ==== ==== ==== ==== 测试 2: 跨字节的位域分配 ==== ==== ==== ==== ====
typedef struct {
unsigned int b0 : 1;
unsigned int b1 : 1;
unsigned int b2 : 1;
unsigned int b3 : 1;
unsigned int b4 : 1;
unsigned int b5 : 1;
unsigned int b6 : 1;
unsigned int b7 : 1;
unsigned int b8 : 1; // 第 9 位,应该到第 2 个字节
unsigned int b9 : 1;
} BitField2Byte;
// ==== ==== ==== ==== ==== 打印内存字节 ==== ==== ==== ==== ====
void print_memory_bytes(const char *name, void *ptr, size_t size)
{
unsigned char *bytes = (unsigned char *)ptr;
printf("%s 内存布局 (低地址 -> 高地址):\n", name);
printf(" 地址: ");
for (size_t i = 0; i < size; i++) {
printf("[%p] ", (void*)(bytes + i));
}
printf("\n");
printf(" 数据: ");
for (size_t i = 0; i < size; i++) {
printf(" 0x%02X ", bytes[i]);
}
printf("\n");
printf(" 二进制: ");
for (size_t i = 0; i < size; i++) {
for (int j = 7; j >= 0; j--) {
printf("%d", (bytes[i] >> j) & 1);
}
printf(" ");
}
printf("\n\n");
}
// ==== ==== ==== ==== ==== 测试 1: 验证单字节内位域分配顺序 ==== ==== ==== ==== ====
void test_single_byte_bitfield(void)
{
printf("========== 测试1: 单字节内位域分配 ==========\n\n");
BitField1Byte test;
// 测试: 只设置 bit0 (最低位)
memset(&test, 0, sizeof(test));
test.b0 = 1;
printf("设置 b0 = 1 (最低位):\n");
print_memory_bytes("BitField1Byte", &test, sizeof(test));
// 测试: 只设置 bit7 (最高位)
memset(&test, 0, sizeof(test));
test.b7 = 1;
printf("设置 b7 = 1 (最高位):\n");
print_memory_bytes("BitField1Byte", &test, sizeof(test));
// 测试: 设置 b0, b3, b7
memset(&test, 0, sizeof(test));
test.b0 = 1;
test.b3 = 1;
test.b7 = 1;
printf("设置 b0=1, b3=1, b7=1:\n");
print_memory_bytes("BitField1Byte", &test, sizeof(test));
// 结论
printf(">>> 结论: 位域从低地址字节的低位bit开始分配 <<<\n");
printf(" b0 -> 第0字节 bit0 (最低位)\n");
printf(" b7 -> 第0字节 bit7 (最高位)\n\n");
}
// ==== ==== ==== ==== ==== 测试 2: 验证跨字节位域分配 ==== ==== ==== ==== ====
void test_multi_byte_bitfield(void)
{
printf("========== 测试2: 跨字节位域分配 ==========\n\n");
BitField2Byte test;
// 测试: 只设置 b0 (第 0 位)
memset(&test, 0, sizeof(test));
test.b0 = 1;
printf("设置 b0 = 1:\n");
print_memory_bytes("BitField2Byte", &test, sizeof(test));
// 测试: 只设置 b7 (第 7 位)
memset(&test, 0, sizeof(test));
test.b7 = 1;
printf("设置 b7 = 1:\n");
print_memory_bytes("BitField2Byte", &test, sizeof(test));
// 测试: 只设置 b8 (第 8 位,应该到第 2 个字节)
memset(&test, 0, sizeof(test));
test.b8 = 1;
printf("设置 b8 = 1 (第9位,应该到第2个字节):\n");
print_memory_bytes("BitField2Byte", &test, sizeof(test));
// 测试: 只设置 b9 (第 9 位)
memset(&test, 0, sizeof(test));
test.b9 = 1;
printf("设置 b9 = 1 (第10位):\n");
print_memory_bytes("BitField2Byte", &test, sizeof(test));
// 测试: 设置 b0, b7, b8, b9
memset(&test, 0, sizeof(test));
test.b0 = 1;
test.b7 = 1;
test.b8 = 1;
test.b9 = 1;
printf("设置 b0=1, b7=1, b8=1, b9=1:\n");
print_memory_bytes("BitField2Byte", &test, sizeof(test));
printf(">>> 结论: 第1个字节(低地址)存bit0-bit7 <<<\n");
printf(" 第2个字节(高地址)存bit8-bit9 <<<\n\n");
}
// ==== ==== ==== ==== ==== 测试 3: 直接查看位域在内存中的对应关系 ==== ==== ==== ==== ====
void test_bitfield_mapping(void)
{
printf("========== 测试3: 位域与内存位的映射关系 ==========\n\n");
union {
struct {
unsigned int b0 : 1;
unsigned int b1 : 1;
unsigned int b2 : 1;
unsigned int b3 : 1;
unsigned int b4 : 1;
unsigned int b5 : 1;
unsigned int b6 : 1;
unsigned int b7 : 1;
} bits;
unsigned char byte;
} test;
printf("逐位设置测试:\n");
printf("----------------------------------------\n");
for (int i = 0; i <= 7; i++) {
memset(&test, 0, sizeof(test));
// 设置第 i 位
switch(i) {
case 0: test.bits.b0 = 1; break;
case 1: test.bits.b1 = 1; break;
case 2: test.bits.b2 = 1; break;
case 3: test.bits.b3 = 1; break;
case 4: test.bits.b4 = 1; break;
case 5: test.bits.b5 = 1; break;
case 6: test.bits.b6 = 1; break;
case 7: test.bits.b7 = 1; break;
}
printf("设置 b%d=1 -> 内存字节值: 0x%02X (二进制: ", i, test.byte);
for (int j = 7; j >= 0; j--) {
printf("%d", (test.byte >> j) & 1);
}
printf(")\n");
}
printf("\n>>> 结论: b0 对应内存字节的 bit0 (LSB) <<<\n");
printf(" b7 对应内存字节的 bit7 (MSB)\n\n");
}
// ==== ==== ==== ==== ==== 主函数 ==== ==== ==== ==== ====
int main()
{
printf("\n");
printf("############################################\n");
printf("# 位域存储顺序验证 - 验证用户规律 #\n");
printf("############################################\n\n");
test_single_byte_bitfield();
test_multi_byte_bitfield();
test_bitfield_mapping();
printf("========== 总结 ==========\n");
printf("验证结果: 位域分配确实遵循以下规律:\n");
printf(" 1. 从内存低地址向高地址分配\n");
printf(" 2. 从低地址字节的低位bit开始\n");
printf(" 3. 依次向高位bit扩展\n");
printf(" 4. 一个字节用完后再到下一个字节\n");
printf("\n注意: 这个规律与大小端无关!\n");
printf(" 大小端只影响字节序,不影响位域内的位序\n");
return 0;
}将会得到以下输出信息:
sumu@virtual-machine:~/hk/alpha$ gcc main.c -Wall
sumu@virtual-machine:~/hk/alpha$ ./a.out
############################################
# 位域存储顺序验证 - 验证用户规律 #
############################################
========== 测试1: 单字节内位域分配 ==========
设置 b0 = 1 (最低位):
BitField1Byte 内存布局 (低地址 -> 高地址):
地址: [0x7ffea1519c04] [0x7ffea1519c05] [0x7ffea1519c06] [0x7ffea1519c07]
数据: 0x01 0x00 0x00 0x00
二进制: 00000001 00000000 00000000 00000000
设置 b7 = 1 (最高位):
BitField1Byte 内存布局 (低地址 -> 高地址):
地址: [0x7ffea1519c04] [0x7ffea1519c05] [0x7ffea1519c06] [0x7ffea1519c07]
数据: 0x80 0x00 0x00 0x00
二进制: 10000000 00000000 00000000 00000000
设置 b0=1, b3=1, b7=1:
BitField1Byte 内存布局 (低地址 -> 高地址):
地址: [0x7ffea1519c04] [0x7ffea1519c05] [0x7ffea1519c06] [0x7ffea1519c07]
数据: 0x89 0x00 0x00 0x00
二进制: 10001001 00000000 00000000 00000000
>>> 结论: 位域从低地址字节的低位bit开始分配 <<<
b0 -> 第0字节 bit0 (最低位)
b7 -> 第0字节 bit7 (最高位)
========== 测试2: 跨字节位域分配 ==========
设置 b0 = 1:
BitField2Byte 内存布局 (低地址 -> 高地址):
地址: [0x7ffea1519c04] [0x7ffea1519c05] [0x7ffea1519c06] [0x7ffea1519c07]
数据: 0x01 0x00 0x00 0x00
二进制: 00000001 00000000 00000000 00000000
设置 b7 = 1:
BitField2Byte 内存布局 (低地址 -> 高地址):
地址: [0x7ffea1519c04] [0x7ffea1519c05] [0x7ffea1519c06] [0x7ffea1519c07]
数据: 0x80 0x00 0x00 0x00
二进制: 10000000 00000000 00000000 00000000
设置 b8 = 1 (第9位,应该到第2个字节):
BitField2Byte 内存布局 (低地址 -> 高地址):
地址: [0x7ffea1519c04] [0x7ffea1519c05] [0x7ffea1519c06] [0x7ffea1519c07]
数据: 0x00 0x01 0x00 0x00
二进制: 00000000 00000001 00000000 00000000
设置 b9 = 1 (第10位):
BitField2Byte 内存布局 (低地址 -> 高地址):
地址: [0x7ffea1519c04] [0x7ffea1519c05] [0x7ffea1519c06] [0x7ffea1519c07]
数据: 0x00 0x02 0x00 0x00
二进制: 00000000 00000010 00000000 00000000
设置 b0=1, b7=1, b8=1, b9=1:
BitField2Byte 内存布局 (低地址 -> 高地址):
地址: [0x7ffea1519c04] [0x7ffea1519c05] [0x7ffea1519c06] [0x7ffea1519c07]
数据: 0x81 0x03 0x00 0x00
二进制: 10000001 00000011 00000000 00000000
>>> 结论: 第1个字节(低地址)存bit0-bit7 <<<
第2个字节(高地址)存bit8-bit9 <<<
========== 测试3: 位域与内存位的映射关系 ==========
逐位设置测试:
----------------------------------------
设置 b0=1 -> 内存字节值: 0x01 (二进制: 00000001)
设置 b1=1 -> 内存字节值: 0x02 (二进制: 00000010)
设置 b2=1 -> 内存字节值: 0x04 (二进制: 00000100)
设置 b3=1 -> 内存字节值: 0x08 (二进制: 00001000)
设置 b4=1 -> 内存字节值: 0x10 (二进制: 00010000)
设置 b5=1 -> 内存字节值: 0x20 (二进制: 00100000)
设置 b6=1 -> 内存字节值: 0x40 (二进制: 01000000)
设置 b7=1 -> 内存字节值: 0x80 (二进制: 10000000)
>>> 结论: b0 对应内存字节的 bit0 (LSB) <<<
b7 对应内存字节的 bit7 (MSB)
========== 总结 ==========
验证结果: 位域分配确实遵循以下规律:
1. 从内存低地址向高地址分配
2. 从低地址字节的低位bit开始
3. 依次向高位bit扩展
4. 一个字节用完后再到下一个字节
注意: 这个规律与大小端无关!
大小端只影响字节序,不影响位域内的位序3. 存储规则
在 C 语言标准中并没有规定位域的具体存储方式,不同的编译器有不同的实现,但它们都 尽量压缩存储空间。
3.1 规则一
当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。
#include <stdio.h>
/* 定义位域数据类型 */
struct data
{
unsigned int a: 6;
unsigned int b: 12;
unsigned int c: 4;
};
int main(int argc, char *argv[])
{
struct data d1;/* 定义位域变量 1:0000 0001 6:0000 0110 13:0000 1101*/
printf("sizeof(unsigned int) = %ld\n", sizeof(unsigned int));
printf("sizeof(d1) = %ld\n", sizeof(d1));
printf("sizeof(struct data) = %ld\n", sizeof(struct data));
return 0;
}在终端执行以下命令:
gcc test.c -Wall # 编译程序
./a.out # 执行可执行文件会看到有如下信息输出:
sizeof(unsigned int) = 4
sizeof(d1) = 4
sizeof(struct data) = 4a 、 b 、 c 的类型都是 unsigned int , sizeof 的结果为 4 个字节( Byte ),也就是 32 个位( Bit )。 a 、 b 、 c 的位宽之和为 6+12+4 = 22 ,小于 32 ,所以它们会挨着存储,中间没有缝隙。
sizeof(struct data) 的大小之所以为 4 ,而不是 3 ,是因为要将内存对齐到 4 个字节,以便提高存取效率。
如果将成员 a 的位宽改为 22,那么输出结果将会是 8,因为 22+12 = 34,大于 32,b 会从新的位置开始存储,相对 a 的偏移量是 sizeof(unsigned int),也即 4 个字节。
如果再将成员 c 的位宽也改为 22,那么输出结果将会是 12,三个成员都不会挨着存储。
3.2 规则二
当相邻成员的类型不同时,不同的编译器有不同的实现方案, GCC 会压缩存储,而 VC/VS 不会。
#include <stdio.h>
/* 定义位域数据类型 */
struct data
{
unsigned int a: 12;
unsigned char b: 4;
unsigned int c: 12;
};
int main(int argc, char *argv[])
{
struct data d1;/* 定义位域变量 */
printf("sizeof(unsigned int) = %ld\n", sizeof(unsigned int));
printf("sizeof(unsigned char) = %ld\n", sizeof(unsigned char));
printf("sizeof(d1) = %ld\n", sizeof(d1));
printf("sizeof(struct data) = %ld\n", sizeof(struct data));
return 0;
}在终端执行以下命令:
gcc test.c -Wall # 编译程序
./a.out # 执行可执行文件会看到有如下信息输出:
sizeof(unsigned int) = 4
sizeof(unsigned char) = 1
sizeof(d1) = 4
sizeof(struct data) = 43.3 规则三
如果成员之间穿插着非位域成员,那么不会进行压缩。
#include <stdio.h>
/* 定义位域数据类型 */
struct data
{
unsigned int a: 12;
unsigned char b;
unsigned int c: 12;
};
int main(int argc, char *argv[])
{
struct data d1;/* 定义位域变量 */
printf("sizeof(unsigned int) = %ld\n", sizeof(unsigned int));
printf("sizeof(unsigned char) = %ld\n", sizeof(unsigned char));
printf("sizeof(d1) = %ld\n", sizeof(d1));
printf("sizeof(struct data) = %ld\n", sizeof(struct data));
return 0;
}在终端执行以下命令:
gcc test.c -Wall # 编译程序
./a.out # 执行可执行文件会看到有如下信息输出:
sizeof(unsigned int) = 4
sizeof(unsigned char) = 1
sizeof(d1) = 8
sizeof(struct data) = 83.4 规则四
含有位域的 整个结构体的总大小为最宽基本类型成员大小的整数倍。
3.5 规则五
若位域之间定义有匿名位域成员,则匿名位域成员指定的空闲位不用于后续成员的数据存储;
3.6 一些补充的规则
(1)一个位段必须存储在同一存储单元中,不能跨两个单元。如果第一个单元空间不能容纳下一个位段,则该空间不用,而从下一个单元起存放该位段。
(2)位段可以用整型格式符输出。
(3)位段可以在数值表达式中引用,它会被系统自动地转换成整型数。
(4)已命名位域不能有零宽度。
(5)给位域变量赋值的时候如果超过赋值范围,超过的位忽略。
(6)位段定义的第一个位段长度不能为 0。
4. 示例分析
4.1 示例 1
相邻位域成员相同:
#include <stdio.h>
/* 定义位域数据类型 */
struct data
{
unsigned short a: 3;
unsigned short b: 4;
unsigned short : 5; // 无名位域
unsigned short c: 3;
unsigned short d: 5; // 前一个字节剩余空间不足以存放下一个成员,跳过剩余位
};
int main(int argc, char *argv[])
{
struct data d1;/* 定义位域变量 */
printf("sizeof(struct data) = %ld\n", sizeof(struct data));
return 0;
}编译运行后得到结构体大小为 4,它的存储如下图:
(1)对于成员 a、b、c 再加上匿名成员占用的总内存空间并未超过 unsigned short 类型空间的大小,因此在 unsigned short 类型可容纳的范围内,这些成员可以紧挨着存放;
(2)当后续存放成员 d 时,前一个 unsigned short 类型数据剩余的空间已不足以容纳 d,因此选择下一个内存单元进行存放。
(3)特别地,如果定义上述结构体中 匿名成员占用的位数为 0,那么对于第一个 unsigned short 类型数据在除被 a、b 占用区域的其它剩余空间都将不会被使用,则成员 c 需要从 byte2 开始进行存储。
4.2 示例 2
现在考虑相邻位域成员类型不同的情况,定义如下结构体并画出其在内存中的布局如下(这个结果基于 gcc 编译器):
#include <stdio.h>
/* 定义位域数据类型 */
struct data
{
unsigned char a: 3;
unsigned char b: 4;
unsigned short c: 5;
unsigned short d: 5;
};
int main(int argc, char *argv[])
{
struct data d1;/* 定义位域变量 */
printf("sizeof(struct data) = %ld\n", sizeof(struct data));
return 0;
}编译运行后得到的结构体大小为 4.我们看一下存储情况:
GCC 编译器会尽可能地利用空闲的内存位进行位域成员的存放,这里有一点需要注意的是,尽管对于第一个 unsigned char 类型可容纳的单字节范围在存放完成员 a 和 b 后,剩余的一位已不足以存放成员 c,但是 GCC 编译器仍然将这一位分配给了 c。
五、位域与大小端
1. 什么是大小端?
参考 01-编程语言/01-C 语言/26-字节序/LV005-大小端模式.md
2. 位域的内存分配规则
2.1 基本规则
系统的大小端差异会同时牵涉到字节序和比特序问题,对于结构体位域这种会涉及比特位层面数据的操作,几乎需要时刻考虑平台大小端的差异。结构体内位域成员在大小端系统上的内存分配规则如下:
- 无论是大端或小端模式,位域的存储都是由内存低地址向高地址分配,即从低地址字节的低位 bit 开始向高地址字节的高位 bit 分配空间。(具体可以看 内存低地址向高地址分配)
- 位域成员在已分配的内存区域内,按照机器定义的比特序对数据的各个 bit 位进行排列。即在小端模式中,位域成员的最低有效位存放在内存低 bit 位,最高有效位存放在内存高 bit 位;大端模式则相反。
2.2 示例
2.2.1 测试代码
看一个示例:
#include <stdio.h>
union {
struct {
unsigned short a: 1;
unsigned short b: 3;
unsigned short c: 4;
unsigned short d: 4;
unsigned short e: 4;
} bits;
unsigned short s_data;
} val;
int main(int argc, char *argv[])
{
val.bits.a = 1;
val.bits.b = 3;
val.bits.c = 5;
val.bits.d = 7;
val.bits.e = 15;
printf("val is 0x%x\n", val.s_data);
return 0;
}运行上述程序:
- 在小端设备上的输出的结果为:
0xf757(1111 0111 0101 0111); - 换成大端设备,程序的运行结果为:
0xb57f(1011 0101 0111 1111)。
2.2.2 结果分析
我们画出程序所定义结构体位域成员在内存中的存储布局如下所示(因为平时多数都是在使用小端机器,在此有意将字节的顺序和字节内的位顺序进行颠倒,方便对比):
- 小端系统
.a .b .c .d .e
bit [00:00] [03:01] [07:04] [11:08] [15:12]
data 1 011 0101 0111 1111
存放在小端CPU的RAM时,[16:0] = 0xf757
bit: 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
val: 1 1 1 1 0 1 1 1 0 1 0 1 0 1 1 1- 大端系统
.a .b .c .d .e
bit [00:00] [01:03] [04:07] [08:11] [12:15]
data 1 011 0101 0111 1111
存放在大端CPU的RAM时,[0:31] = b57f
bit: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15
val: 1 0 1 1 0 1 0 1 0 1 1 1 1 1 1 1为了更便于理解位域在内存中的排列规则,建议将位域成员内存空间的分配和解析分开来看:
(1)第一步先考虑内存空间的分配,从上图中可以看到,不论大小端都是从内存地址的低位开始;
(2)当位域成员占用空间确定之后,考虑于位域成员数据位的排布,可以看到小端系统从低 bit 位开始存放数据,这是符合我们预期的,而大端设备则恰恰相反,大端系统从高 bit 位开始存放数据,因此在大端设备中,我们需要转换下思维从内存的高位开始解析数据位。
参考资料: