何时在 C 中使用位域?
Posted
技术标签:
【中文标题】何时在 C 中使用位域?【英文标题】:When to use bit-fields in C? 【发布时间】:2014-09-15 23:32:22 【问题描述】:关于“为什么我们需要使用位域”的问题,在谷歌上搜索我发现位域用于标志。 现在我很好奇,
-
这是实际使用位域的唯一方法吗?
我们是否需要使用位域来节省空间?
书中位域的定义方式:
struct
unsigned int is_keyword : 1;
unsigned int is_extern : 1;
unsigned int is_static : 1;
flags;
-
为什么要使用 int?
占用了多少空间?
我很困惑为什么我们使用int
,而不是short
或小于int
的东西。
-
据我了解,内存中只占用了 1 位,而不是整个 unsigned int 值。对吗?
【问题讨论】:
关于位域的一切都是实现定义的,从来没有? 【参考方案1】:为了利用内存空间,我们可以使用位域。
据我所知,在现实世界的编程中,如果我们需要,我们可以使用布尔值而不是将其声明为整数然后生成位域。
【讨论】:
“在现实世界中”,布尔值通常不止一点。【参考方案2】:为什么我们需要使用位域?
当您想要存储一些可以存储少于字节的数据时,可以使用位字段在结构中耦合这些数据。 在嵌入式字中,当任何寄存器的一个 32 位世界对不同的字有不同的含义时,您也可以使用位文件使它们更具可读性。
我发现位字段用于标志。现在我很好奇,这是实际使用位域的唯一方法吗?
不,这不是唯一的方法。您也可以以其他方式使用它。
我们是否需要使用位域来节省空间?
是的。
据我了解,内存中只占用了 1 位,而不是整个 unsigned int 值。对吗?
没有。内存只能占用字节的倍数。
【讨论】:
【参考方案3】:一个很好的资源是Bit Fields in C。
基本原因是减少使用的尺寸。例如,如果你写:
struct
unsigned int is_keyword;
unsigned int is_extern;
unsigned int is_static;
flags;
您将至少使用3 * sizeof(unsigned int)
或 12 个字节来表示 3 个小标志,这应该只需要 3 位。
所以如果你写:
struct
unsigned int is_keyword : 1;
unsigned int is_extern : 1;
unsigned int is_static : 1;
flags;
这占用了与unsigned int
相同的空间,因此占用了 4 个字节。您可以在结构需要更多空间之前将 32 个一位字段放入结构中。
这有点相当于经典的自制位字段:
#define IS_KEYWORD 0x01
#define IS_EXTERN 0x02
#define IS_STATIC 0x04
unsigned int flags;
但是位域语法更简洁,比较一下:
if (flags.is_keyword)
反对:
if (flags & IS_KEYWORD)
而且显然更不容易出错。
【讨论】:
不错的答案!在谈论位域及其在内存中的大小时,应记住 C++ 编译器将按如下方式在内存中分配位域:same 类型的几个连续位域成员将按顺序分配。一旦需要分配 new 类型,它将与下一个逻辑内存块的开头对齐。下一个逻辑块将取决于您的处理器。一些处理器可以对齐到 8 位边界,而其他处理器只能对齐到 16 位边界。 下一个问题是:什么时候我需要节省空间?几乎从不。除非您处于非常有限的环境中,否则请避免使用位域。 另外:它的行为更像一个布尔值:你可以写flags.is_keyword == flags.is_extern
(与((flags & IS_KEYWORD) == 0) == ((flags & IS_EXTERN) == 0)
比较)。另一方面,使用传统位域,您可以使用一个比较语句检查多个值:(flags & (IS_KEYWORD IS_EXTERN)) == IS_KEYWORD
(这意味着 IS_KEYWORD
但不是 IS_EXTERN
)
@Yawar 如果结构是不透明的,您只能通过指针来处理它。在 C 中,指针的类型是无关紧要的,而在 C++ 中,它只影响名称修饰。所以简短的回答是“否”,长的回答是“如果它是不透明的,它就不是 ABI 的一部分。”
@rioki 我的意思是 _Bool 作为位域中的类型:_Bool is_xyz : 1;
。可能会影响静态分析(在我的情况下为 MISRA)或 _Generic 的行为。【参考方案4】:
位域常见的另一个地方是硬件寄存器。如果你有一个 32 位的寄存器,每个位都有特定的含义,你可以用一个位域来优雅地描述它。
这样的位域本质上是特定于平台的。在这种情况下,可移植性并不重要。
【讨论】:
可移植性不仅仅适用于硬件。同一架构的不同编译器可能在位域的顺序上存在分歧。 虽然警告是正确的,但我很少看到使用多个编译器的嵌入式项目。通常你会在一个项目中坚持使用一个。 如果硬件寄存器在 IP 块中,并且 IP 块驱动程序用于多个体系结构,您将拥有多个编译器。 IE。这并不像人们想象的那么罕见。【参考方案5】:如果它也是我们经常使用的值,我们不仅可以节省空间,还可以获得性能,因为我们不需要污染缓存。然而,缓存也是使用位字段的危险,因为对不同位的并发读取和写入会导致数据竞争,并且更新到完全独立的位可能会用旧值覆盖新值。..
【讨论】:
【参考方案6】:您可以使用它们来扩展包装的无符号类型的数量。普通的你只有 8,16,32,64... 的幂,但是你可以通过位域拥有所有的幂。
struct a
unsigned int b : 3 ;
;
struct a w = 0 ;
while( 1 )
printf("%u\n" , w.b++ ) ;
getchar() ;
【讨论】:
【参考方案7】:现在我很好奇,[是标志]实际使用位域的唯一方法吗?
不,标志不是使用位域的唯一方式。它们也可用于存储大于一位的值,尽管标志更常见。例如:
typedef enum
NORTH = 0,
EAST = 1,
SOUTH = 2,
WEST = 3
directionValues;
struct
unsigned int alice_dir : 2;
unsigned int bob_dir : 2;
directions;
我们是否需要使用位域来节省空间?
位域确实节省空间。它们还允许以更简单的方式设置非字节对齐的值。我们可以使用与在struct
中设置字段相同的语法,而不是移位和使用按位运算。这提高了可读性。使用位域,您可以编写
directions.alice_dir = WEST;
directions.bob_dir = SOUTH;
但是,要在一个 int
(或其他类型)的空间中存储多个独立值,而不需要位字段,您需要编写如下内容:
#define ALICE_OFFSET 0
#define BOB_OFFSET 2
directions &= ~(3<<ALICE_OFFSET); // clear Alice's bits
directions |= WEST<<ALICE_OFFSET; // set Alice's bits to WEST
directions &= ~(3<<BOB_OFFSET); // clear Bob's bits
directions |= SOUTH<<BOB_OFFSET; // set Bob's bits to SOUTH
提高位域的可读性可以说比在这里和那里节省几个字节更重要。
为什么要使用 int?占用了多少空间?
整个int
的空间都被占用了。我们使用int
,因为在很多情况下,它并不重要。如果对于单个值,您使用 4 个字节而不是 1 或 2 个字节,您的用户可能不会注意到。对于某些平台,大小确实更重要,您可以使用占用更少空间的其他数据类型(char
、short
、uint8_t
等)。
据我了解,内存中只占用了 1 位,而不是整个 unsigned int 值。对吗?
不,这是不正确的。整个unsigned int
将存在,即使您只使用其中的 8 个位。
【讨论】:
您能详细介绍一下手动操作部分吗?为什么需要这样做? @Willwsharp 我很乐意添加更多细节;你能告诉我你在努力理解的哪一部分吗? 我想我现在明白了,“手动执行”将尝试在没有支持结构的情况下提取数据,这就是为什么您必须自己进行位操作的原因。对吗? 是的,完全正确。我可以清除那种语言,“手动”可能不够具体。 @EricFinn 如果 整个 int 的空间都被占用了 ,为什么sizeof(directions)
是 4 个字节(按照你说的应该是 8 个字节)?在我的机器中,sizeof(int)
是 4 个字节【参考方案8】:
回答问题中没有人回答的部分:
Ints 不是 Shorts
使用整数而不是短裤等的原因是,在大多数情况下,这样做不会节省空间。
现代计算机具有 32 位或 64 位架构,即使您使用更小的存储类型(例如 short),也需要 32 位或 64 位。
较小的类型只有在您可以将它们打包在一起时才对节省内存有用(例如,短数组可能比 int 数组使用更少的内存,因为短数组可以在数组中更紧密地打包在一起)。对于大多数使用位域的情况,情况并非如此。
其他用途
位域最常用于标志,但它们还有其他用途。例如,在许多国际象棋算法中表示棋盘的一种方法是使用 64 位整数来表示棋盘(8*8 像素)并在该整数中设置标志以给出所有白棋子的位置。另一个整数表示所有黑棋等。
【讨论】:
注意:许多(每年 100 亿 - 2013 年)嵌入式处理器使用 8 位和 16 位架构。 C 在那儿非常很受欢迎。 @chux-ReinstateMonica 几乎所有的微控制器!【参考方案9】:我们主要(但不限于)将位字段用于标志结构 - 字节或字(或可能更大的东西),我们试图在其中打包微小(通常是 2 态)的(通常相关的)信息片段。
在这些场景中,使用位域是因为它们正确地模拟了我们正在解决的问题:我们正在处理的并不是一个真正的 8 位(或 16 位或 24 位或 32 位)数字,而是 8 条(或 16 条或 24 条或 32 条)相关但不同的信息的集合。
我们使用位域解决的问题是,将信息“打包”紧密会带来可衡量的好处和/或“解包”信息不会带来损失。例如,如果您通过 8 个引脚公开 1 个字节,并且每个引脚的位通过它们自己的总线,该总线已经印在板上,以便准确地引导到它应该到达的位置,那么一个位域是理想的。 “打包”数据的好处是可以一次性发送(如果总线频率有限且我们的操作依赖于其执行频率,这很有用),而“解包”数据的代价是不存在(或存在但值得)。
另一方面,由于计算机体系结构通常的工作方式,我们不会在其他情况下(例如正常的程序流控制)将位字段用于布尔值。大多数常见的 CPU 不喜欢从内存中获取一位 - 它们喜欢获取字节或整数。他们也不喜欢处理位——他们的指令经常操作更大的东西,比如整数、单词、内存地址等。
因此,当您尝试对位进行操作时,由您或编译器(取决于您使用的语言)编写执行位掩码的额外操作并剥离除您的信息之外的所有内容的结构其实想操作上。如果“打包”信息没有任何好处(在大多数情况下没有好处),那么将位字段用于布尔值只会在代码中引入开销和噪音。
【讨论】:
【参考方案10】:一个好的用法是实现一个块来转换到base64或任何未对齐的数据结构。
struct
unsigned int e1:6;
unsigned int e2:6;
unsigned int e3:6;
unsigned int e4:6;
base64enc; //I don't know if declaring a 4-byte array will have the same effect.
struct
unsigned char d1;
unsigned char d2;
unsigned char d3;
base64dec;
union base64chunk
struct base64enc enc;
struct base64dec dec;
;
base64chunk b64c;
//you can assign 3 characters to b64c.enc, and get 4 0-63 codes from b64dec instantly.
这个例子有点幼稚,因为 base64 还必须考虑空终止(即没有长度 l
的字符串,因此 l
% 3 为 0)。但作为访问未对齐数据结构的示例。
另一个示例:使用此功能将 TCP 数据包标头分解为其组件(或您要讨论的其他网络协议数据包标头),尽管这是一个更高级且最终用户较少的示例.一般来说:这对于 PC 内部、SO、驱动程序、编码系统很有用。
另一个例子:分析float
号码。
struct _FP32
unsigned int sign:1;
unsigned int exponent:8;
unsigned int mantissa:23;
union FP32_t
_FP32 parts;
float number;
(免责声明:不知道应用它的文件名/类型名称,但在 C 中,这是在标题中声明的;不知道如何对 64 位浮点数执行此操作,因为尾数必须具有52 位和 - 在 32 位目标中 - 整数有 32 位)。
结论:正如概念和这些示例所示,这是一个很少使用的功能,因为它主要用于内部目的,而不是用于日常软件。
【讨论】:
联合化float
的问题:字节序。在相反的字节序机器中,所需的结构可能是struct _FP32 unsigned int mantissa:23; unsigned int exponent:8; unsigned int sign:1;
。当大于unsigned
的位宽时,位域定义不明确。由于unsigned
必须至少为 16 位,因此任何大于 16 的宽度都会遇到可移植性问题 - “如何为 64 位浮点数做到这一点”。
“一个好的用法应该是” - 这是真的。但是:它有效吗?在我的情况下它没有,因为编译器不打包单个位。【参考方案11】:
位域可用于节省内存空间(但很少使用位域来实现此目的)。它用于有内存限制的地方。例如)在嵌入式系统中编程时。
但只有在非常需要时才应该使用它。
因为我们不能有位域的地址。所以地址运算符 & 不能和它们一起使用。
【讨论】:
@Jerfov2 他们节省了大量空间。想象一个使用 48 位数字(数百万个)的服务器应用程序。您要购买 48GB 内存还是 64GB?您的客户更喜欢哪一种?【参考方案12】:为什么要使用 int?占用了多少空间?
我在其他任何答案中都没有提到过这个问题的一个答案,即 C 标准保证支持 int。具体来说:
位域的类型应为 _Bool、signed int、unsigned int 或其他一些实现定义的类型的限定或非限定版本。
编译器通常允许额外的位域类型,但不是必需的。如果你真的关心可移植性,int 是最好的选择。
【讨论】:
【参考方案13】:要回答最初的问题 »何时在 C 中使用位域?« ... 根据 Brian Hook 的“编写可移植代码”一书(ISBN 1-59327-056-9,我阅读了德文版 ISBN 3- 937514-19-8)和个人经验:
永远不要使用 C 语言的位域习语,而是自己做。
许多实现细节是编译器特定的,尤其是结合联合时,不同的编译器和不同的字节序不能保证事情。如果您的代码必须具有可移植性并且将针对不同的架构和/或使用不同的编译器进行编译的可能性很小,请不要使用它。
当将代码从带有一些专有编译器的小端微控制器移植到另一个带有 GCC 的大端微控制器时,我们遇到了这种情况,这并不有趣。 :-/
这就是我从那时起使用标志(主机字节顺序;-))的方式:
# define SOME_FLAG (1 << 0)
# define SOME_OTHER_FLAG (1 << 1)
# define AND_ANOTHER_FLAG (1 << 2)
/* test flag */
if ( someint & SOME_FLAG )
/* do this */
/* set flag */
someint |= SOME_FLAG;
/* clear flag */
someint &= ~SOME_FLAG;
然后不需要与 int 类型和一些位域结构的联合。如果您阅读大量嵌入式代码,这些测试、设置和清晰模式将变得很常见,并且您很容易在代码中发现它们。
【讨论】:
您能否分享一些实际代码,这些代码会与特定编译器中断或无法在不同的架构上运行?像“NEVER”这样用笑脸装饰但没有反例的东西听起来像是一个固执己见的神话。 IMO,如果您在考虑使用位域的环境中,您可能应该同时考虑字节序。【参考方案14】:位域更加紧凑,这是一个优势。
但不要忘记打包结构比普通结构要慢。它们也更难构建,因为程序员必须定义每个字段使用的位数。这是一个缺点
【讨论】:
【参考方案15】:在我们的项目中,我们使用它从给定的内存地址中提取页表项和页目录项:
union VADDRESS
struct
ULONG64 BlockOffset : 16;
ULONG64 PteIndex : 14;
ULONG64 PdeIndex : 14;
ULONG64 ReservedMBZ : (64 - (16 + 14 + 14));
;
ULONG64 AsULONG64;
;
现在假设,我们有一个地址:
union VADDRESS tempAddress; tempAddress.AsULONG64 = 0x1234567887654321;
现在我们可以从这个地址访问 PTE 和 PDE: cout
【讨论】:
【参考方案16】:如今,微控制器 (MCU) 具有外围设备,例如 I/O 端口、ADC、DAC,以及处理器板载芯片。在 MCU 与所需的外围设备一起可用之前,我们将通过连接到微处理器的缓冲地址和数据总线来访问我们的一些硬件。指针将设置为设备的内存地址,如果设备看到它的地址以及 r/w 和可能的芯片选择,它将被访问。很多时候,我们希望访问设备上的单个或一小组位。
【讨论】:
以上是关于何时在 C 中使用位域?的主要内容,如果未能解决你的问题,请参考以下文章