O.2 — 位运算符

位运算符

C++ 提供了 6 种位操作符,通常称为位运算符

运算符符号形式操作返回一个值,其中
左移<<x << nx 中的位向左移动 n 位,新位为 0
右移>>x >> nx 中的位向右移动 n 位,新位为 0
按位非~~xx 中的每个位都被翻转。
按位与&x & yxy 中相应的位都为 1 时,每个位都设置为 1
按位或|x | yxy 中相应的位有一个为 1 时,每个位都设置为 1
按位异或^x ^ yxy 中相应的位不同时,每个位都设置为 1

这些是非修改性运算符(它们不修改其操作数)。

作者注

在以下示例中,我们将主要使用 4 位二进制值。这是为了方便和简化示例。在实际程序中,使用的位数取决于对象的大小(例如,一个 2 字节的对象将存储 16 位)。

为了提高可读性,我们也可以在代码示例之外省略二进制字面量的 0b 前缀(例如,我们可能会选择使用 0101 而不是 0b0101)。

位运算符是为整数类型和 std::bitset 定义的。我们将在示例中使用 std::bitset,因为它更容易以二进制形式打印输出。

避免对有符号整数操作数使用位运算符,因为在 C++20 之前,许多运算符会返回实现定义的结果,或者存在其他潜在的陷阱,这些陷阱可以通过使用无符号操作数(或 std::bitset)轻松避免。

最佳实践

为避免意外,请对无符号整数操作数或 std::bitset 使用位运算符。

按位左移 (<<) 和按位右移 (>>) 运算符

按位左移 (<<) 运算符将位向左移动。左操作数是提供初始位序列的表达式,右操作数是指定要移动的位数位置的整数。例如,当我们写 x << 2 时,我们是说“生成一个值,其中 x 中的位已向左移动 2 个位置。”

左操作数未被修改,从右侧移入的新位为 0

以下是一些左移位序列 0011 的示例

0011 \<\< 1 is 0110
0011 \<\< 2 is 1100
0011 \<\< 3 is 1000

请注意,在第三种情况下,我们从数字的末尾移出了一位 1!从位序列末尾移出的位将永远丢失。

按位右移 (>>) 运算符的工作方式类似,但将位向右移动。

以下是一些右移位序列 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;
}

这会打印

1100
0110
1000

致进阶读者

C++ 中的位移是与字节序无关的。左移总是朝向最高有效位,右移总是朝向最低有效位。

什么!?运算符 << 和运算符 >> 不是用于输入和输出吗?

确实如此。

今天的程序通常不会大量使用位左移和右移运算符来移位。相反,位左移运算符更常与 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;
}

这个程序打印

1000

在上面的程序中,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;
}

这会打印

01101
1100

第一行打印 x 的值 (0110),然后是字面量 1。第二行打印 x 左移 1 的值 (1100)。

按位非

按位非运算符 (~) 在概念上很简单:它只是将每个位从 0 翻转为 1,反之亦然。

~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;
}

这会打印

Initial values:
Bits: 0100 00000100
Values: 4 4

After bitwise NOT:
Bits: 1011 11111011
Values: 11 251

最初,b4b8 都设置为 0b100。当用前导零填充时,b4 最终变为 0100b8 变为 00000100,这将在下一行打印。

然后我们使用 to_ulong() 成员函数将位的​​值解释为 long 整数。您可以看到 b4b8 都打印值 4。尽管位数不同,它们都表示相同的值。这是因为前导零位对解释的整数没有贡献。

然后我们使用按位非来翻转每个位的​​位,所以 b4 现在有位 1011b8 现在有位 1111 1011。当作为整数打印时,这会打印值 11251。如您所见,这些值不再相同。这是因为前导一确实对解释的整数有贡献,并且 b8b4 有更多的前导一。

按位或

按位或 (|) 的工作方式与逻辑或类似。如果您还记得,如果两个操作数中任一为 true,逻辑或将计算结果为 true (1),否则计算结果为 false (0)。

但是,逻辑或应用于整个操作数(以产生单个真或假结果),而按位或应用于操作数中的每一对位(为每个位产生单个真或假结果)。

让我们用一个例子来说明这一点。考虑表达式 0b0101 | 0b0110

提示

要手动执行任何二进制位运算,最简单的方法是将两个操作数像这样对齐

0 1 0 1 OR (or whatever bitwise operation you are doing)
0 1 1 0

然后,将操作应用于每个位,并将结果写在下面。

在第一列中,000,所以我们在线下面放一个 0。

