为什么大厂工程师偏爱位运算符?解析&和|的高效编程之道

位运算符是计算机编程中用于直接操作二进制位的工具。它们通常用于底层编程、性能优化以及处理特定的数据格式。

1. 位运算符的基本概念

1.1 按位与 (&)
按位与运算符 & 对两个二进制数的每一位进行比较,如果两个对应的位都为 1,则结果位为 1,否则为 0。

示例:

a = 5  # 二进制: 0101
b = 3  # 二进制: 0011
result = a & b  # 二进制: 0001 (十进制: 1)
print(result)  # 输出: 1

1.2 按位或 (|)
按位或运算符 | 对两个二进制数的每一位进行比较,如果两个对应的位中至少有一个为 1,则结果位为 1,否则为 0。

示例:

a = 5  # 二进制: 0101
b = 3  # 二进制: 0011
result = a | b  # 二进制: 0111 (十进制: 7)
print(result)  # 输出: 7

2. 位运算符的实战应用

2.1 权限控制
在许多系统中,权限控制通常使用位掩码来表示不同的权限。通过位运算符 & 和 |,可以方便地检查和设置权限。

示例:

READ = 1  # 二进制: 0001
WRITE = 2  # 二进制: 0010
EXECUTE = 4  # 二进制: 0100

用户权限

user_permissions = READ | WRITE  # 二进制: 0011 (十进制: 3)

检查是否有读取权限

if user_permissions & READ:
    print("有读取权限")

添加执行权限

user_permissions |= EXECUTE  # 二进制: 0111 (十进制: 7)
print(user_permissions)  # 输出: 7

2.2 标志位处理
在底层编程中,标志位常用于表示某些状态或选项。通过位运算符 & 和 |,可以方便地设置和清除标志位。

示例:

FLAG_A = 1  # 二进制: 0001
FLAG_B = 2  # 二进制: 0010
FLAG_C = 4  # 二进制: 0100

设置标志位

flags = FLAG_A | FLAG_B  # 二进制: 0011 (十进制: 3)

检查是否设置了 FLAG_A

if flags & FLAG_A:
    print("FLAG_A 已设置")

清除 FLAG_B

flags &= ~FLAG_B  # 二进制: 0001 (十进制: 1)
print(flags)  # 输出: 1

2.3 数据压缩与解压缩
在某些情况下,位运算符可以用于数据的压缩和解压缩。例如,将多个布尔值压缩到一个字节中。

示例:

假设有四个布尔值

bool1 = True
bool2 = False
bool3 = True
bool4 = False

压缩到一个字节

compressed = (bool1 << 3) | (bool2 << 2) | (bool3 << 1) | bool4
print(compressed)  # 输出: 10 (二进制: 1010)

解压缩

bool1 = (compressed & 8) >> 3
bool2 = (compressed & 4) >> 2
bool3 = (compressed & 2) >> 1
bool4 = compressed & 1
print(bool1, bool2, bool3, bool4)  # 输出: True False True False

这里还用到了 << 左移,也回忆一下:

位左移运算符 << 将一个数的二进制表示向左移动指定的位数,右侧用 0 填充。左移一位相当于将数值乘以 2 的 1 次方。

压缩过程

bool1 = True  # 二进制: 1
bool2 = False  # 二进制: 0
bool3 = True  # 二进制: 1
bool4 = False  # 二进制: 0

#将每个布尔值移动到指定的位置

compressed = (bool1 << 3) | (bool2 << 2) | (bool3 << 1) | bool4

详细步骤:

bool1 << 3:将 bool1 向左移动 3 位,结果为 1000(二进制)。
bool2 << 2:将 bool2 向左移动 2 位,结果为 0000(二进制)。
bool3 << 1:将 bool3 向左移动 1 位,结果为 0010(二进制)。
bool4:保持原位,结果为 0000(二进制)。

将这些结果按位或 (|) 运算,得到最终的压缩值:

compressed = 1000 | 0000 | 0010 | 0000 = 1010 (二进制,十进制: 10)
print(compressed)  # 输出: 10

看下解压缩过程:

compressed & 8:提取第 4 位(从右数第 4 位,值为 8)。
1010 & 1000 = 1000(二进制)。

右移 3 位,得到 1(二进制),即 True。

compressed & 4:提取第 3 位(从右数第 3 位,值为 4)。
1010 & 0100 = 0000(二进制)。

右移 2 位,得到 0(二进制),即 False。

compressed & 2:提取第 2 位(从右数第 2 位,值为 2)。
1010 & 0010 = 0010(二进制)。

右移 1 位,得到 1(二进制),即 True。

compressed & 1:提取第 1 位(从右数第 1 位,值为 1)。
1010 & 0001 = 0000(二进制)。

