位运算 (Bitwise Operator, 按位操作符, 位运算符) (Binary System, 二进制)

创建时间:
2014-06-10 23:07
最近更新:
2018-08-12 23:40

Brief

  • 位运算原理: 直接对整数在内存中的二进制位进行操作。
  • 低位、高位: 二进制数 1111 0000 最高位是 1、最低位是 0,与十进制同理。

ECMAScript 有 3 种 Boolean 运算符:NOT、AND 和 OR

在 ECMAScript 中,逻辑 NOT 运算符用感叹号 ! 表示,与 C 和 Java 中的逻辑 NOT 运算符相同。
在 ECMAScript 中,逻辑 AND 运算符用双和号 && 表示。
在 ECMAScript 中,逻辑 OR 运算符用双竖线 || 表示,与 Java 中的相同。

NOT / ~ / 非

C# 中

~ 运算符对操作数执行 按位求补 运算,其效果相当于反转每一位。
按位求补运算符 是为 int、uint、long 和 ulong 类型预定义的。

TonyRemark:
C# 中与位运算符~ 类似的是逻辑运算符 !:逻辑非运算符 ! 是对操作数求反的一元运算符。为 bool 定义了该运算符,当且仅当操作数为 false 时才返回 true。

JavaScript 中

位运算 NOT 由否定号 ~ 表示,它是 ECMAScript 中为数不多的与二进制算术有关的运算符之一。
位运算 NOT 是三步的处理过程:

  1. 把运算数转换成 32 位二进制数;
  2. 把二进制数转换成它的二进制反码;
  3. 把二进制数转换成浮点数。

例如:

var iNum1 = 25; //25 等于 00000000000000000000000000011001
var iNum2 = ~iNum1; //转换为 11111111111111111111111111100110
alert(iNum2); //输出 -26

位运算 NOT 的结果等效于对数字求负,然后减 1,因此 25 变 -26。
用下面的方法也可以得到同样的方法:

var iNum1 = 25;
var iNum2 = -iNum1 -1;
alert(iNum2); //输出 -26

AND / & / 与

C# 中

& 运算符既可作为一元运算符也可作为二元运算符。
一元 & 运算符返回操作数的地址 (要求 unsafe 上下文)。
为整型和 bool 类型预定义了二进制 & 运算符。
对于整型,& 计算操作数的 按位与 (bitwise and)。
对于 bool 操作数,& 计算操作数的 逻辑与 (logical and);也就是说,当且仅当两个操作数均为 true 时,结果才为 true。
& 运算符计算两个运算符,与第一个操作数的值无关。(Tony 认为这一句要表达的意思是:相对于 && 来说,&不是短路运算)

条件 "与" 运算符 && 执行其 bool 操作数的 逻辑与 运算,但仅在必要时才计算第二个操作数。
操作 x && y 对应于操作 x & y 不同的是,如果 x 为 false,则不计算 y (因为不论 y 为何值,"与" 操作的结果都为 false)。这被称作为 "短路" 计算。

&= 与赋值运算符:
表达式 x &= y 按如下规则计算:x = x & y。不同的是 x 只计算一次。& 运算符对整数操作数执行 按位逻辑与 运算,对 bool 操作数执行 逻辑与 运算。

JavaScript 中

位运算 AND 由和号 & 表示,直接对数字的二进制形式进行运算。
它把每个数字中的数位对齐,然后用下面的规则对同一位置上的两个数位进行 AND 运算:
1 & 1 结果为 1
1 & 0 结果为 0
0 & 1 结果为 0
0 & 0 结果为 0

例如,要对数字 25 和 3 进行 AND 运算,代码如下所示:

var iResult = 25 & 3;
alert(iResult); //输出 1

25 和 3 进行 AND 运算的结果是 1。为什么?分析如下:

25	=	0000 0000 0000 0000 0000 0000 0001 1001
3	=	0000 0000 0000 0000 0000 0000 0000 0011
		---------------------------------------
AND	=	0000 0000 0000 0000 0000 0000 0000 0001

可以看出,在 25 和 3 中,只有一个数位 (位 0) 存放的都是 1,因此,其他数位生成的都是 0,所以结果为 1。

OR / | / 或

C# 中

二元 | 运算符是为整型和 bool 类型预定义的。
对于整型,| 计算操作数的 按位或 (bitwise or) 结果。
对于 bool 操作数,| 计算操作数的 逻辑或 (logical or) 结果;也就是说,当且仅当两个操作数均为 false 时,结果才为 false。

条件 "或" 运算符 || 执行 bool 操作数的逻辑 "或" 运算,但仅在必要时才计算第二个操作数。
操作 x || y 对应于操作 x | y 不同的是,如果 x 为 true,则不计算 y (因为不论 y 为何值,"或" 操作的结果都为 true)。这被称作为 "短路" 计算。

