【问题标题】:Bitfield manipulation in CC中的位域操作
【发布时间】:2010-11-05 21:22:23
【问题描述】:

在 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) {
                [...]
        }
        [...]
}

但这让我畏缩

我和我的同事对此的有趣争论仍未解决。这两种风格都有效,我认为经典的位掩码方法简单、安全、清晰。我的同事同意它很常见且简单,但位域联合方法值得多花几行代码以使其可移植且更安全。

双方还有什么争论吗?特别是是否存在一些可能的失败,也许是字节顺序,位掩码方法可能会丢失,但结构方法在哪里是安全的?

【问题讨论】:

  • 我认为你畏缩是对的。
  • flags.compatflags.force 是如何工作的?他们不应该是flags.{structure_name}.compatflags.{structure_name}.force吗?
  • @Lazer 可以直接在 gcc GNU 扩展中引用未命名的 struct/union 的成员,现在是 C11 标准。
  • 以这种方式使用位域是高度不可移植的。请参阅en.cppreference.com/w/c/language/bit_field 注释部分:未指定位字段的对齐和排序。

标签: c bit-manipulation


【解决方案1】:

位域并不像你想象的那么可移植,因为“C 不能保证机器字中字段的顺序”(The C book)

忽略这一点,正确使用,这两种方法都是安全的。这两种方法还允许对整数变量进行符号访问。您可以争辩说位域方法更容易编写,但也意味着要查看更多代码。

【讨论】:

  • 我在将代码移植到愚蠢的位字段顺序向后的编译器时遇到了问题。很烦人。我会坚持戴口罩,谢谢。 :)
  • @Matthew Flaschen:flags.compatflags.force 是如何工作的?他们不应该是flags.{structure_name}.compatflags.{structure_name}.force吗?
  • @eSKay,你是对的。 C 标准不支持未命名的结构字段(在问题中使用),但 gcc (gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Unnamed-Fields.html) 和 Visual C++ (msdn.microsoft.com/en-us/library/z2cx9y4f.aspx) 可以作为扩展
  • 没有“向后”,因为没有定义向前。远离。我很遗憾曾经使用位域作为从字节数据数组中提取内存的方法。
【解决方案2】:

如果问题是设置和清除位容易出错,那么正确的做法是编写函数或宏以确保正确操作。

// 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);这将是等效的。

真的,答案是考虑将你想要的东西与你想做的事情分开。

【讨论】:

  • 其实我在这里更进一步,实现了一个名为 Flags 的类,它提供了用于操作位标志的成员函数。我的基类使用原子操作(在不同线程中设置和测试时很方便),我什至从它派生了一些情况,以便在位更改时自动将标志状态存储在 Windows 注册表中。作为一个类,意味着我可以轻松地从它派生,只需将状态标志支持添加到需要此类功能的任何对象。
  • 我建议在宏扩展中至少将 bitIndex 宏参数括起来:#define SET_BIT(val, bitIndex) val |= (1 &lt;&lt; (bitIndex)) 以避免在参数是表达式时出现潜在的优先级问题。
【解决方案3】:

位域很棒且易于阅读,但不幸的是 C 语言没有指定位域在内存中的布局,这意味着它们对于处理磁盘格式的打包数据或二进制有线协议。如果你问我,这个决定是 C 语言中的一个设计错误——Ritchie 可以选择一个订单并坚持下去。

【讨论】:

  • 我有一个问题要问你:我可以将具有特定结构的字节文件(例如存储 TCP/IP 数据包的那个)直接读取到包含位字段的数据结构中(所以我会定义我自己的“struct Packet {..}”),而不是先将字节读入 char 数组,然后一个一个地处理它们?正如您提到的“C 没有指定内存中的位域布局”,这是否意味着错误的位可以分配给我的数据结构的错误位域,如果我这样做: Packet* p = malloc(sizeof(Packet)); fread(fp, 1, sizeof(Packet), p) - 可以做这样的事情吗?
  • @mercury0114:对于希望跨各种编译器和平台移植的代码,这样做并不可靠。因此上面答案的最后一句话。
  • 迟到的读者的迟到评论:即使没有位域,通过 TCP_IP 发送的此类结构 (@mercury0114) 也会带来麻烦,因为不同的编译器可能会对成员应用不同的对齐方式,因此它们可以是无论如何,便携性问题!