0 1 0 1 OR
0 1 1 0
-------
0

第二列,111。第三列 011。第四列,101

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;
}

这会打印

0111

我们可以对复合按位或表达式做同样的事情,例如 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;
}

这会打印

0111

按位与

按位与 (&) 的工作方式与上面类似,只是它使用 AND 逻辑而不是 OR 逻辑。也就是说,对于操作数中的每一对位,按位与将结果位设置为 true (1),如果两个配对位都为 1,否则设置为 false (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;
}

这会打印

0100

同样,我们可以对复合按位与表达式做同样的事情,例如 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;
}

这会打印

0001

按位异或

最后一个运算符是按位异或 (^),也称为异或

对于操作数中的每一对位,按位异或在配对位中恰好有一个为 1 时将结果位设置为 true (1),否则设置为 false (0)。换句话说,当配对位不同(一个为 0 另一个为 1)时,按位异或将结果位设置为 true

考虑表达式 0b0110 ^ 0b0011

0 1 1 0 XOR
0 0 1 1
-------
0 1 0 1

也可以按列样式评估复合 XOR 表达式,例如 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++ 提供了按位赋值运算符。这些运算符会修改左操作数。

运算符符号形式该操作修改左操作数,其中
左移<<x <<= nx 中的位向左移动 n 位,新位为 0
右移>>x >>= nx 中的位向右移动 n 位,新位为 0
按位与&x &= yxy 中相应的位都为 1 时,每个位都设置为 1
按位或|x |= yxy 中相应的位有一个为 1 时,每个位都设置为 1
按位异或^x ^= yxy 中相应的位不同时,每个位都设置为 1

例如,您可以写 x >>= 1; 而不是写 x = x >> 1;

#include <bitset>
#include <iostream>

int main()
{
    std::bitset<4> bits { 0b0100 };
    bits >>= 1;
    std::cout << bits << '\n';

    return 0;
}

这个程序打印

0010

题外话…

没有按位非赋值运算符。这是因为其他位运算符是二元运算符,而按位非是一元运算符(那么 ~= 运算符的右侧会是什么?)。如果要翻转对象的所有位,可以使用普通赋值:x = ~x;

位运算符对较小的整数类型执行整数提升 高级

如果位运算符的操作数是小于 int 的整数类型,则这些操作数将被提升(转换)为 intunsigned int,并且返回的结果也将是 intunsigned int。例如,如果我们的操作数是 unsigned short,它们将被提升(转换)为 unsigned int,并且操作的结果将作为 unsigned int 返回。

在许多情况下,这无关紧要。

相关内容

我们在课程 10.2 -- 浮点和整数提升 中介绍了整数提升。

但是,当对窄于 intunsigned int 的整数类型使用位运算符时,有两种情况需要注意

  • operator~operator<< 对宽度敏感,并且可能根据操作数的宽度产生不同的结果。
  • 将结果初始化或赋值给较小整数类型的变量是一种窄化转换(因为将 intunsigned int 转换为较小的整数类型可能导致数据丢失)。这在列表初始化中是不允许的,并且您的编译器可能会或可能不会抱怨窄化赋值。

以下程序展示了这些问题(假设是 32 位 int)

#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;
}

警告

位运算符会将具有较窄整数类型的操作数提升为 intunsigned int

operator~operator<< 对宽度敏感,并且可能根据操作数的宽度产生不同的结果。在使用此类位运算的结果之前,请将其 static_cast 回较窄的整数类型,以确保结果正确。

最佳实践

尽可能避免对小于 int 的整数类型进行位移。

总结

总结如何利用列方法评估位运算

当评估按位或时,如果一列中的任何位为 1,则该列的结果为 1。
当评估按位与时,如果一列中的所有位都为 1,则该列的结果为 1。
当评估按位异或时,如果一列中有奇数个 1 位,则该列的结果为 1。

在下一课中,我们将探讨如何将这些运算符与位掩码结合使用以促进位操作。

小测验时间

问题 #1

a) 0110 >> 2 在二进制中计算结果是什么?

显示答案

b) 以下在二进制中计算结果是什么:0011 | 0101?

显示答案

c) 以下在二进制中计算结果是什么:0011 & 0101?

显示答案

d) 以下在二进制中计算结果是什么 (0011 | 0101) & 1001?

显示答案

问题 #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

显示答案

问题 #3

额外加分:重做测验 #2,但不要使用 test 和 set 函数(使用位运算符)。

显示提示

显示提示

显示答案

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
519 评论
最新
最早 最多投票
内联反馈
查看所有评论