位标志是什么的简要概述
位标志是定义一组某种类型的常量,通常是各种类型的选项。位标志通常定义为十六进制常量,其目的是将位运算符与这些常量一起使用,以便通过使用位运算符从常量的总集合中创建一些子集。
位运算符是|
(bitwise Or)、&
(bitwise And)、^
(bitwise Exclusive Or)和~
(bitwise Not)等运算符,它们对两个值逐位执行运算符指定的布尔逻辑运算,生成一个新的价值。位运算符不同于逻辑运算符,例如||
(逻辑或)、&&
(逻辑与),它们与计算结果为布尔值 true(非零)或 false(零)的表达式一起使用。
define
使用 C 预处理器指令创建按位标志的典型定义示例如下:
#define ITM_FLAG_EXAMPLE1 0x00000001L // an example bitwise flag
#define ITM_FLAG_EXAMPLE2 0x00000002L // another example flag
#define ITM_FLAG_JUMP01 0x00040000L // another example
#define ITM_FLAG_JUMP02 0x00080000L // another example
此外,现代 C 编译器也将允许使用enum
类型。
typedef enum { ITEM_FLAG_1 = 1, ITEM_FLAG_2 = 2, ITEM_FLAG_3 = 4 } ItemType;
ItemType AnItem = ITEM_FLAG_1; // defining a variable of the type
ItemType AnItem2 = ITEM_FLAG_1 | ITEM_FLAG_2; // defining a second variable
或者
enum { ITEM_FLAG_1 = 1, ITEM_FLAG_2 = 2, ITEM_FLAG_3 = 4 } ItemType;
enum ItemType AnItem = ITEM_FLAG_1; // defining a variable of the type
enum ItemType AnItem2 = ITEM_FLAG_1 | ITEM_FLAG_2; // defining a second variable
以及如何使用这些的一些示例:
unsigned long ulExample = ITM_FLAG_EXAMPLE2; // ulExample contains 0x00000002L
unsigned long ulExamplex = ITM_FLAG_EXAMPLE1 | ITM_FLAG_EXAMPLE2; // ulExamplex contains 0x00000003L
unsigned long ulExampley = ulExamplex & ITM_FLAG_EXAMPLE2; // ulExampley contains 0x00000002L
请参阅这篇博文,介绍真值表和布尔代数,其中描述了各种布尔代数运算以及关于真值表的这个 Wikipedia 主题。
使用位标志的一些注意事项
使用位标志时可能会遇到一些问题,并且需要注意一些事项。
- 使用位标志定义的常量为零,未设置位,可能会导致问题
- 与逻辑运算混合的按位运算可能是缺陷区域
通常,使用定义为零而不是非零值的位标志会导致错误。大多数程序员都希望位标志有一个非零值,表示某种集合中的成员资格。定义为零的位标志是一种违反预期的行为,在按位运算中使用时可能会导致意想不到的后果和行为。
当将位标志变量的按位运算与逻辑运算符结合起来时,强制特定运算符优先级的括号通常更容易理解,因为它不需要读者知道C 运算符优先级表。
关于已发布的问题
当提供库时,通常有一个或多个伴随库文件的包含文件,以便提供许多需要的项目。
- 库提供的函数的函数原型
- 库使用的类型的变量类型声明和定义
- 用于控制库函数行为的操作数和标志的特殊常量
位标志是一种为功能接口提供选项的历史悠久的方式。位标志有几个很好的属性,使它们对 C 程序员很有吸引力。
- 易于通过接口传输的紧凑表示
- 非常适合布尔代数运算和集合操作
- 与 C 位运算符自然匹配以执行这些操作
位标志可以节省空间,因为标志的名称可能很长且具有描述性,但可以编译为单个无符号值,例如 unsigned short 或 unsigned long 或 unsigned char。
使用位标志的另一个好处是,当对表达式中的常量使用按位运算符时,大多数现代编译器会将按位运算作为编译表达式的一部分进行计算。因此,现代编译器将在表达式中采用多个按位运算符,例如O_RDONLY | O_WRONLY
在编译源代码时执行按位或运算,并将表达式替换为评估表达式的值。
在大多数计算机体系结构中,按位运算符是使用加载数据的寄存器执行的,然后执行按位运算。对于 32 位架构,使用 32 位变量包含一组位自然适合 CPU 寄存器,就像在 64 位架构中一样,使用 32 位或 64 位变量包含一组位自然适合寄存器。这种自然契合允许对相同变量进行多次按位运算,而无需从 CPU 缓存或主内存中进行提取。
C 的位运算符几乎总是具有 CPU 机器指令类似物,因此 C 位运算符具有几乎完全相同的 CPU 操作,因此编译器生成的结果机器代码非常有效。
通过使用unsigned long
传递 32 个不同的标志或unsigned long long
将 64 个不同的标志传递给函数,可以很容易地看到位标志的紧凑表示。通过使用数组偏移和位标志方法以及一组 C 处理器宏或一组函数来操作数组,unsigned char
可以使用数组来传递更多标志。
一些可能的例子
位运算符与用于集合的逻辑运算符非常相似,并且用位标志表示集合效果很好。如果您有一个包含操作数的集合,其中一些不应该与其他一些标志一起使用,那么使用按位运算符和位掩码可以很容易地查看是否指定了两个冲突标志。
#define FLAG_1 0x00000001L // a required flag if FLAG_2 is specified
#define FLAG_2 0x00001000L // must not be specified with FLAG_3
#define FLAG_3 0x00002000L // must not be specified with FLAG_2
int func (unsigned long ulFlags)
{
// check if both FLAG_2 and FLAG_3 are specified. if so error
// we do a bitwise And to isolate specific bits and then compare that
// result with the bitwise Or of the bits for equality. this approach
// makes sure that a check for both bits is turned on.
if (ulFlags & (FLAG_2 | FLAG_3) == (FLAG_2 | FLAG_3)) return -1;
// check to see if either FLAG_1 or FLAG_3 is set we can just do a
// bitwise And against the two flags and if either one or both are set
// then the result is non-zero.
if (ulFlags & (FLAG_1 | FLAG_3)) {
// do stuff if either or both FLAG_1 and/or FLAG_3 are set
}
// check that required option FLAG_1 is specified if FLAG_2 is specified.
// we are using zero is boolean false and non-zero is boolean true in
// the following. the ! is the logical Not operator so if FLAG_1 is
// not set in ulFlags then the expression (ulFlags & FLAG_1) evaluates
// to zero, False, and the Not operator inverts the False to True or
// if FLAG_1 is set then (ulFlags & FLAG_1) evaluates to non-zero, True,
// and the Not operator inverts the True to False. Both sides of the
// logical And, &&, must evaluate True in order to trigger the return.
if ((ulFlags & FLAG_2) && ! (ulFlags & FLAG_1)) return -2;
// other stuff
}
例如,请参阅将 select() 用于非阻塞套接字,以简要概述socket()
使用位标志的标准接口和select()
函数,以了解使用与位标志可以完成的操作类似的集合操作抽象的示例。这些socket
函数允许设置各种特性,例如通过对函数使用位标志来设置非阻塞,fcntl()
并且该select()
函数具有一组相关的函数/宏(FD_SET()
、FD_ZERO()
等),它提供了一个抽象来指示哪些套接字句柄是中进行监控select()
。我并不是要暗示select()
套接字集是位图,尽管在最初的 UNIX 中,我相信它们是位图。然而抽象的设计select()
及其相关的实用程序提供了一种可以用位标志实现的集合。
对包含按位标志的变量的评估也可以更快、更容易、更高效、更易读。例如,在使用某些已定义标志调用的函数中:
#define ITEM_FLG_01 0x0001
#define ITEM_FLG_02 0x0002
#define ITEM_FLG_03 0x0101
#define ITEM_FLG_04 0x0108
#define ITEM_FLG_05 0x0200
#define ITEM_FLG_SPL1 (ITEM_FLG_01 | ITEM_FLG_02)
可能会有这样的switch()
声明:
switch (bitwiseflags & ITEM_FLG_SPL1) {
case ITEM_FLG_01 | ITEM_FLG_02:
// do things if both ITEM_FLG_01 and ITEM_FLG_02 are both set
break;
case ITEM_FLG_01:
// do things if ITEM_FLG_01 is set
break;
case ITEM_FLG_02:
// do things if ITEM_FLG_02 is set
break;
default:
// none of the flags we are looking for are set so error
return -1;
}
并且您可以使用与上面相同的定义来做一些简短的表达式,例如以下。
// test if bitwiseflags has bit ITEM_FLG_5 set and if so then call function
// doFunc().
(bitwiseflags & ITEM_FLG_5) == ITEM_FLG_5 && doFunc();
附录:用于真正大型位标志集的技术
请参阅此答案,Creating bitflag variables with large amount of flags or how to create large bit-width numbers for a approach for large set, than can fit in a 32 bit or 64 bit variable, of bitflags。