【解决方案4】:

您必须从作家的角度来考虑这一点——了解您的受众。所以有几个“观众”需要考虑。

首先是经典的 C 程序员,他们一生都在进行位掩码,并且可以在睡梦中做到这一点。

其次是新手,他不知道这一切 |, & 东西是什么。他们在上一份工作中编写 php,现在他们为您工作。 (我说这是一个做php的新手)

如果您写作是为了满足第一批读者(即整天使用位掩码),您会让他们非常高兴,并且他们将能够保持蒙着眼睛的代码。但是,新手可能需要克服很大的学习曲线才能维护您的代码。他们需要了解二元运算符,如何使用这些操作来设置/清除位等。几乎可以肯定,新手会引入错误,因为他/她需要使用所有技巧才能使其正常工作。

另一方面,如果您编写内容是为了满足第二批读者的需求,那么新手将更容易维护代码。他们会更容易摸索

 flags.force = 0;

 flags &= 0xFFFFFFFE;

第一批观众只会脾气暴躁,但很难想象他们无法理解和维持新的语法。只是更难搞砸。不会有新的bug,因为newb会更容易维护代码。你只会得到关于“在我那个时代,你需要一只稳定的手和一根磁化的针来设置位......我们甚至没有位掩码!”的讲座。 (感谢XKCD)。

所以我强烈建议使用位掩码上的字段来保证您的代码的新安全。

【讨论】:

  • “搞砸要难得多。”简而言之,位域。
  • 那个 |= 设置了很多标志位。标志 &= 0xFFFFFFFE;可能是?您实际上是在帮助位字段位置。而且,根据他的定义,在最高位域为位 0 的系统上,它将是 0xFFFFFFFB。
  • 是的,你是正确的 dblack。但我想这证明了整体观点:)
  • 要清除位掩码 N,我会使用标志 &= ~N;这具有不受标志大小限制的优点。使用 64 位整数运行您的代码,您的代码稍微有点 scr*wed...
  • 我真的不认为位掩码那么难。在 Wikipedia 上阅读 30 分钟(或更少)的时间,您就可以了解正在发生的事情。
【解决方案5】:

根据 ANSI C 标准,联合用法具有未定义的行为,因此不应使用(或至少不被视为可移植)。

来自ISO/IEC 9899:1999 (C99) standard

附件 J - 可移植性问题:

1 以下未指定:

——在结构或联合中存储值时填充字节的值 (6.2.6.1)。

- 联合成员的值,而不是存储到 (6.2.6.1) 中的最后一个成员。

6.2.6.1 - 语言概念 - 类型的表示 - 常规:

6 当值存储在结构或联合类型的对象中时,包括在成员中 对象,对应于任何填充字节的对象表示的字节采用 未指定的值。[42])结构或联合对象的值永远不是陷阱 表示,即使结构或联合对象的成员的值可能是 陷阱表示。

7 当一个值存储在联合类型对象的成员中时,该对象的字节数 不对应于该成员但对应于其他成员的表示 取未指定的值。

所以,如果你想保持位域↔整数的对应关系,并保持可移植性,我强烈建议你使用位掩码方法,与链接的博客文章相反,它不是差练习。

【讨论】:

  • 附件 J 是非规范性的。此外,附件 J 表示该值未指定,而不是您声称的未定义行为。 (这是一个有争议的问题,因为它是非规范的)。 6.2.6.1/7 指的是联合对象的字节,这些字节不位于正在写入的子对象中。一份缺陷报告澄清了写一个工会成员和读另一个工会成员的行为就像类型双关语;明确的措辞出现在 C11 标准中(它取消并取代了旧标准)
【解决方案6】:

位域方法让你畏缩的原因是什么?

这两种技术各有所长,我唯一的决定是使用哪一种:

对于简单的“一次性”位摆弄,我直接使用位运算符。

