在C中用单位测试和设置单个位的经典问题可能是最常见的中级编程技能之一.您可以使用简单的位掩码进行设置和测试
unsigned int mask = 1<<11; if (value & mask) {....} // Test for the bit value |= mask; // set the bit value &= ~mask; // clear the bit
一篇有趣的博客文章认为,这容易出错,难以维护,而且做法不佳.C语言本身提供了类型安全和可移植的位级访问:
typedef unsigned int boolean_t; #define FALSE 0 #define TRUE !FALSE typedef union { struct { boolean_t user:1; boolean_t zero:1; boolean_t force:1; int :28; /* unused */ boolean_t compat:1; /* bit 31 */ }; int raw; } flags_t; int create_object(flags_t flags) { boolean_t is_compat = flags.compat; if (is_compat) flags.force = FALSE; if (flags.force) { [...] } [...] }
但这让我感到畏缩.
我的同事和我对此有趣的争论仍然没有得到解决.两种样式都有效,我保持经典的位掩码方法简单,安全,清晰.我的同事认为这是常见且容易的,但是bitfield联合方法值得额外的几行,以使其便携和安全.
对于任何一方,还有其他争论吗?特别是有一些可能的失败,也许是字节顺序,位掩码方法可能会错过,但结构方法是安全的?
Bitfields并不像你想象的那样便携,因为"C不能保证机器字内字段的排序"(C书)
忽略这一点,正确使用,任何一种方法都是安全的.这两种方法还允许对整数变量进行符号访问.您可以争辩说bitfield方法更容易编写,但它也意味着需要查看更多代码.
如果问题是设置和清除位容易出错,那么正确的做法是编写函数或宏以确保正确执行.
// off the top of my head #define SET_BIT(val, bitIndex) val |= (1 << bitIndex) #define CLEAR_BIT(val, bitIndex) val &= ~(1 << bitIndex) #define TOGGLE_BIT(val, bitIndex) val ^= (1 << bitIndex) #define BIT_IS_SET(val, bitIndex) (val & (1 << bitIndex))
如果您不介意val必须是左值,除了BIT_IS_SET之外,这使您的代码可读.如果这不会让你开心,那么你取出赋值,将它括起来并将其用作val = SET_BIT(val,someIndex); 这将是等同的.
真的,答案是考虑将你想要的东西与你想要的东西分离.
Bitfield很棒且易于阅读,但不幸的是C语言没有指定内存中位域的布局,这意味着它们对于处理磁盘格式或二进制线协议中的打包数据基本上没用.如果你问我,这个决定是C-Ritchie的一个设计错误本可以选择订单并坚持下去.
你必须从作家的角度思考这个问题 - 了解你的观众.因此,需要考虑几个"受众".
首先是经典的C程序员,他们一生都在蒙着头脑,可以在睡梦中做到这一点.
第二个是newb,谁也不知道所有这些&和东西是什么.他们在上一份工作中编写了php编程,现在他们为你工作了.(我说这是一个做php的新手)
如果你写作是为了满足第一批观众(即全天的位掩码),你会让他们非常高兴,他们将能够保持蒙住眼睛的代码.但是,newb可能需要在能够维护代码之前克服大的学习曲线.他们需要了解二元运算符,如何使用这些运算来设置/清除位等等.你几乎可以肯定会遇到newb引入的bug,因为他/她需要所有这些才能让它工作.
另一方面,如果您编写以满足第二个受众,则newbs将更容易维护代码.他们将有更轻松的时间
flags.force = 0;
比
flags &= 0xFFFFFFFE;
并且第一批观众会变得脾气暴躁,但很难想象他们无法理解和维护新语法.搞砸了起来要困难得多.不会有新的错误,因为newb将更容易维护代码.你会得到一些讲座,讲述"在我的日子里,你需要一只稳定的手和一根磁化针来设置位......我们甚至都没有位掩码!" (感谢XKCD).
所以我强烈建议使用位掩码上的字段来保护你的代码.
根据ANSI C标准,联合使用具有未定义的行为,因此不应使用(或至少不被视为可移植).
根据ISO/IEC 9899:1999(C99)标准:
附件J - 可移植性问题:
1以下未指定:
- 在结构或联合中存储值时填充字节的值(6.2.6.1).
- 除了存储在(6.2.6.1)中的最后一个成员之外的联合成员的值.
6.2.6.1 - 语言概念 - 类型表示 - 概述:
6当值存储在结构或联合类型的对象中时,包括在成员对象中,对应于任何填充字节的对象表示的字节采用未指定的值.[42])结构或联合对象的值是永远不是陷阱表示,即使结构或联合对象的成员的值可能是陷阱表示.
7当值存储在union类型的对象的成员中时,对象表示的不对应于该成员但与其他成员对应的字节采用未指定的值.
所以,如果你想保持位域↔整数对应,并保持可移植性,我强烈建议你使用bitmasking方法,这与链接的博客文章相反,这并不是一个糟糕的做法.
比特场方法让你感到畏缩的是什么?
这两种技术都有它们的位置,我唯一的决定就是使用哪种技术:
对于简单的"一次性"位摆弄,我直接使用位运算符.
对于任何更复杂的东西 - 例如硬件寄存器映射,位域方法都会失败.
Bitfields更简洁易用(代价/略微/更冗长的写作.
Bitfields更健壮(无论如何,大小是"int")
位域通常与按位运算符一样快.
当您混合使用单个和多个位字段时,位域非常强大,并且提取多位字段涉及大量手动移位.
Bitfields实际上是自我记录的.通过定义结构并因此命名元素,我知道它的意图.
Bitfields还可以无缝地处理大于单个int的结构.
对于按位运算符,典型(坏)实践是位掩码的大量#defines.
有关位域的唯一警告是确保编译器确实将对象打包成您想要的大小.我不记得这是否由标准定义,因此断言(sizeof(myStruct)== N)是一个有用的检查.
无论哪种方式,位域已经在GNU软件中使用了几十年,并没有对它们造成任何伤害.我喜欢它们作为函数的参数.
我认为位域是常规的而不是结构.每个人都知道如何将值设置为各种选项,并且编译器将其归结为CPU上非常有效的按位操作.
如果您以正确的方式使用掩码和测试,编译器提供的抽象应该使其健壮,简单,可读和干净.
当我需要一组开/关开关时,我将继续在C中使用它们.
您所指的博客文章提到了raw
union字段作为bitfields的替代访问方法.
博客文章作者使用的目的raw
是可以的,但是如果你计划将其用于其他任何事情(例如位字段的序列化,设置/检查单个位),灾难只是在等待你.内存中位的排序依赖于体系结构,内存填充规则因编译器而异(参见维基百科),因此每个位域的确切位置可能不同,换句话说,您永远无法确定raw
每个位域的哪个位对应.
但是,如果你不打算混合它,你最好取出raw
它,你将是安全的.
那么结构映射就不会出错,因为这两个字段都是可访问的,它们可以互换使用.
位字段的一个好处是您可以轻松地聚合选项:
mask = USER|FORCE|ZERO|COMPAT; vs flags.user = true; flags.force = true; flags.zero = true; flags.compat = true;
在诸如处理协议选项的某些环境中,必须单独设置选项或使用多个参数来运送中间状态以实现最终结果.
但有时设置flag.blah并在IDE中设置列表弹出窗口非常棒,特别是如果您喜欢我并且不记得要设置的标志名称而不经常引用列表.
我个人有时会回避声明布尔类型,因为在某些时候我最终会误以为我刚刚切换的字段与其他"看似"的r/w状态不相关(思考多线程并发)碰巧共享相同32位字的无关字段.
我的投票是,它取决于具体情况,在某些情况下,这两种方法都可能很有效.
在C++中,只需使用std::bitset
.