得到 0(二进制),即 False。

为什么使用 &8、&4 等?

在解压缩过程中,&8、&4 等操作是为了提取压缩字节中的特定位。这些数值(8、4、2、1)是 2 的幂次方,分别对应二进制中的不同位:

8 对应二进制 1000,表示第 4 位。
4 对应二进制 0100,表示第 3 位。
2 对应二进制 0010,表示第 2 位。
1 对应二进制 0001,表示第 1 位。

通过按位与运算,会对两个数的每一位进行比较,如果两个对应的位都为 1,则结果的该位为 1,否则为 0,所以可以提取出这些位的值,然后通过右移操作将其移动到最低位,得到原始的布尔值。

3. 源码应用分析

从上面可以看到 按位与(&)和按位或(|)操作主要用于高效地设置和检测标志位。

这里我们再拿 Muduo 库源码来分析下对应按位与(&)和按位或(|)的应用。

按位或(|)操作
1.设置 socket 选项组合

// 在 Acceptor.cc 中创建 idleFd_
idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC))

意义:同时指定两个标志 - O_RDONLY(只读打开)和 O_CLOEXEC(exec 时关闭)

  1. 创建非阻塞 socket
// 在 SocketsOps.cc 中创建 socket
int sockfd = ::socket(family, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_TCP);

意义:一次性设置三个 socket 属性 - SOCK_STREAM(流式套接字)、SOCK_NONBLOCK(非阻塞)和 SOCK_CLOEXEC(exec 时自动关闭)

3.定义事件类型常量

// 在 Channel.cc 中定义事件常量
const int Channel::kReadEvent = POLLIN | POLLPRI;

意义:定义读事件为普通数据(POLLIN)或优先数据(POLLPRI)都可触发

4.添加事件监听

// 在 Channel 类中启用读事件
void Channel::enableReading() { events_ |= kReadEvent; update(); }
void Channel::enableWriting() { events_ |= kWriteEvent; update(); }

意义:在当前事件集中添加读/写事件,保留其他已设置的事件标志

按位与(&)操作
1.检查事件类型

// 在 EPollPoller 中检查事件
if (what & CURL_POLL_OUT)
{
  ch->enableWriting();
}

意义:检查 what 变量中是否设置了 CURL_POLL_OUT 标志(是否可写)

2.清除特定事件标志

// 在 Channel 类中禁用读事件
void Channel::disableReading() { events_ &= ~kReadEvent; update(); }
void Channel::disableWriting() { events_ &= ~kWriteEvent; update(); }

意义:从当前事件集中删除读/写事件,同时保留其他事件标志

~kReadEvent 创建一个除读事件外所有位都为 1 的掩码

events_ &= ~kReadEvent 清除读事件位,保留其他位

这里 ~ 符号在 C++ 中是按位取反(bitwise NOT)操作符,它会把操作数的每一个二进制位都取反:0 变为 1,1 变为 0。

基本原理

对于一个整数 x,~x 会产生一个新的值,其二进制表示是 x 的每一位取反。例如:

如果 x = 5(二进制 0000 0101)

则 ~x = -6(二进制 1111 1010,使用 2 的补码表示)

在事件处理中清除特定事件位

void Channel::disableReading() { events_ &= ~kReadEvent; update(); }
这里实际发生的操作是:

假设 kReadEvent 值为 3(二进制 0000 0011,表示 POLLIN | POLLPRI)

~kReadEvent 结果为 -4(二进制 1111 1100)

events_ &= ~kReadEvent 将 events_ 中对应 kReadEvent 的位置为 0,保留其他位不变

高效清除特定位:这是一种常见的位操作技巧,用于清除特定位而不影响其他位

3.状态检查

// 在 TcpConnection 中检查连接状态
bool connected() const { return state_ == kConnected; }
// 检查 Redis 连接
return channel_ && context_ && (context_->c.flags & REDIS_CONNECTED);

意义:检查标志位中是否设置了特定状态位

4. 总结

位运算符 & 和 | 是处理二进制数据的强大工具,广泛应用于权限控制、标志位处理、数据压缩等场景。

位运算符通常在以下场景中使用:

性能优化:位运算通常比算术运算更快,因此在需要高性能的场景中,位运算符可以用于替代某些算术运算。

底层编程:在处理硬件寄存器、网络协议或文件格式时,位运算符常用于直接操作二进制数据。

标志位和权限控制:位运算符非常适合用于表示和操作多个布尔标志或权限。

数据压缩:位运算符可以用于将多个布尔值或小整数压缩到一个更大的数据类型中,以节省存储空间。

你可能感兴趣的:(C++大厂高频面试题拆解,c++,位运算,C/C++面试)