对于更复杂的东西 - 例如硬件寄存器映射,位域方法胜出。

  • 位域使用起来更简洁 (以/稍微/更多为代价 写的冗长。
  • 位域是 更健壮(“int”的大小是多少, 无论如何)
  • 位域通常只是 与按位运算符一样快。
  • 位域非常强大,当你 混合了单个和多个位 字段,并提取 多位字段涉及负载 手动班次。
  • 位域是 有效地自我记录。经过 定义结构,因此 命名元素,我知道它是什么 打算这样做。
  • 位域还可以无缝处理大于单个 int 的结构。
  • 对于位运算符,典型的(不好的)做法是对位掩码使用大量#define。

  • 对位域的唯一警告是确保编译器确实将对象打包成您想要的大小。我不记得这是否由标准定义,因此 assert(sizeof(myStruct) == N) 是一个有用的检查。

【讨论】:

  • 我发现当我从一个嵌入式平台迁移到另一个嵌入式平台时,它通常需要编写示例代码并检查生成的程序集以确认从结构中的位域到实际机器字的映射。太多的编译器将他们的文档组织得很好,以至于很容易查找。我也遇到了硬件要求保证内存访问为特定大小的问题,我不得不找到一个几乎没有记录的开关来关闭破坏它的优化。
  • 您没有提到的另一个缺点是您必须检查位是否从最高有效位到最低有效位排序,反之亦然。
【解决方案7】:

您所指的blog post 提到raw 联合字段作为位字段的替代访问方法。

博文作者使用raw 的目的是可以的,但是如果您打算将它用于其他任何事情(例如位字段的序列化、设置/检查单个位),灾难就在拐角处等着您。内存中位的顺序取决于体系结构,并且内存填充规则因编译器而异(请参阅wikipedia),因此每个位域的确切位置可能不同,换句话说,您永远无法确定raw 每个位域的哪个位对应。

但是,如果您不打算混合使用,最好将 raw 带出去,这样您就安全了。

【讨论】:

  • 字节顺序不应该影响它,只要您使用掩码的具体表示。这样,两者都受到字节顺序的同等影响,这意味着高/低位相关。
  • @Aiden,我认为他的观点是原始字段和(匿名)结构成员之间的映射高度依赖于平台。过去,我在嵌入式项目中被此严重烧毁,只是试图编写一个与寄存器的数据表描述相匹配的结构。当然,#$@(&* 制造商对位进行编号使得位 0 是高位位这一事实根本没有帮助!
  • @RBerteig 正是我的意思!在我的帖子的初始版本中,我也提到了字节序,但因为即使在具有相同字节序的架构上也不能保证它我把它拿出来。
【解决方案8】:

结构映射不会出错,因为这两个字段都是可访问的,它们可以互换使用。

位域的一个好处是您可以轻松地聚​​合选项:

mask = USER|FORCE|ZERO|COMPAT;

vs

flags.user = true;
flags.force = true;
flags.zero = true;
flags.compat = true;

在某些环境中,例如处理协议选项,必须单独设置选项或使用多个参数来传递中间状态以影响最终结果可能会变得相当陈旧。

但有时设置 flag.blah 并在您的 IDE 中弹出列表非常棒,特别是如果您像我一样并且不记得要设置的标志的名称而不经常引用列表。

我个人有时会回避声明布尔类型,因为在某些时候我会产生错误的印象,即我刚刚切换的字段不依赖于(想想多线程并发)其他的读/写状态“看似”不相关的字段碰巧共享相同的 32 位字。

我的投票是,这取决于具体情况,在某些情况下,两种方法都可能效果很好。

【讨论】:

    【解决方案9】:

    不管怎样,位域已经在 GNU 软件中使用了几十年,并且没有对它们造成任何伤害。我喜欢它们作为函数的参数。

    我认为位域是传统的,而不是结构。每个人都知道如何通过 AND 值来设置各种选项,编译器将其归结为 非常有效 CPU 上的按位运算。

    如果您以正确的方式使用掩码和测试,编译器提供的抽象应该使其健壮、简单、可读和干净。

    当我需要一组开/关开关时,我会继续在 C 中使用它们。

    【讨论】:

      【解决方案10】:

      在 C++ 中,只需使用 std::bitset&lt;N&gt;

      【讨论】:

        【解决方案11】:

        这很容易出错,是的。我在这种代码中看到了很多错误,主要是因为有些人觉得他们应该以完全杂乱无章的方式弄乱它和业务逻辑,从而造成维护噩梦。他们认为“真正的”程序员可以在任何地方写value |= mask;value &amp;= ~mask;或更糟糕的东西,这没关系。如果周围有一些增量运算符、几个memcpy、指针强制转换以及当时他们想到的任何晦涩且容易出错的语法,那就更好了。当然不需要保持一致,你可以用两种或三种不同的方式翻转比特,随机分布。

        我的建议是:

        1. SetBit(...)ClearBit(...)等方法将这个----封装在一个类中。 (如果您没有 C 语言中的类,则在一个模块中。)当您使用它时,您可以记录它们的所有行为。
        2. 对该类或模块进行单元测试。

        【讨论】:

        • 类、方法、模块?我一个字都听不懂。
        • @Nosredna:你可以随时问。
        • 增量运算符、memcpy 和指针强制转换 - 你真的认为这很晦涩吗?引用、复制构造函数、模板和运算符重载,现在这些都晦涩难懂!
        • 我提到“增量运算符”是为了说明在同一条指令中混合了查询和命令。而且 memcpy 的级别太低,因此晦涩难懂。这是典型的低级沙拉的结果,它同时处理字节操作和几个值修改,因此我称之为晦涩难懂。你可以编写很难维护的代码,逐行阅读,很清楚它“做什么”,但没有人知道为什么。
        【解决方案12】:

        您的第一种方法更可取,恕我直言。为什么要混淆这个问题?位摆弄是一个非常基本的事情。 C做得对。字节序无关紧要。 union 解决方案唯一要做的就是命名事物。 11 可能很神秘,但 #defined 为有意义的名称或枚举就足够了。

        不能处理像“|&^~”这样的基础知识的程序员可能是在错误的工作领域。

        【讨论】:

        • -1: "like '|&^~' 可能是在错误的工作范围内。"。虽然我自己是一名 C 程序员......我可以理解按位运算让高级语言的程序员感到困惑。不会让他们像不了解 OOP 的汇编大师那样成为程序员。
        • @Aiden - 我不同意。无论您是哪种程序员,即使您整天都在使用功能性脚本元语言编写 Web 应用程序,如果您不理解位和字节,您就会错过正在发生的事情的基本基础。 @xcramps,如果您要处理大于字节的任何内容,例如处理 32 位整数的操作示例,字节顺序绝对很重要(尤其是对于使用网络的任何内容)。
        【解决方案13】:

        当我在谷歌上搜索“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

        ..所以我认为关于语言新手的争论有点愚蠢。

        【讨论】:

        • 我完全同意。我相信 20 分钟足以很好地掌握按位运算符。
        【解决方案14】:

        我几乎总是直接或作为宏使用带有位掩码的逻辑运算。例如

         #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】:

          好吧,我想这是一种方法,但我总是更喜欢keep it simple

          一旦你习惯了,使用面具就很简单、明确和便携。

          位域很简单,但如果不做额外的工作,它们是不可移植的。

          如果您必须编写符合 MISRA 的代码,MISRA 指南不赞成位域、联合以及 C 的许多其他方面,以避免未定义或依赖于实现的行为。

          【讨论】:

            【解决方案16】:

            一般来说,更容易阅读和理解的就是更容易维护的。如果您的同事不熟悉 C,“更安全”的方法可能更容易让他们理解。

            【讨论】:

            • 我看了两段代码,对我来说第一段看起来更容易理解。第二个可能更安全。
            • 就个人而言,我同意你的看法。然而,也许就我一个人,却遇到了很多不知道什么的“C高手” | 天天要闻或 & 做。
            【解决方案17】:

            位域很棒,只是位操作操作不是原子的,因此会导致多线程应用程序出现问题。

            例如,可以假设一个宏:

            #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
            

            结果可以是 field' = field OR mask1 OR mask2(有意),或者结果可以是 field' = field OR mask1(无意),或者结果可以是 field' = field OR mask2(无意)。

            【讨论】:

              【解决方案18】:

              除了强调两点外,我不会对已经说过的内容添加太多内容:

              编译器可以随意在位域中任意排列位。这意味着如果您尝试操作微控制器寄存器中的位,或者如果您想将位发送到另一个处理器(甚至是具有不同编译器的相同处理器),您必须使用位掩码。

              另一方面,如果您尝试创建用于单个处理器的位和小整数的紧凑表示,位域更易于维护,因此更不容易出错,并且 - - 对于大多数编译器 - 至少与手动屏蔽和移位一样有效。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2018-04-06
                • 1970-01-01
                • 2017-01-12
                • 2021-12-25
                • 2011-10-14
                • 1970-01-01
                相关资源
                最近更新 更多