1

我刚刚发现了位标志的乐趣。我有几个关于在 C 中使用位标志的“最佳实践”的问题。我从网上找到的各种示例中学到了一切,但仍有疑问。

为了节省空间,我在 struct ( A->flag) 中使用单个 32 位整数字段来表示几个不同的布尔属性集。总共有 20 个不同的位是#defined。其中一些是真正的存在/不存在标志(STORAGE-INTERNAL vs. STORAGE-EXTERNAL)。其他的有两个以上的值(例如互斥格式集:FORMAT-A、FORMAT-B、FORMAT-C)。我已经定义了用于设置特定位的宏(同时关闭互斥位)。我还定义了用于测试标志中是否设置了特定位组合的宏。

但是,在上述方法中丢失的是最能被枚举捕获的特定标志分组。对于编写函数,我想使用枚举(例如,STORAGE-TYPE 和 FORMAT-TYPE),这样函数定义看起来不错。我希望仅使用枚举来传递参数和#defined 宏来设置和测试标志。

  1. (a) 如何A->flag以可移植方式(跨 32 位/64 位平台)将标志 ( ) 定义为 32 位整数?

  2. (b) 我是否应该担心A->flag#defined 常量与枚举的存储方式的潜在大小差异?

  3. (c) 我是否让事情变得不必要地复杂,这意味着我应该坚持使用#defined 常量作为普通ints 传递参数吗?在这一切中我还应该担心什么?

对于这个表述不清的问题,我深表歉意。这反映了我对潜在问题的无知。

4

6 回答 6

7

有一个 C99 标头旨在解决该确切问题 (a),但由于某种原因,Microsoft 没有实现它。幸运的是,您可以<stdint.h>在此处获取 Microsoft Windows。每个其他平台都已经拥有它。32 位 int 类型是uint32_tint32_t。这些也有 8 位、16 位和 64 位版本。

所以,这需要处理(a)。

(b) 和 (c) 是同一个问题。每当我们开发某些东西时,我们都会做出假设。您假设 C 将可用。你假设<stdint.h>可以在某个地方找到它。您总是可以假设它int至少是 16 位,现在 >= 32 位的假设是相当合理的。

通常,您应该尝试编写不依赖于布局的符合要求的程序,但它们会对字长做出假设。您应该担心算法级别的性能,也就是说,我是否在编写二次、多项式、指数的东西?

在 (a) 你注意到性能滞后,并且 (b) 你已经对你的程序进行了概要分析之前,你不应该担心操作级别的性能。您需要完成工作,而不必担心个人操作。:-)

哦,我应该补充一点,当您首先用 C 编写程序时,您尤其不需要担心低级操作性能。C 是最接近金属的、尽可能快的语言。我们经常用 php、python、ruby 或 lisp 编写东西,因为我们想要一种强大的语言,而现在 CPU 的速度如此之快,以至于我们可以摆脱整个解释器,更不用说不完美的 bit-twiddle-word 选择长度操作。:-)

于 2009-10-03T23:30:48.913 回答
6

您可以使用位域并让编译器进行位旋转。例如:

struct PropertySet {
  unsigned internal_storage : 1;
  unsigned format : 4;
};

int main(void) {
  struct PropertySet x;
  struct PropertySet y[10]; /* array of structures containing bit-fields */
  if (x.internal_storage) x.format |= 2;
  if (y[2].internal_storage) y[2].format |= 2;
  return 0;
}

编辑添加结构数组

于 2009-10-03T23:40:48.113 回答
3

正如其他人所说,您的问题 (a) 可以通过使用<stdint.h>and 或者uint32_tor来解决uint_least32_t(如果您想担心具有 36 位字的 Burroughs 大型机)。请注意,MSVC 不支持 C99,但@DigitalRoss 显示了您可以在哪里获得合适的标头以与 MSVC 一起使用。

您的问题 (b) 不是问题;如果有必要,C 会为您安全地键入 convert,但它可能甚至没有必要。

