行客 发表于 5 天前

C语言位域(位段)详解

在数据存储的实际场景中,部分数据在存储时无需占用一个完整的字节,仅需一个或几个二进制位就能满足存储需求。以开关为例,它仅有通电和断电这两种状态,用 0 和 1 来表示就足够了,这实际上只需一个二进制位。基于这样的实际需求,C 语言提供了一种名为位域的数据结构。

位域的定义:在进行结构体定义时,我们能够指定某个成员变量所占用的二进制位数(Bit),这便是位域。

以下是一个示例:

struct bs {
    unsigned m;
    unsigned n: 4;
    unsigned char ch: 6;
};

这里,:后面的数字用于限定成员变量占用的位数。
成员m未作位数限制,依据其数据类型unsigned可推算出它占用 4 个字节(Byte)的内存。而成员n和ch受到:后面数字的约束,不能再单纯依据数据类型来计算其长度,它们分别占用 4 位和 6 位(Bit)的内存。

位域成员的取值范围与溢出问题
位域成员n和ch的取值范围十分有限,当数据稍大时就可能发生溢出。下面是一个具体的示例代码:

#include <stdio.h>

int main() {
    struct bs {
      unsigned m;
      unsigned n: 4;
      unsigned char ch: 6;
    } a = { 0xad, 0xE, '$' };

    // 第一次输出
    printf("%#x, %#x, %c\n", a.m, a.n, a.ch);

    // 更改值后再次输出
    a.m = 0xb8901c;
    a.n = 0x2d;
    a.ch= 'z';
    printf("%#x, %#x, %c\n", a.m, a.n, a.ch);

    return 0;
}
运行结果如下:

0xad, 0xe, $
0xb8901c, 0xd, :
从输出结果可以看出,对于
n

ch
,第一次输出的数据是完整的,而第二次输出的数据是残缺的。

第一次输出时,
n
的值为
0xE

ch
的值对应的 ASCII 码为
0x24
(字符
$
对应的 ASCII 码),将它们换算成二进制分别是
1110

10 0100
,都未超出限定的位数,所以能够正常输出。

第二次输出时,
n
的值变为
0x2d

ch
的值对应的 ASCII 码为
0x7a
(字符
z
对应的 ASCII 码),换算成二进制分别是
10 1101

111 1010
,均超出了限定的位数。超出部分会被直接截去,剩下
1101

11 1010
,再换算成十六进制就是
0xd

0x3a

0x3a
对应的字符是
:
)。

位域宽度的限制
C 语言标准明确规定,位域的宽度不能超过它所依附的数据类型的长度。通俗来讲,成员变量都有其特定的类型,该类型限制了成员变量的最大长度,
:
后面的数字不能超出这个长度。

以之前的
bs
结构体为例,
n
的类型是
unsigned int
,其长度为 4 个字节,总共 32 位,那么
n
后面的数字就不能超过 32;
ch
的类型是
unsigned char
,长度为 1 个字节,共计 8 位,所以
ch
后面的数字不能超过 8。

我们可以把位域技术理解为在成员变量所占用的内存中选取一部分位宽来存储数据。

可用于位域的数据类型
C 语言标准规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这些数据类型包括
int

signed int

unsigned int

int
默认就是
signed int
);到了 C99 标准,
_Bool
类型也被支持。

关于 C 语言标准以及 ANSI C 和 C99 的区别,我们已在付费教程《C 语言的三套标准:C89、C99 和 C11》中进行了详细讲解。

不过,编译器在具体实现时通常会进行扩展,额外支持
char

signed char

unsigned char
以及
enum
类型。所以,前面的示例代码虽然不完全符合 C 语言标准,但依然能够被编译器支持。

位域的存储
C 语言标准并未明确规定位域的具体存储方式,不同的编译器有不同的实现方式,但它们都会尽量压缩存储空间。位域的具体存储规则如下:

相邻成员类型相同的情况
当相邻成员的类型相同时,如果它们的位宽之和小于该类型的
sizeof
大小,那么后面的成员会紧邻前一个成员存储,直至无法容纳为止;若它们的位宽之和大于该类型的
sizeof
大小,后面的成员将从新的存储单元开始存储,其偏移量为该类型大小的整数倍。

以下是一个示例代码:

#include <stdio.h>

int main() {
    struct bs {
      unsigned m: 6;
      unsigned n: 12;
      unsigned p: 4;
    };
    printf("%d\n", sizeof(struct bs));
    return 0;
}
运行结果为:

4
在这个例子中,
m

n

p
的类型都是
unsigned int

sizeof
的结果为 4 个字节(Byte),即 32 个位(Bit)。
m

n

p
的位宽之和为
6 + 12 + 4 = 22
,小于 32,所以它们会挨着存储,中间没有缝隙。

sizeof(struct bs)
的大小为 4 而不是 3,这是因为要将内存对齐到 4 个字节,以提高存取效率。