|= 或赋值运算符:
表达式 x |= y 按如下规则计算:x = x | y。不同的是 x 只计算一次。| 运算符对整型操作数执行 按位逻辑或 运算,对布尔操作数执行 逻辑或 运算。

JavaScript 中

位运算 OR 由符号 | 表示,也是直接对数字的二进制形式进行运算。
在计算每位时,OR 运算符采用下列规则:
1 | 1 结果为 1
1 | 0 结果为 1
0 | 1 结果为 1
0 | 0 结果为 0

仍然使用 AND 运算符所用的例子,对 25 和 3 进行 OR 运算,代码如下:

var iResult = 25 | 3;
alert(iResult); //输出 27

25 和 3 进行 OR 运算的结果是 27:

25	=	0000 0000 0000 0000 0000 0000 0001 1001
3	=	0000 0000 0000 0000 0000 0000 0000 0011
		---------------------------------------
OR	=	0000 0000 0000 0000 0000 0000 0001 1011

可以看出,在两个数字中,共有 4 个数位存放的是 1,这些数位被传递给结果。二进制代码 11011 等于 27。

XOR / ^ / 异或

行为:将 "异或" 改名为 "异" 更能精确表达其行为,才达到 "非与或" 一致的水平。如左右操作数的某 bit 位不同,则结果中该 bit 位为 1,否则为 0。相当于不进位加法:1+0=1、1+1=0、0+0=0。

C# 中

二元运算符 ^ 是为整型和 bool 类型预定义的。
对于整型,^ 将计算操作数的 按位异或 (bitwise exclusive or)。
对于 bool 操作数,^ 将计算操作数的 逻辑异或 (logical exclusive or);也就是说,当且仅当只有一个操作数为 true 时,结果才为 true。

^= 异或赋值运算符:
表达式 x ^= y 按如下规则计算:x = x ^ y。不同的是 x 只计算一次。^ 运算符对整数操作数执行 按位异或 运算,对 bool 操作数执行 逻辑异或 运算。

JavaScript 中

位运算 XOR 由符号 ^ 表示,当然,也是直接对二进制形式进行运算。
XOR 不同于 OR,当只有一个数位存放的是 1 时,它才返回 1。真值表如下:
1 ^ 1 结果为 0
1 ^ 0 结果为 1
0 ^ 1 结果为 1
0 ^ 0 结果为 0

对 25 和 3 进行 XOR 运算,代码如下:

var iResult = 25 ^ 3;
alert(iResult); //输出 26

25 和 3 进行 XOR 运算的结果是 26:

25	=	0000 0000 0000 0000 0000 0000 0001 1001
3	=	0000 0000 0000 0000 0000 0000 0000 0011
		---------------------------------------
XOR	=	0000 0000 0000 0000 0000 0000 0001 1010

可以看出,在两个数字中,共有 4 个数位存放的是 1,这些数位被传递给结果。二进制代码 11010 等于 26。

按位异或的 3 个特点

0^0 结果为 0;0^1 结果为 1。0 异或任何数 结果为 任何数。
1^0 结果为 1;1^1 结果为 0。1 异或任何数 结果为 任何数取反。
0^0 结果为 0;1^1 结果为 0。任何数异或自己 结果为 把自己置 0。

<< / Left-Shift / SHL

C# 中

左移运算符 << 将第一个操作数向左移动第二个操作数指定的位数。第二个操作数的类型必须是 int。
如果第一个操作数是 int 或 uint (32 位数),则移位数由第二个操作数的低 5 位给出 (5 位 2 进制数最多为 11111,等于 10 进制数 31,Tony 理解为限制最多可左移 31 位,底层可能是将第二个操作数 & 0x1f,相当于 % 32)。
如果第一个操作数是 long 或 ulong (64 位数),则移位数由第二个操作数的低 6 位给出 (6 位 2 进制数最多为 111111,等于 10 进制数 63,Tony 理解为限制最多可左移 63 位,底层可能是将第二个操作数 & 0x3f,相当于 % 64)。
第一个操作数的高序位被放弃,低序空位用 0 填充。移位操作从不导致溢出。
请注意,i<<1i<<33 给出相同的结果,因为 1 和 33 的低序 5 个位相同。

Note: 以上内容在 TlTest.BitAndByte_Test.右移运算符() 已通过验证,总结: 1. 左移总是丢弃最高位 (即使它是符号位)、右侧补 0,无论 正数、负数、int、uint、long、ulong。2. 右移的右操作数 底层有模 32/64,大于该阈值时开始循环。

<<= 左移赋值运算符:
表达式 x <<= y 按如下规则计算:x = x << y。不同的是 x 只计算一次。<< 运算符将 x 向左移动 y 指定的位数。

JavaScript 中

左移运算由两个小于号表示 <<。它把数字中的所有数位向左移动指定的数量。
例如,把数字 2 (等于二进制中的 10) 左移 5 位,结果为 64 (等于二进制中的 1000000):

