在C中用单位测试和设置单个位的经典问题可能是最常见的中级编程技能之一。您可以使用简单的位掩码进行设置和测试,例如
unsigned int mask = 1<<11;
if (value & mask) {....} // Test for the bit
value |= mask; // set the bit
value &= ~mask; // clear the bit
interesting blog post认为这很容易出错,难以维护,而且做法很差。 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 union方法值得花费额外的几行来使它更便携和更安全。
双方还有其他争论吗?特别是有一些可能的失败,也许是字节顺序,位掩码方法可能会错过,但结构方法是安全的吗?
答案 0 :(得分:38)
Bitfields并不像您想象的那么便携,因为“C不能保证机器词中字段的排序”(The C book)
忽略这一点,正确使用 ,这两种方法都是安全的。这两种方法还允许对整数变量进行符号访问。你可以说比特域方法更容易编写,但它也意味着要审查更多的代码。
答案 1 :(得分:29)
如果问题是设置和清除位容易出错,那么正确的做法是编写函数或宏以确保正确执行。
// 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);这将是等效的。
真的,答案是考虑将你想要的东西与你想要的东西分离。
答案 2 :(得分:23)
Bitfields很棒且易于阅读,但遗憾的是 C语言没有指定内存中位域的布局,这意味着它们对于处理磁盘格式的打包数据基本没用二进制线协议。如果你问我,这个决定是C-Ritchie的一个设计错误可能已经选择了订单而且坚持下去。
答案 3 :(得分:19)
你必须从作家的角度考虑这一点 - 了解你的观众。因此,需要考虑几个“受众”。
首先是经典的C程序员,他们一生都在掩饰自己,并且可以在睡梦中做到这一点。
第二个是newb,谁不知道这一切是什么,&amp;东西是。他们在上一份工作中编写了php编程,现在他们为你工作了。 (我说这是一个做php的新手)
如果你写作是为了满足第一批观众(即全天的位掩码),你会让他们非常高兴,他们将能够维护蒙住眼睛的代码。但是,newb可能需要在能够维护代码之前克服大的学习曲线。他们需要了解二元运算符,如何使用这些操作来设置/清除位等等。你几乎可以肯定会遇到新手引入的bug,因为他/她需要所有这些才能让它工作。< / p>
另一方面,如果您编写以满足第二个受众,则newbs将更容易维护代码。他们会更容易理解
flags.force = 0;
大于
flags &= 0xFFFFFFFE;
并且第一批观众会变得脾气暴躁,但很难想象他们无法理解和维护新语法。搞砸了起来要困难得多。不会有新的错误,因为newb将更容易维护代码。你会得到一些讲座,讲述“在我的日子里,你需要一只稳定的手和一根磁化针来设置位......我们甚至都没有位掩码!” (感谢XKCD)。
所以我强烈建议您使用位掩码上的字段来保护您的代码。
答案 4 :(得分:14)
根据ANSI C标准,联合使用具有未定义的行为,因此不应使用(或至少不被视为可移植)。
来自ISO/IEC 9899:1999 (C99) standard:
附件J - 可移植性问题:
1未指定以下内容:
- 在结构或联合中存储值时填充字节的值(6.2.6.1)。
- 除了存储在(6.2.6.1)中的最后一个工会成员以外的工会成员的价值。
6.2.6.1 - 语言概念 - 类型表示 - 概述:
6当值存储在结构或联合类型的对象中时,包括在成员中 object,对象表示的字节,对应于任何填充字节 未指定的值。[42])结构或联合对象的值永远不是陷阱 表示,即使结构或联合对象的成员的值可能是 陷阱表示。
7当值存储在union类型的对象的成员中时,该对象的字节数 表示与该成员不对应但与其他成员对应的表示 取未指定的值。
所以,如果你想保持位域↔整数对应,并保持可移植性,我强烈建议你使用bitmasking方法,这与链接的博客文章相反,它是不差实践。
答案 5 :(得分:10)
比特场方法让你感到畏缩的是什么?
这两种技术都有它们的位置,我唯一的决定就是使用哪种技术:
对于简单的“一次性”比特摆弄,我直接使用按位运算符。
对于任何更复杂的东西 - 例如硬件寄存器映射,位域方法都会失败。
对于按位运算符,典型(坏)练习是位掩码的大量#defines。
对位域的唯一警告是确保编译器确实将对象打包成您想要的大小。我不记得这是否由标准定义,因此断言(sizeof(myStruct)== N)是一个有用的检查。
答案 6 :(得分:6)
您所指的blog post提及raw
union字段作为位域的替代访问方法。
博客文章作者使用raw
的目的是好的,但是如果你计划将其用于其他任何事情(例如位字段的序列化,设置/检查个别位),灾难只是等着你角。内存中位的排序依赖于体系结构,内存填充规则因编译器而异(参见wikipedia),因此每个位域的确切位置可能不同,换句话说,您永远无法确定{{1每个位域对应于。
但是如果你不打算混合使用,你最好把raw
拿出去,这样你就会安全。
答案 7 :(得分:6)
结构映射你不会出错,因为这两个字段都是可访问的,它们可以互换使用。
位字段的一个好处是您可以轻松地聚合选项:
mask = USER|FORCE|ZERO|COMPAT;
vs
flags.user = true;
flags.force = true;
flags.zero = true;
flags.compat = true;
在某些环境中,例如处理协议选项,必须单独设置选项或使用多个参数来运送中间状态以实现最终结果。
但有时设置flag.blah并在IDE中设置列表弹出窗口非常棒,特别是如果您喜欢我并且不记得要设置的标志名称而不经常引用列表。
我个人有时会回避声明布尔类型,因为在某些时候我最终会误以为我刚刚切换的字段对其他人的r / w状态没有依赖(思考多线程并发) “似乎”不相关的字段碰巧共享相同的32位字。
我的投票是,它取决于具体情况,在某些情况下,这两种方法都可能很有效。
答案 8 :(得分:6)
无论哪种方式,在GNU软件中已经使用了几十年的位域,并没有对它们造成任何伤害。我喜欢它们作为函数的参数。
我认为位域是传统而不是结构。每个人都知道如何和值设置各种选项,编译器将其归结为CPU上的非常高效的按位操作。
如果你以正确的方式使用掩码和测试,编译器提供的抽象应该使它健壮,简单,可读和干净。
当我需要一组开/关开关时,我将继续在C中使用它们。
答案 9 :(得分:5)
在C ++中,只需使用std::bitset<N>
。
答案 10 :(得分:4)
这很容易出错,是的。我在这种代码中看到了很多错误,主要是因为有些人认为他们应该以完全混乱的方式混淆它和业务逻辑,从而造成维护噩梦。他们认为“真正的”程序员可以在任何的地方写value |= mask;
,value &= ~mask;
甚至更糟糕的事情,这就没问题。更好的是,如果有一些增量运算符,那么几个memcpy
的指针强制转换以及当时出现的任何模糊和容易出错的语法。当然,没有必要保持一致,你可以用两种或三种不同的方式翻转位,随机分布。
我的建议是:
SetBit(...)
和ClearBit(...)
等方法在一个类中封装此----。 (如果你在模块中没有C语言的课程。)当你在它的时候,你可以记录他们的所有行为。答案 11 :(得分:3)
你的第一种方法更可取,恕我直言。为什么要混淆这个问题呢?小小的摆弄是一件非常基本的事情。 C做得对。 Endianess无关紧要。联合解决方案唯一能做的就是命名。 11可能是神秘的,但#defined为一个有意义的名字或枚举应该足够了。
无法处理“|&amp; ^〜”等基础知识的程序员可能处于错误的工作中。
答案 12 :(得分:2)
当我谷歌为“c运营商”
前三页是:
http://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B http://h30097.www3.hp.com/docs/base_doc/DOCUMENTATION/V40F_HTML/AQTLTBTE/DOCU_059.HTM http://www.cs.mun.ca/~michael/c/op.html
..所以我认为关于对语言不熟悉的人的争论有点傻。
答案 13 :(得分:2)
嗯,我想这是一种做法,但我总是喜欢keep it simple。
一旦你习惯了,使用面具很简单,明确和便携。
Bitfields非常简单,但无需进行额外的工作就无法移植。
如果您必须编写符合MISRA的代码,MISRA指南会对位域,联合以及C的许多其他方面不屑一顾,以避免未定义或依赖于实现的行为。
答案 14 :(得分:2)
我几乎总是使用位掩码来直接或作为宏使用逻辑运算。 e.g。
#define ASSERT_GPS_RESET() { P1OUT &= ~GPS_RESET ; }
顺便提一句,原始问题中的联合定义不适用于我的处理器/编译器组合。 int类型只有16位宽,位域定义是32.为了使它稍微更便携,那么你必须定义一个新的32位类型,然后你可以映射到每个目标体系结构上所需的基本类型作为移植练习。在我的情况下
typedef unsigned long int uint32_t
并在原始示例中
typedef unsigned int uint32_t
typedef union {
struct {
boolean_t user:1;
boolean_t zero:1;
boolean_t force:1;
int :28; /* unused */
boolean_t compat:1; /* bit 31 */
};
uint32_t raw;
} flags_t;
重叠的int也应该是无符号的。
答案 15 :(得分:1)
通常,更容易阅读和理解的那个也更容易维护。如果你有C的新手,那么“更安全”的方法可能会让他们更容易理解。
答案 16 :(得分:1)
Bitfield很棒,除了位操作不是原子操作,因此可能导致多线程应用程序出现问题。
例如,人们可以假设一个宏:
#define SET_BIT(val, bitIndex) val |= (1 << bitIndex)
定义原子操作,因为| =是一个语句。但是编译器生成的普通代码不会尝试生成| = atomic。
因此,如果多个线程执行不同的设置位操作,则设置位操作之一可能是虚假的。由于两个线程都将执行:
thread 1 thread 2
LOAD field LOAD field
OR mask1 OR mask2
STORE field STORE field
结果可以是字段&#39; = field OR mask1 OR mask2(有意),或者结果可以是field&#39; = field OR mask1(没有意图)或结果可以是field&#39; = field OR mask2(不打算)。
答案 17 :(得分:0)
除了要强调两点外,我没有增加太多内容:
编译器可以根据需要随意在位域中排列位。这意味着,如果您要操纵微控制器寄存器中的位,或者要将位发送给另一个寄存器处理器(甚至具有不同编译器的同一处理器),您必须使用位掩码。
另一方面,如果您试图创建位和小整数的紧凑表示形式以在单个处理器中使用,则位域更易于维护,因此出错率更低,并且- -对于大多数编译器而言-至少与手动屏蔽和移位一样有效。