枚举标志的初学者指南






3.54/5 (97投票s)
一篇解释如何在 C++ 中处理标志的文章
目录
引言
我曾(再次)浏览 Visual C++ 论坛,不得不面对一个事实:按位操作,尤其是二进制,对于初学者来说很少是常识。在费尽力气写了一个非常长的答案给那位初学者后,我意识到我必须通过这篇文章与社区分享这些鲜为人知的知识。
这显然是一篇入门文章,但如果您想深入了解 C/C++ 的按位运算符,可以阅读 PJ Arends 的非常全面的文章 《按位运算符简介》。您还可以通过 Sean Eron Anderson 的文章 《位操作技巧》 进行一项非常复杂(但有效)的位操作分析。
我将尽我所能完整地介绍我们如何使用按位运算符、标志以及所有这些二进制操作。
事实
我们最常发现这些操作的应用之一是,当一个库提供了一系列枚举,并且函数使用 DWORD
作为标志容器时。我们以文章中的一个 enum
为例,该枚举定义了一些样式:
enum {
STYLE1 = 1,
STYLE2 = 2,
STYLE3 = 4,
STYLE4 = 8,
STYLE5 = 16,
STYLE6 = 32,
STYLE7 = 64,
STYLE8 = 128
};
|
或者 |
enum {
STYLE1 = 0x1,
STYLE2 = 0x2,
STYLE3 = 0x4,
STYLE4 = 0x8,
STYLE5 = 0x10,
STYLE6 = 0x20,
STYLE7 = 0x40,
STYLE8 = 0x80
};
|
正如我们所见,这些常量都是 **2 的幂**。要理解为什么选择这些常量,我们必须看一下二进制表示:
1 -> 0b 00000000 00000000 00000000 00000001
2 -> 0b 00000000 00000000 00000000 00000010
4 -> 0b 00000000 00000000 00000000 00000100
8 -> 0b 00000000 00000000 00000000 00001000
16 -> 0b 00000000 00000000 00000000 00010000
32 -> 0b 00000000 00000000 00000000 00100000
64 -> 0b 00000000 00000000 00000000 01000000
128 -> 0b 00000000 00000000 00000000 10000000
请注意,对于所有这些值,每次只有一个 位
被设置,所有其他位都等于 0
。现在您可以看到这里出现了很大的兴趣点:每个位都被用作一个功能的标志(这里,每个位代表一个样式)。我们现在可以设想一种方法,将标志混合在一个变量中,以避免使用与标志数量一样多的布尔变量。请看下面的示例:
0b 00000000 00000000 00000000 00100101
Flags of Style1, Style3 and Style6 are set
主运算符
现在我们面临一个问题。C++ 不直接处理二进制。我们必须使用 按位运算符
。有 **3 个原子按位** 运算符需要了解,按优先级升序排列:**OR** (|
)、**AND** (&
) 和 **NOT** (~
)。以下是它们的功能:
x y | x y & x ~
--------- --------- -------
0 0 0 0 0 0 0 1
0 1 1 0 1 0 1 0
1 0 1 1 0 0
1 1 1 1 1 1
了解了这些,我们就可以使用这些运算符来构建上面介绍的**混合**。
在指令 STYLE1 | STYLE3 | STYLE6
的情况下,我们像这样对常量进行 OR
操作:
0b 00000000 00000000 00000000 00000001 <- STYLE1
0b 00000000 00000000 00000000 00000100 <- STYLE3
0b 00000000 00000000 00000000 00100000 <- STYLE6
-----------------------------------------------
0b 00000000 00000000 00000000 00100101 <- STYLE1 | STYLE3 | STYLE6
我们可以看到,按位 OR
运算符与加法 (+) 运算符非常相似。但是,如果您想使用 + 而不是 |,则必须非常小心。原因很简单:将 1 + 1
相加结果是 0
(加上一个进位 1
)。如果所有常量都严格不同,您不会看到任何问题,但我不会深入探讨,因为在处理二进制操作时使用非按位运算符是一种不良做法。
DWORD 的应用
通常,此类混合值会保留在 DWORD
类型中。但是,这并非强制要求,因为我们可以对任何整数类型(char
、short
、int
、long
...)执行此操作。DWORD
是一个无符号 32 位整数(就像本文的二进制表示中使用的那些)。让我们设想一种情况,在一个函数中构造了这样的 DWORD
,并将其作为参数传递给另一个函数。被调用的函数如何知道哪些位被设置,哪些位未被设置?很简单……跟我来!
假设我们想知道在传递给参数的 DWORD
中,STYLE8
的位是否被设置。我们必须有一个掩码,我们将称之为 AND
参数。实际上,掩码与我们要测试的常量相同,因此无需额外的代码来创建这样的掩码:
DWORD parameter -> 0b 00000000 00000000 00000000 00100101
STYLE8 mask -> 0b 00000000 00000000 00000000 10000000
----------------------------------------
Bitwise AND -> 0b 00000000 00000000 00000000 00000000 <- 0x00000000
DWORD parameter -> 0b 00000000 00000000 00000000 10100101
STYLE8 mask -> 0b 00000000 00000000 00000000 10000000
----------------------------------------
Bitwise AND -> 0b 00000000 00000000 00000000 10000000 <- STYLE8
如果 AND
操作返回 0
,则位未被设置,否则,您将得到应用的掩码。
好的,现在,在实践中,您通常会看到如下方式:
void SetStyles(DWORD dwStyles) {
if ((STYLE1 & dwStyles) == STYLE1) {
//Apply style 1
}
else if ((STYLE2 & dwStyles) == STYLE2) {
//Apply style 2
}
else if ((STYLE3 & dwStyles) == STYLE3) {
//Apply style 3
}
//etc...
}
我还没有介绍第三个运算符 **NOT** (~
)。它通常用于您拥有一组位,其中有些位被设置,有些位未被设置,并且您想删除其中一个。下面的代码示例展示了如何做到这一点:
void RemoveStyle5 (DWORD& dwStyles) {
if ((STYLE5 & dwStyles) == STYLE5) {
dwStyles = dwStyles & ~STYLE5;
}
}
我还没有提到 **XOR** ( ^
) 运算符。它之所以排在最后,仅仅是因为该运算符不是 **原子** 的;这意味着我们可以使用前面介绍的其他运算符来重现它的行为。
#define XOR(a,b) (((a) & ~(b)) | ((b) & ~(a)))
无论如何,此运算符可用于轻松切换一个位:
void SwitchStyle5 (DWORD& dwStyles) {
dwStyles = dwStyles ^ STYLE5;
}
其他运算符
现在,为了完美您的位处理技能,还有几个其他运算符您需要了解才能所向披靡:移位运算符。它们可以通过以下方式识别:<<
(左移,沿着箭头方向)、>>
(猜猜是什么,这是右移)。
移位运算符是将位的 *n* 个位置向右或向左移动。移动产生的“空间”用零填充,被推过内存区域边界的位将丢失。
举个例子:
BYTE dwStyle = STYLE1 | STYLE3 | STYLE6; // [00100101]000
<-
dwStyle = dwStyle << 3; // 001[00101000]
BYTE dwStyle = STYLE1 | STYLE3 | STYLE6; // 000[00100101]
->
dwStyle = dwStyle >> 3; // [00000100]101
“但是,”我听到您说,“这有什么用?”好吧,有很多应用程序我现在想不起来,但请相信我,当您需要它时,庆幸它的存在。
无论如何,我可以想到两种有趣的移位运算符用法是整数除以 2 和乘以 2。请看:
char i = 127; // 0b01111111 ( = 127)
i = i >> 1; // 0b00111111 ( = 63)
i = i << 1; // 0b01111110 ( = 126)
向右移一位,然后向左移一位,其结果与最初不同。这是因为正如我们刚才看到的,“溢出”的位丢失了,新插入的位设置为 0。但让我们仔细看看。这仍然有效,因为我们正在处理整数。也就是说,将一个奇数除以 2 不会得到一个浮点数(我们不关心剩余的 0.5)。
在这里,127 / 2
应该是 63.5
,但由于截断,它得到 63
。63 * 2 = 126
,这不正是我们想要的吗?! :-)
现在我们已经了解了所有有用的运算符,只需知道它们都有自己的赋值运算符。
dwStyle &= 0x2 --> dwStyle = dwStyle & 0x2
dwStyle |= 0x2 --> dwStyle = dwStyle | 0x2
dwStyle ^= 0x2 --> dwStyle = dwStyle ^ 0x2
dwStyle <<= 0x2 --> dwStyle = dwStyle << 0x2
dwStyle >>= 0x2 --> dwStyle = dwStyle >> 0x2
一个有趣的例子
感谢 VC++ 论坛上的 Randor,以下是一个关于使用这些运算符的可能性的小例子(下面这个巧妙的小技巧的发现功劳归功于斯坦福大学的 Sean Anderson)。
“如何反转一个整数的位,例如从 11010001 到 10001011?”
BYTE b = 139; // 0b10001011
b = ((b * 0x0802LU & 0x22110LU) | (b * 0x8020LU & 0x88440LU)) * 0x10101LU >> 16;
“哇!这是怎么回事,哥们?!”
DWORD iTmp = 0;
BYTE b = 139; // 00000000 00000000 00000000 10001011
DWORD c = (b * 0x0802LU); // 00000000 00000100 01011001 00010110
c &= 0x22110LU; // 00000000 00000000 00000001 00010000
DWORD d = (b * 0x8020LU); // 00000000 01000101 10010001 01100000
d &= 0x88440LU; // 00000000 00000000 10000000 01000000
iTmp = (c | d); // 00000000 00000000 10000001 01010000
iTmp = iTmp * 0x10101LU; // 10000001 11010001 11010001 01010000
iTmp >>= 16; // 00000000 00000000 10000001 11010001
b = iTmp; // 00000000 00000000 00000000 11010001
结论
好了,到这里就结束了。如果您来这里是为了理解这些二进制操作,我希望我已经帮助了您。如果您来看发生了什么,那么请不要犹豫指出我的错误(如果有的话),以便我进行修正。
链接
如果您想深入了解位处理,以下链接提供了补充知识: