O-2 位运算符
位运算符
C++ 提供了 6 个位操作运算符,通常称为位运算符:
| Operator | Symbol | Form | The operation returns a value where: |
|---|---|---|---|
| left shift | << | x << n | the bits from x are shifted left by n positions, new bits are 0. |
| right shift | >> | x >> n | the bits from x are shifted right by n positions, new bits are 0. |
| bitwise NOT | ~ | ~x | each bit from x is flipped. |
| bitwise AND | & | x & y | each bit is set when both corresponding bits in x and y are 1. |
| bitwise OR | x | ||
| bitwise XOR | ^ | x ^ y | each bit is set when the corresponding bits in x and y are different. |
这些是非修饰运算符(它们不会修改其操作数)。
作者注
在以下示例中,我们将主要处理4位二进制值。此举旨在简化示例并方便说明。实际程序中使用的位数取决于对象大小(例如2字节对象将存储16位)。
为提升可读性,代码示例外的二进制常量可省略0b前缀(例如0b0101可简写为0101)。
位运算符适用于整数类型及std::bitset。示例中将使用std::bitset,因其输出结果更易于二进制展示。
避免对带符号整数操作数使用位运算符,因为在 C++20 之前,许多运算符会返回实现定义的结果,或者存在其他潜在陷阱,而使用无符号操作数(或 std::bitset)可轻松规避这些问题。
最佳实践
为避免意外结果,请使用无符号整数操作数或 std::bitset 进行位运算。
位移运算符:左移运算符(<<)与右移运算符(>>)
左移运算符bitwise left shift(<<)将位向左移动。左操作数是提供初始位序列的表达式,右操作数是指定位移位数的整数。例如,当我们写 x << 2 时,表示“生成一个值,其中 x 的位向左移动了 2 个位置”。
左操作数不会被修改,从右侧移入的新位均为 0。
以下是将位序列 0011 左移的示例:
0011 \<\< 1 is 0110
0011 \<\< 2 is 1100
0011 \<\< 3 is 1000
请注意在第三种情况下,我们将数字末尾的1位移除了!从位序列末尾移除的位将永远丢失。
右移运算符bitwise right shift(>>)的工作原理类似,但会将位向右移位。
以下是位序列1100向右移位的示例:
1100 />/> 1 is 0110
1100 />/> 2 is 0011
1100 />/> 3 is 0001
请注意,在第三种情况下,我们向右偏移了一点,因此数字的右端被丢失了。
下面我们用C++编写一个示例,你可以编译并运行它:
#include <bitset>
#include <iostream>
int main()
{
std::bitset<4> x { 0b1100 };
std::cout << x << '\n';
std::cout << (x >> 1) << '\n'; // shift right by 1, yielding 0110
std::cout << (x << 1) << '\n'; // shift left by 1, yielding 1000
return 0;
}
这将输出:

对于高级读者
C++中的位移操作与端序endian无关。左移始终指向最高有效位,右移则指向最低有效位。
什么!?运算符operator<<和运算符operator>>不是用于输入和输出吗?
确实如此。
如今的程序通常很少使用位移运算符来进行位移操作。相反,位移运算符更常与std::cout(或其他输出流对象)配合使用来输出文本。请看以下程序:
#include <bitset>
#include <iostream>
int main()
{
unsigned int x { 0b0100 };
x = x << 1; // use operator<< for left shift
std::cout << std::bitset<4>{ x } << '\n'; // use operator<< for output
return 0;
}
该程序输出:

在上面的程序中,运算符operator<<如何知道在一种情况下执行位移操作,而在另一种情况下输出x?答案在于它会检查操作数的类型。如果左操作数是整数类型,则运算符operator<<会执行常规的位移操作;如果左操作数是输出流对象(如std::cout),则它会执行输出操作。
运算符operator>>的情况也是如此。
相关内容
运算符根据实参类型改变行为的能力,正是基于运算符重载这一特性实现的。我们将在第13.5节——I/O运算符重载介绍中详细介绍该特性。
请注意,若同时使用运算符operator<<进行输出和左移操作,则需使用括号来实现左移:
#include <bitset>
#include <iostream>
int main()
{
std::bitset<4> x{ 0b0110 };
std::cout << x << 1 << '\n'; // print value of x (0110), then 1
std::cout << (x << 1) << '\n'; // print x left shifted by 1 (1100)
return 0;
}
这将输出:

第一行输出变量x的值(0110),随后输出字面量1。第二行输出变量x左移一位后的值(1100)。
位运算取反
位运算取反bitwise NOT运算符(~)的概念很简单:它只是将每个位从0翻转为1,或从1翻转为0。
~0011 is 1100
~0000 0100 is 1111 1011
对于高级读者
当被解释为整数时,位取反运算的结果位数会影响最终值。
以下程序演示了这一特性:
#include <bitset> #include <iostream> int main() { std::bitset<4> b4{ 0b100 }; // b4 is 0100 std::bitset<8> b8{ 0b100 }; // b8 is 0000 0100 std::cout << "Initial values:\n"; std::cout << "Bits: " << b4 << ' ' << b8 << '\n'; std::cout << "Values: " << b4.to_ulong() << ' ' << b8.to_ulong() << "\n\n"; b4 = ~b4; // flip b4 to 1011 b8 = ~b8; // flip b8 to 1111 1011 std::cout << "After bitwise NOT:\n"; std::cout << "Bits: " << b4 << ' ' << b8 << '\n'; std::cout << "Values: " << b4.to_ulong() << ' ' << b8.to_ulong() << '\n'; return 0; }这将输出:
初始时,b4和b8均设置为0b100。在补零后,b4最终变为0100,b8变为00000100,该结果在下一行输出。
随后我们使用 to_ulong() 成员函数将位值解释为长整型。可见 b4 和 b8 均输出数值 4。尽管位数不同,它们表示的数值相同,这是因为前导零位对解释后的整型数值不产生影响。
接着使用位反转操作翻转各位,使得 b4 变为 1011,b8 变为 1111 1011。当以整数形式输出时,分别得到 11 和 251。可见这两个值不再相同,这是因为前导 1 位会影响解释后的整数值,而 b8 的前导 1 位数量多于 b4。
位运算或
位运算或Bitwise OR(|)的工作原理与其逻辑或类似。如您所知,逻辑或运算当任一操作数为真时返回真(1),否则返回假(0)。
然而,逻辑或运算对整个操作数进行处理(产生单一的真或假结果),而位或运算则对操作数中的每对位进行处理(为每个位产生单一的真或假结果)。
让我们通过一个示例来说明。考虑表达式 0b0101 | 0b0110。
提示
要手动执行任何二进制位运算,最简便的方法是将两个操作数对齐排列如下:
0 1 0 1 OR(或你正在执行的任何位运算)
0 1 1 0
然后,对每列位执行运算,并将结果写在下方。
在第一列中,0 或 0 均为 0,因此我们在横线下方写下 0。
0 1 0 1 OR
0 1 1 0
-------
0
第二列,1 或 1 结果为 1。第三列,0 或 1 结果为 1。第四列,1 或 0 结果为 1。
0 1 0 1 OR
0 1 1 0
-------
0 1 1 1
我们的结果是二进制数0111。
#include <bitset>
#include <iostream>
int main()
{
std::cout << (std::bitset<4>{ 0b0101 } | std::bitset<4>{ 0b0110 }) << '\n';
return 0;
}
这将输出:

我们同样可以对复合位运算或表达式进行处理,例如 0b0111 | 0b0011 | 0b0001。若某列中存在任意位为 1,则该列结果即为 1:
0 1 1 1 OR
0 0 1 1 OR
0 0 0 1
--------
0 1 1 1
以下是上述内容的代码:
#include <bitset>
#include <iostream>
int main()
{
std::cout << (std::bitset<4>{ 0b0111 } | std::bitset<4>{ 0b0011 } | std::bitset<4>{ 0b0001 }) << '\n';
return 0;
}
这将输出

位与运算
位与运算Bitwise AND(&)的工作原理与上述类似,但使用的是与逻辑而非或逻辑。具体而言,对于操作数中的每对位,若两位均为1,则位与运算将结果位设为真(1);否则设为假(0)。
考虑表达式 0b0101 & 0b0110。将各位对齐后,对每列位执行位与运算:
0 1 0 1 AND
0 1 1 0
--------
0 1 0 0
#include <bitset>
#include <iostream>
int main()
{
std::cout << (std::bitset<4>{ 0b0101 } & std::bitset<4>{ 0b0110 }) << '\n';
return 0;
}
这将输出

同样地,我们也可以对复合位与表达式进行相同操作,例如 0b0001 & 0b0011 & 0b0111。若某列所有位均为 1,则该列结果为 1。
0 0 0 1 AND
0 0 1 1 AND
0 1 1 1
--------
0 0 0 1
#include <bitset>
#include <iostream>
int main()
{
std::cout << (std::bitset<4>{ 0b0001 } & std::bitset<4>{ 0b0011 } & std::bitset<4>{ 0b0111 }) << '\n';
return 0;
}
这将输出

位运算异或
最后一个运算符是位运算异或bitwise XOR(^),也称为排他或exclusive or。
对于操作数中的每对位,当且仅当其中一位为1时,位异或运算将结果位设为真(1);否则设为假(0)。换言之,当配对位不同(一位为0而另一位为1)时,位异或运算将结果位设为真。
考虑表达式 0b0110 ^ 0b0011:
0 1 1 0 XOR
0 0 1 1
-------
0 1 0 1
同样可以求值复合异或表达式的列样式,例如 0b0001 ^ 0b0011 ^ 0b0111。若某列中1位数的数量为偶数,则结果为0;若某列中1位数的数量为奇数,则结果为1。
0 0 0 1 XOR
0 0 1 1 XOR
0 1 1 1
--------
0 1 0 1
位运算赋值运算符
与算术赋值运算符类似,C++ 提供了位运算赋值运算符。这些运算符确实会修改左操作数。
| Operator | Symbol | Form | The operation modifies the left operand where: |
|---|---|---|---|
| left shift | << | x <<= n | the bits in x are shifted left by n positions, new bits are 0. |
| right shift | >> | x >>= n | the bits in x are shifted right by n positions, new bits are 0. |
| bitwise AND | & | x &= y | each bit is set when both corresponding bits in x and y are 1. |
| bitwise OR | x | ||
| bitwise XOR | ^ | x ^= y | each bit is set when the corresponding bits in x and y are different. |
例如,你可以用 x >>= 1; 代替 x = x >> 1;。
#include <bitset>
#include <iostream>
int main()
{
std::bitset<4> bits { 0b0100 };
bits >>= 1;
std::cout << bits << '\n';
return 0;
}
这个程序将输出

顺带一提……
没有位运算取反赋值运算符。这是因为其他位运算符都是二元运算符,而位运算取反是单项运算符(那么~=运算符的右侧该放什么呢?)。若需翻转对象的所有位,可使用常规赋值语法:x = ~x;
位运算符对较小的整数类型执行整数提升
如果位运算符的操作数是小于 int 的整数类型,这些操作数将被提升(转换)为 int 或 unsigned int,返回的结果也将是 int 或 unsigned int。例如,若操作数为unsigned short类型,它们将被提升(转换)为unsigned int类型,运算结果也将以unsigned int类型返回。
多数情况下这不会造成影响。
相关内容
我们在第10.2节——浮点数与整数提升中介绍了整数提升。
然而,当对小于 int 或 unsigned int 的整数类型使用位运算符时,需注意两种情况:
- operator~ 运算符和 operator<< 运算符对宽度敏感,其结果可能因操作数的宽度而不同。
- 将结果初始化或赋值给更小整数类型的变量属于窄化转换(因将 int 或 unsigned int 转换为更小整数类型可能导致数据丢失)。列表初始化中禁止此操作,编译器对窄化赋值的警告行为可能因编译器而异。
以下程序演示了这些问题(假设 int 为 32 位):
#include <bitset>
#include <cstdint>
#include <iostream>
int main()
{
std::uint8_t c { 0b00001111 };
std::cout << std::bitset<32>(~c) << '\n'; // incorrect: prints 11111111111111111111111111110000
std::cout << std::bitset<32>(c << 6) << '\n'; // incorrect: prints 0000000000000000001111000000
std::uint8_t cneg { ~c }; // error: narrowing conversion from unsigned int to std::uint8_t
c = ~c; // possible warning: narrowing conversion from unsigned int to std::uint8_t
return 0;
}
这些问题可通过使用 static_cast 将位运算结果转换回更窄的整数类型来解决。以下程序可产生正确结果:
#include <bitset>
#include <cstdint>
#include <iostream>
int main()
{
std::uint8_t c { 0b00001111 };
std::cout << std::bitset<32>(static_cast<std::uint8_t>(~c)) << '\n'; // correct: prints 00000000000000000000000011110000
std::cout << std::bitset<32>(static_cast<std::uint8_t>(c << 6)) << '\n'; // correct: prints 0000000000000000000011000000
std::uint8_t cneg { static_cast<std::uint8_t>(~c) }; // compiles
c = static_cast<std::uint8_t>(~c); // no warning
return 0;
}
警告
位运算符会将较窄整数类型的操作数提升为 int 或 unsigned int。
~ 运算符和 << 运算符对宽度敏感,可能根据操作数的宽度产生不同结果。使用前请将此类位运算的结果通过 static_cast 转换回较窄整数类型,以确保结果正确。
最佳实践
尽可能避免对小于 int 的整数的类型进行位移操作。
总结
总结如何利用列法评求值运算:
求值位或运算bitwise OR时,若某列存在任意位为1,则该列结果为1。
求值位与运算bitwise AND时,若某列所有位均为1,则该列结果为1。
求值位异或运算bitwise XOR时,若某列1位数为奇数,则该列结果为1。
在下一课中,我们将探讨如何结合位掩码使用这些运算符来实现位操作。
测验时间
问题 #1
a) 0110 >> 2 在二进制中计算结果是什么?
显示答案
0110 >> 2 计算结果为 0001
b) 下列运算结果在二进制中是什么:0011 | 0101?
显示答案
0 0 1 1 OR
0 1 0 1
--------
0 1 1 1
c) 0011 & 0101 的二进制结果是什么?
显示答案
0 0 1 1 AND
0 1 0 1
--------
0 0 0 1
d) (0011 | 0101) & 1001 的二进制结果是什么?
显示答案
Inside the parenthesis:
0 0 1 1 OR
0 1 0 1
--------
0 1 1 1
Then:
0 1 1 1 AND
1 0 0 1
--------
0 0 0 1
问题 #2
位移运算类似于位移操作,但不同之处在于:移出的一端位数会补回另一端。例如 0b1001 << 1 结果为 0b0010,但左移 1 位则得到 0b0011。请实现一个对 std::bitset<4> 执行左移的函数。此处允许使用 test() 和 set() 方法。
以下代码应能执行:
#include <bitset>
#include <iostream>
// "rotl" stands for "rotate left"
std::bitset<4> rotl(std::bitset<4> bits)
{
// Your code here
}
int main()
{
std::bitset<4> bits1{ 0b0001 };
std::cout << rotl(bits1) << '\n';
std::bitset<4> bits2{ 0b1001 };
std::cout << rotl(bits2) << '\n';
return 0;
}
并打印以下内容:
0010
0011
显示解答
#include <bitset>
#include <iostream>
std::bitset<4> rotl(std::bitset<4> bits)
{
// keep track of whether the leftmost bit was a 1
const bool leftbit{ bits.test(3) };
bits <<= 1; // do left shift (which shifts the leftmost bit off the end)
// if the left bit was a 1
if (leftbit)
bits.set(0); // set the rightmost bit to a 1
return bits;
}
int main()
{
std::bitset<4> bits1{ 0b0001 };
std::cout << rotl(bits1) << '\n';
std::bitset<4> bits2{ 0b1001 };
std::cout << rotl(bits2) << '\n';
return 0;
}
我们将该函数命名为“rotl”而非“rotateLeft”,因为“rotl”是计算机科学领域公认的标准名称,同时也是标准库函数std::rotl的名称。

问题 #3
额外加分题:重新完成测验 #2,但不要使用test和set函数(使用位运算符)。
显示提示
提示:我们如何将最左边的位移到最右边的位置?
显示提示
提示:不妨尝试转换视角
显示解答
#include <bitset>
#include <iostream>
// h/t to reader Chris for this solution
std::bitset<4> rotl(std::bitset<4> bits)
{
// bits << 1 does the left shift
// bits >> 3 handle the rotation of the leftmost bit
return (bits<<1) | (bits>>3);
}
int main()
{
std::bitset<4> bits1{ 0b0001 };
std::cout << rotl(bits1) << '\n';
std::bitset<4> bits2{ 0b1001 };
std::cout << rotl(bits2) << '\n';
return 0;
}



浙公网安备 33010602011771号