65.9K
CodeProject 正在变化。 阅读更多。
Home

枚举标志的初学者指南

2006年4月10日

CPOL

6分钟阅读

viewsIcon

320952

downloadIcon

643

一篇解释如何在 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 类型中。但是,这并非强制要求,因为我们可以对任何整数类型(charshortintlong...)执行此操作。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)。

如何反转一个整数的位,例如从 1101000110001011

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

结论

好了,到这里就结束了。如果您来这里是为了理解这些二进制操作,我希望我已经帮助了您。如果您来看发生了什么,那么请不要犹豫指出我的错误(如果有的话),以便我进行修正。

链接

如果您想深入了解位处理,以下链接提供了补充知识:

© . All rights reserved.