最受关注的领域是 (c),尤其是格式子字段。在那里,3 个值是有效的。您可以通过分配 3 位并要求 3 位字段是值 1、2 或 4 之一来处理此问题(任何其他值都无效,因为设置的位太多或太少)。或者您可以分配一个 2 位数字,并指定 0 或 3(或者,如果您真的想要,1 或 2 之一)是无效的。第一种方法使用更多位(目前不是问题,因为您只使用 32 位中的 20 位),但它是一种纯位标志方法。

在编写函数调用时,编写没有特别的问题:

some_function(FORMAT_A | STORAGE_INTERNAL, ...);

无论 FORMAT_A 是 #define 还是枚举,这都将起作用(只要您正确指定了枚举值)。被调用的代码应该检查调用者是否注意力不集中,并写道:

some_function(FORMAT_A | FORMAT_B, ...);

但这是模块需要担心的内部检查,而不是模块用户需要担心的检查。

如果人们要在flags成员中频繁切换位,则用于设置和取消设置格式字段的宏可能是有益的。不过,有些人可能会争辩说,任何纯布尔字段几乎都不需要它(我很同情)。最好将flags成员视为不透明并提供“函数”(或宏)来获取或设置所有字段。犯错的人越少,出错的就越少。

考虑使用位域是否适合您。我的经验是它们会导致大代码而不一定是非常高效的代码;YMMV。

嗯......到目前为止,这里没有什么非常确定的。

  • 我会对所有内容都使用枚举,因为它们保证在 #define 值不存在的调试器中可见。
  • 我可能不会提供宏来获取或设置位,但有时我是一个残忍的人。
  • 我将提供有关如何设置标志字段的格式部分的指导,并可能提供一个宏来做到这一点。

像这样,也许:

enum { ..., FORMAT_A = 0x0010, FORMAT_B = 0x0020, FORMAT_C = 0x0040, ... };
enum { FORMAT_MASK = FORMAT_A | FORMAT_B | FORMAT_C };

#define SET_FORMAT(flag, newval)    (((flag) & ~FORMAT_MASK) | (newval))
#define GET_FORMAT(flag)            ((flag) & FORMAT_MASK)

SET_FORMAT如果正确使用是安全的,但如果被滥用是可怕的。宏的一个优点是您可以在必要时将它们替换为彻底验证事物的函数;如果人们始终如一地使用宏,这将很有效。

于 2009-10-04T00:48:56.900 回答
2

对于问题a,如果您使用的是C99(您可能正在使用它),您可以使用uint32_t预定义的类型(或者,如果没有预定义,可以在stdint.h头文件中找到)。

于 2009-10-03T23:17:22.280 回答
1

关于(c):如果您的枚举定义正确,您应该能够毫无问题地将它们作为参数传递。有几点需要考虑:

  1. 枚举存储通常是特定于编译器的,因此根据您正在进行的开发类型(您没有提到它是 Windows 还是 Linux 还是嵌入式还是嵌入式 Linux :)),您可能需要访问枚举存储的编译器选项来确保那里没有问题。我通常同意编译器应该适当地转换你的枚举的上述共识 - 但这是需要注意的事情。
  2. 如果您正在做嵌入式工作,如果您开始对枚举、#defines 和位域变得过于聪明,许多静态质量检查程序(如 PC Lint)将会“吠叫”。如果您正在进行需要通过任何质量关卡的开发,则可能需要牢记这一点。事实上,如果您尝试使用位域进行触发,一些汽车标准(例如 MISRA-C)会变得非常烦躁。
  3. “我刚刚发现了位标志的乐趣。” 我同意你的看法——我发现它们非常有用。
于 2009-10-04T01:09:06.313 回答
0

我在上面的每个答案中添加了评论。我想我有些清楚了。似乎枚举更干净,因为它显示在调试器中并保持字段分开。宏可用于设置和获取值。

我还读到枚举存储为小整数 - 据我了解,布尔测试不是问题,因为这些将从最右边的位开始执行。但是,枚举可以用来存储大整数(1 << 21)吗?

再次感谢大家。我已经比两天前学到了更多!

〜拉斯

于 2009-10-04T14:04:11.887 回答