3.6 位运算符 (Lua 5.2+) Lua 5.2+ 位运算符详解与实践 位运算符概览 Lua 5.2+ 共引入了六个位运算符,它们分别是: 按位与 (AND): 按位或 (OR): 按位异或 (XOR): 按位非 (NOT): (一元运算符) 左移 (Left Shift): 右移 (Right Shift): 这些运算符都作用于整数类型(在 Lua 中,所有数字都是双精度浮点数,但位运算符会将其转换为整数进行操作)。 它们逐位执行操作,产生新的整数结果。 接下来,我们将逐个详细讲解这些运算符。 按位与 (AND) - 2.1 功能详解 按位与运算符 对两个整数的二进制表示进行逐位比较。只有当两个操作数对应位都为 1 时,结果的该位才为 1,否则为 0。
Lua 5.2+ 共引入了六个位运算符,它们分别是:
按位与 (AND): &
按位或 (OR): |
按位异或 (XOR): ~
按位非 (NOT): ~ (一元运算符)
左移 (Left Shift): <<
右移 (Right Shift): >>
这些运算符都作用于整数类型(在 Lua 中,所有数字都是双精度浮点数,但位运算符会将其转换为整数进行操作)。 它们逐位执行操作,产生新的整数结果。 接下来,我们将逐个详细讲解这些运算符。
&按位与运算符 & 对两个整数的二进制表示进行逐位比较。只有当两个操作数对应位都为 1 时,结果的该位才为 1,否则为 0。 我们可以用真值表来更清晰地表示:
| 位 A | 位 B | A & B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
-- 示例 1: 简单的按位与运算 local a = 5 -- 二进制: 0101 local b = 3 -- 二进制: 0011 local result_and = a & b print("5 & 3 =", result_and) -- 输出: 5 & 3 = 1 (二进制: 0001) -- 示例 2: 使用按位与进行掩码操作 (Masking) local flags = 13 -- 二进制: 1101 (假设每一位代表一个标志位) local mask = 4 -- 二进制: 0100 (掩码,用于检查第二个标志位) -- 检查第二个标志位是否被设置 (从右往左数) local is_flag2_set = (flags & mask) ~= 0 print("Flag 2 is set:", is_flag2_set) -- 输出: Flag 2 is set: false (因为 flags 的第二个标志位是 0) mask = 8 -- 二进制: 1000 (掩码,用于检查第四个标志位) is_flag4_set = (flags & mask) ~= 0 print("Flag 4 is set:", is_flag4_set) -- 输出: Flag 4 is set: true (因为 flags 的第四个标志位是 1) -- 示例 3: 清除特定位 (通过与 0 进行 AND 操作) local value = 15 -- 二进制: 1111 local clear_mask = 7 -- 二进制: 0111 (我们想要清除高两位) local cleared_value = value & clear_mask print("15 & 7 =", cleared_value) -- 输出: 15 & 7 = 7 (二进制: 0111,高两位被清除)
示例 1 展示了最基本的按位与运算。5 (0101) 与 3 (0011) 进行按位与操作,只有最后一位都是 1,所以结果是 1 (0001)。
示例 2 演示了按位与在掩码操作中的应用。掩码是一种常用的技术,用于检查或提取整数中特定位的信息。我们定义 flags 变量表示一组标志位,mask 变量用于选择我们想要检查的特定位。
通过 (flags & mask) 运算,我们只保留了 flags 和 mask 中都为 1 的位。如果结果不为 0,则说明 flags 中被 mask 选中的位至少有一个是 1,即对应的标志位被设置了。
我们分别使用掩码 4 (0100) 和 8 (1000) 来检查 flags 变量的第二个和第四个标志位。
示例 3 展示了如何使用按位与来清除特定位。 通过将一个值与一个掩码进行按位与运算,掩码中为 0 的位会强制将原始值对应位设置为 0,从而达到清除位的目的。 在这个例子中,clear_mask (0111) 的高两位为 0,所以与 value (1111) 进行按位与运算后,value 的高两位被清除,结果为 7 (0111)。
标志位管理: 使用按位与可以高效地检查一组标志位中特定标志是否被设置。
权限控制: 可以用位来表示不同的权限,使用按位与来检查用户是否拥有特定权限。
数据过滤: 在处理二进制数据时,可以使用掩码和按位与操作来提取或过滤出需要的数据部分。
|按位或运算符 | 对两个整数的二进制表示进行逐位比较。只要两个操作数对应位至少有一个为 1,结果的该位就为 1,只有当两个操作数对应位都为 0 时,结果的该位才为 0。 真值表如下:
| 位 A | 位 B | A | B |
|---|---|---|---|
| 0 | 0 | 0 | |
| 0 | 1 | 1 | |
| 1 | 0 | 1 | |
| 1 | 1 | 1 |
-- 示例 1: 简单的按位或运算 local a = 5 -- 二进制: 0101 local b = 3 -- 二进制: 0011 local result_or = a | b print("5 | 3 =", result_or) -- 输出: 5 | 3 = 7 (二进制: 0111) -- 示例 2: 使用按位或设置标志位 local flags = 1 -- 二进制: 0001 (初始状态,只有第一个标志位设置) -- 设置第二个和第三个标志位 local set_mask = 6 -- 二进制: 0110 (用于设置第二和第三位) flags = flags | set_mask print("Flags after setting bits:", flags) -- 输出: Flags after setting bits: 7 (二进制: 0111) -- 示例 3: 合并数据 local part1 = 240 -- 二进制: 11110000 (假设代表高 4 位数据) local part2 = 15 -- 二进制: 00001111 (假设代表低 4 位数据) -- 合并两个部分 (这里只是简单演示,实际应用中可能需要移位操作) local combined_data = part1 | part2 print("Combined data:", combined_data) -- 输出: Combined data: 255 (二进制: 11111111)
示例 1 展示了基本的按位或运算。 5 (0101) 与 3 (0011) 进行按位或操作,只要对应位有一个为 1,结果位就为 1,所以结果是 7 (0111)。
示例 2 展示了按位或在设置标志位中的应用。 我们初始化 flags 只设置了第一个标志位。 然后,我们使用 set_mask (0110) 与 flags 进行按位或运算。 set_mask 中为 1 的位会强制将 flags 中对应位设置为 1,而不会影响其他位。 结果是 flags 的第二和第三个标志位也被设置了。
示例 3 演示了按位或可以用于合并数据。 虽然这个例子非常简化,实际应用中合并数据通常需要配合移位操作,但它展示了按位或的基本思想:将多个数据的某些部分组合在一起。 在这个例子中,part1 的高 4 位和 part2 的低 4 位通过按位或运算被合并到 combined_data 中。
设置标志位: 使用按位或可以高效地设置(开启)一组标志位中的一个或多个标志。
组合权限: 可以用位来表示不同的权限,使用按位或来赋予用户多个权限。
数据合并: 在某些情况下,可以使用按位或将来自不同来源的数据片段组合成完整的数据。
~按位异或运算符 ~ 对两个整数的二进制表示进行逐位比较。只有当两个操作数对应位不同时,结果的该位才为 1,如果两个操作数对应位相同,结果的该位为 0。 真值表如下:
| 位 A | 位 B | A ~ B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
注意: Lua 5.2+ 中,按位异或运算符是 ~,而不是常见的 ^。 ^ 在 Lua 中是幂运算符。
-- 示例 1: 简单的按位异或运算 local a = 5 -- 二进制: 0101 local b = 3 -- 二进制: 0011 local result_xor = a ~ b print("5 ~ 3 =", result_xor) -- 输出: 5 ~ 3 = 6 (二进制: 0110) -- 示例 2: 使用按位异或进行位翻转 (Toggling) local flags = 5 -- 二进制: 0101 (假设每一位代表一个开关状态) local toggle_mask = 6 -- 二进制: 0110 (用于翻转第二和第三位) flags = flags ~ toggle_mask print("Flags after toggling bits:", flags) -- 输出: Flags after toggling bits: 3 (二进制: 0011) flags = flags ~ toggle_mask -- 再次异或相同的掩码,位会翻转回原来的状态 print("Flags after toggling again:", flags) -- 输出: Flags after toggling again: 5 (二进制: 0101) -- 示例 3: 交换两个变量的值 (无需临时变量,虽然在 Lua 中不常用,但展示 XOR 的特性) local x = 10 local y = 20 x = x ~ y y = x ~ y x = x ~ y print("x =", x, "y =", y) -- 输出: x = 20 y = 10
示例 1 展示了基本的按位异或运算。 5 (0101) 与 3 (0011) 进行按位异或操作,只有当对应位不同时,结果位才为 1,所以结果是 6 (0110)。
示例 2 演示了按位异或在位翻转 (Toggling) 中的应用。 将一个值与一个掩码进行按位异或运算,掩码中为 1 的位会翻转原始值对应位的状态(0 变为 1,1 变为 0),而掩码中为 0 的位则保持原始值对应位不变。
第一次异或 toggle_mask (0110) 后,flags 的第二和第三位被翻转。
第二次异或相同的 toggle_mask,第二和第三位又被翻转回来,恢复了原始状态。 这体现了 XOR 的一个重要特性:对同一个值连续异或两次相同的掩码,会恢复到原始值。
示例 3 展示了使用按位异或来交换两个变量的值,而无需引入临时变量。 这个技巧虽然在 Lua 中可能不常用(因为 Lua 可以直接使用多重赋值 x, y = y, x),但它巧妙地利用了 XOR 的特性,可以作为理解 XOR 运算的一个有趣例子。
位翻转 (Toggling): 使用按位异或可以快速翻转某些位的状态,例如开关状态的切换。
简单加密/解密: 由于 XOR 的可逆性(A ~ B ~ B = A),可以用于简单的加密和解密操作。 但请注意,这种加密方式非常基础,不适用于安全性要求高的场景。
检测数据差异: 两个值进行按位异或运算后,结果中为 1 的位表示两个值在该位上存在差异。
~ (一元运算符)按位非运算符 ~ 是一个一元运算符,它作用于一个整数。 它将该整数的二进制表示中的每一位取反,即 0 变为 1,1 变为 0。
注意: Lua 5.2+ 中,按位非运算符也是 ~,与按位异或运算符符号相同,但根据操作数的个数来区分是一元还是二元运算符。
-- 示例 1: 简单的按位非运算 local a = 5 -- 二进制: 0000...0101 (假设是 32 位整数) local result_not = ~a print("~5 =", result_not) -- 输出: ~5 = -6 -- 示例 2: 理解按位非的结果 (涉及到二进制补码) -- Lua 中的整数通常是带符号的,按位非会涉及到二进制补码表示。 -- 对于正数,按位非的结果是其负数的绝对值减 1。 -- 例如 ~5 的结果是 -6, 因为 5 的二进制取反后,在补码表示中就是 -6。 local b = -6 local result_not_b = ~b print("~-6 =", result_not_b) -- 输出: ~-6 = 5 -- ~-6 的结果是 5, 因为 -6 的二进制补码取反后,就是 5 的二进制表示。 -- 示例 3: 结合按位与进行特定位的清除 (另一种方式) local value = 15 -- 二进制: 1111 local mask = 8 -- 二进制: 1000 (想要清除第四位) local cleared_value = value & ~mask -- 先对 mask 取反,再进行按位与 print("15 & ~8 =", cleared_value) -- 输出: 15 & ~8 = 7 (二进制: 0111,第四位被清除)
示例 1 展示了基本的按位非运算。 5 (0101) 进行按位非操作,每一位都取反。 需要注意的是,Lua 中的整数是带符号的,通常使用二进制补码表示负数。 因此,按位非运算的结果可能看起来不太直观。
示例 2 解释了按位非运算的结果与二进制补码的关系。 对于正整数 x,~x 的结果通常是 -(x + 1)。 对于负整数 y,~y 的结果通常是 abs(y) - 1 (如果 y 是负数)。 理解这一点需要一些二进制补码的知识。 简单来说,按位非运算不仅是简单的位翻转,还涉及到符号位的变化,因此结果会涉及到正负数之间的转换。
示例 3 展示了按位非运算与其他位运算符的结合应用。 在这个例子中,我们想要清除 value 的第四位。 我们先对 mask (1000) 进行按位非运算,得到 ~mask (01111...11110111,假设是 32 位整数)。 然后将 value 与 ~mask 进行按位与运算。 由于 ~mask 的第四位为 0,其他位都为 1,所以按位与运算后,value 的第四位被强制设置为 0,而其他位保持不变,达到了清除特定位的目的。 这是一种常见的清除特定位的方法,与前面使用掩码直接进行按位与的方法相比,更加灵活,可以方便地清除多个不连续的位。
位掩码的反转: 使用按位非可以快速反转一个位掩码,例如将一个用于设置位的掩码转换为用于清除位的掩码。
与按位与/或/异或结合使用: 按位非通常不单独使用,而是与其他位运算符结合,实现更复杂的位操作,例如清除特定位、选择特定位等。
低级编程和硬件交互: 在低级编程和硬件交互中,按位非操作常用于处理寄存器和控制位。
<<左移运算符 << 将一个整数的二进制表示向左移动指定的位数。 左移过程中,右侧空出的位用 0 填充,左侧移出的位被丢弃。 左移 n 位相当于将原数乘以 2 的 n 次方 (在不溢出的情况下)。
-- 示例 1: 简单的左移运算 local a = 5 -- 二进制: 0101 local result_left_shift = a << 2 -- 左移 2 位 print("5 << 2 =", result_left_shift) -- 输出: 5 << 2 = 20 (二进制: 010100) -- 示例 2: 左移与乘法 local b = 3 print("3 << 3 =", b << 3) -- 输出: 3 << 3 = 24 (3 * 2^3 = 24) -- 示例 3: 使用左移创建掩码 local bit_position = 3 -- 想要创建掩码,设置从右往左数第 3 位为 1 local mask = 1 << bit_position -- 将 1 左移 bit_position 位 print("Mask for bit position 3:", mask) -- 输出: Mask for bit position 3: 8 (二进制: 1000)
示例 1 展示了基本的左移运算。 5 (0101) 左移 2 位,变成 20 (010100)。 相当于将 5 乘以 2 的 2 次方 (即 4)。
示例 2 验证了左移运算与乘法的关系。 左移 n 位确实等价于乘以 2 的 n 次方。 这在某些情况下可以作为一种效率更高的乘法运算方式(尤其是在低级编程中)。
示例 3 展示了使用左移运算来创建掩码。 我们想要创建一个掩码,使得从右往左数第 3 位为 1,其他位为 0。 我们先将整数 1 (二进制 0001) 左移 bit_position (3) 位,得到 8 (二进制 1000)。 这个 8 就是我们需要的掩码,它的第三位为 1,其他位为 0。 这种方法可以方便地创建指定位为 1 的掩码。
快速乘法: 在不溢出的情况下,左移运算可以作为一种快速计算 2 的幂次乘法的方式。
位字段操作 (Bit Fields): 左移运算常用于构建位字段,将不同的数据段移动到二进制表示中的特定位置。
创建掩码: 如示例 3 所示,左移运算是创建位掩码的常用方法。
>>右移运算符 >> 将一个整数的二进制表示向右移动指定的位数。 右移过程中,左侧空出的位用 0 填充(在 Lua 5.2+ 中,这是逻辑右移,也称为无符号右移),右侧移出的位被丢弃。 右移 n 位相当于将原数除以 2 的 n 次方,并向下取整 (对于正数和零)。
注意: Lua 5.2+ 的右移运算符 >> 是逻辑右移,即左侧填充 0。 一些其他语言(如 C++,Java)的右移运算符 >> 可能是算术右移,对于负数,左侧填充符号位 (1)。 Lua 统一使用逻辑右移。