如果将成员
m
的位宽改为 22,那么输出结果将会是 8,因为
22 + 12 = 34
,大于 32,
n
会从新的位置开始存储,相对
m
的偏移量是
sizeof(unsigned int)
,也就是 4 个字节。

若再将成员
p
的位宽也改为 22,那么输出结果将会是 12,此时三个成员都不会挨着存储。

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

下面是一个示例代码:

#include <stdio.h>

int main() {
    struct bs {
      unsigned m: 12;
      unsigned char ch: 4;
      unsigned p: 4;
    };
    printf("%d\n", sizeof(struct bs));
    return 0;
}
在 GCC 编译器下的运行结果为 4,这表明三个成员挨着存储;在 VC/VS 编译器下的运行结果为 12,这意味着三个成员按照各自的类型存储(与不指定位宽时的存储方式相同)。

m

ch

p
的长度分别是 4、1、4 个字节,总共占用 9 个字节内存。在 VC/VS 编译器下输出结果为 12,这是因为要进行内存对齐(
ch
单独占用 4 个字节),以提高存取效率。

成员间穿插非位域成员的情况
如果成员之间穿插着非位域成员,那么不会进行压缩存储。例如下面的
bs
结构体:

struct bs {
    unsigned m: 12;
    unsigned ch;
    unsigned p: 4;
};
在各个编译器下,
sizeof
的结果都是 12。

位域成员地址的获取问题
通过上述分析可知,位域成员往往不占用完整的字节,有时也不处于字节的开头位置。因此,使用
&
运算符获取位域成员的地址是没有意义的,C 语言也明确禁止这样做。因为地址是字节(Byte)的编号,而非位(Bit)的编号。

无名位域
位域成员可以没有名称,只需给出数据类型和位宽,示例如下:

struct bs {
    int m: 12;
    int: 20;// 该位域成员不能使用
    int n: 4;
};
无名位域通常用于填充或者调整成员的位置。由于没有名称,无名位域不能被使用。

在上面的例子中,如果没有位宽为 20 的无名成员,
m

n
将会挨着存储,
sizeof(struct bs)
的结果为 4;有了这 20 位作为填充,
m

n
将分开存储,
sizeof(struct bs)
的结果为 8。

FishC 发表于 5 天前

C语言位域(位段)详解
位域用于节省内存空间,允许结构体成员按指定位数存储数据。

一、基本定义
定义语法:在结构体成员后加:位数
示例:

struct bs {
    unsigned m;         // 默认占4字节
    unsigned n: 4;      // 占4位
    unsigned char ch: 6;// 占6位
};


二、取值范围与溢出
位域成员超出位数会被截断:

#include <stdio.h>
int main() {
    struct bs a = {0xad, 0xE, '$'};
    printf("%#x, %#x, %c\n", a.m, a.n, a.ch); // 输出:0xad, 0xe, $
      
    a.n = 0x2d;// 二进制101101(超4位) → 截断为1101(0xd)
    a.ch = 'z';// 二进制1111010(超6位) → 截断为111010(字符':')
    printf("%#x, %#x, %c\n", a.m, a.n, a.ch); // 输出:0xb8901c, 0xd, :
    return 0;
}


三、位域宽度限制
位宽不能超过成员类型的长度:

[*] unsigned int类型 → 最大32位
[*] unsigned char类型 → 最大8位


四、可用数据类型
标准支持:

[*] int, signed int, unsigned int
[*] C99新增 _Bool

编译器扩展通常支持 char, enum 等。

五、存储规则
1. 相邻同类型成员压缩存储

struct bs {
    unsigned m: 6;
    unsigned n: 12;
    unsigned p: 4;   // 总22位 < 32位 → sizeof=4
};


2. 不同类型成员:编译器差异
GCC压缩存储,VC/VS不压缩:

struct bs {
    unsigned m: 12;
    unsigned char ch: 4;
    unsigned p: 4;
};
// GCC输出 sizeof=4,VC/VS输出 sizeof=12


3. 穿插非位域成员不压缩

struct bs {
    unsigned m: 12;
    unsigned ch;      // 非位域成员
    unsigned p: 4;   // sizeof恒为12
};


六、位域地址限制
禁止用 & 获取位域成员地址(地址按字节编址)。

七、无名位域
用于占位或对齐,无名称不可访问:

struct bs {
    int m: 12;
    int: 20;       // 无名位域,占20位
    int n: 4;      // 导致m和n分开存储
};                  // sizeof=8


关键点总结:

[*] 位域节省空间但可能溢出
[*] 存储规则依赖编译器(尤其类型不同时)
[*] 无名位域用于内存对齐


以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。

player-none 发表于 5 天前

https://www.cnblogs.com/lcgbk/p/14215449.html

转载没表明?
页: [1]
查看完整版本: C语言位域(位段)详解