var iOld = 2; //等于二进制 10
var iNew = iOld << 5; //等于二进制 1000000、十进制 64

注意:
在左移数位时,数字右边多出 5 个空位。
左移运算用 0 填充这些空位,使结果成为完整的 32 位数字。
注意:
左移运算保留数字的符号位。
例如,如果把 -2 左移 5 位,得到的是 -64,而不是 64。
符号仍然存储在第 32 位中吗? 是的,不过这在 ECMAScript 后台进行,开发者不能直接访问第 32 个数位。
即使输出二进制字符串形式的负数,显示的也是负号形式 (例如 Number(-15).toString(2) 将输出 -1111)。

>> / Right-Shift / SHR

C# 中

右移运算符 >> 将第一个操作数向右移动第二个操作数所指定的位数。
如果第一个操作数为 int 或 uint (32 位数),则移位数由第二个操作数的低五位给出 (5 位 2 进制数最多为 11111,等于 10 进制数 31,Tony 理解为限制最多可左移 31 位,底层可能是将第二个操作数 & 0x1f,相当于 % 32)。
如果第一个操作数为 long 或 ulong (64 位数),则移位数由第二个操作数的低六位给出 (6 位 2 进制数最多为 111111,等于 10 进制数 63,Tony 理解为限制最多可左移 63 位,底层可能是将第二个操作数 & 0x3f,相当于 % 64)。
如果第一个操作数为 int 或 long,则右移位是 "算术移位" (高序空位设置为符号位) (即正数补 0、负数补 1)。
如果第一个操作数为 uint 或 ulong 类型,则右移位是 "逻辑移位" (高位填充 0)。

Note: 以上内容在 TlTest.BitAndByte_Test.右移运算符() 已通过验证,总结: 1. 右移操作在左侧产生的空位用符号位填充 (即正数填充 0、负数填充 1)。2. 右移的右操作数 底层有模 32/64,大于该阈值时开始循环。

>>= 右移赋值运算符:
表达式 x >>= y 按如下规则计算:x = x>>y。不同的是 x 只计算一次。>> 运算符根据 y 指定的量对 x 进行右移位。

JavaScript 中

有符号右移运算符由两个大于号表示 >>
它把 32 位数字中的所有数位整体右移,同时保留该数的符号 (正号或负号)。
有符号右移运算符恰好与左移运算相反。
例如,把 64 右移 5 位,将变为 2:

var iOld = 64; //等于二进制 1000000
var iNew = iOld >> 5; //等于二进制 10 十进制 2

同样,移动数位后会造成空位。
这次,空位位于数字的左侧,但位于符号位之后。
ECMAScript 用符号位的值填充这些空位,创建完整的数字 (即正数补 0、负数补 1)。
Tony 理解为:此操作不改变符号位。

无符号右移运算符由三个大于号 >>> 表示,它将无符号 32 位数的所有数位整体右移。
对于正数,无符号右移运算的结果与有符号右移运算一样。
用有符号右移运算中的例子,把 64 右移 5 位,将变为 2:

var iOld = 64; //等于二进制 1000000
var iNew = iOld >>> 5; //等于二进制 10 十进制 2

对于负数,情况就不同了。
无符号右移运算用 0 填充所有空位。
对于正数,这与有符号右移运算的操作一样,而负数则被作为正数来处理。

由于无符号右移运算的结果是一个 32 位的正数,所以负数的无符号右移运算得到的总是一个非常大的数字。
例如,如果把 -64 右移 5 位,将得到 134217726。

-64>>>5 == parseInt('00000111111111111111111111111110', 2) //true

如何得到这种结果的呢?
要实现这一点,需要把这个数字转换成无符号的等价形式 (尽管该数字本身还是有符号的),可以通过以下代码获得这种形式:
var iUnsigned64 = -64 >>> 0;
然后,用 Number 类型的 toString () 获取它的真正的位表示,采用的基为 2:
alert(iUnsigned64.toString(2));
这将生成 11111111111111111111111111000000,即有符号整数 -64 的二进制补码表示,不过它等于无符号整数 4294967232。

出于这种原因,使用无符号右移运算符要小心。

Tony 总结位移

SHL 总是保留符号、放弃高位、用 0 填充低位,永不溢出。
SHR 总是保留符号、放弃低位、用符号位的值填充高位 (即正数补 0、负数补 1),永不溢出。

特殊:JavaScript 比 C# 多了 >>>

注意:
任何数连续补 0,结果总是 0;
任何数连续补 1,结果总是-1。

Resource - Authoritative

  1. ECMAScript 位运算符
  2. ECMAScript Boolean 运算符
  3. JavaScript 运算符

Resource

  1. 位运算讲解系列文章 (目录)
  1. VB6 位运算
  2. 在 VB 里怎么实现移位的算术运算操作 - VB 没有移位运算符,可以用 *2/2 来代替: 左移n位 = num * 2^n; 右移n位 = num \ 2^n