C++---有符号和无符号整数的位移操作
在C++中,位移操作(左移<<
和右移>>
)是对整数二进制位的直接操作,但其行为在有符号整数(signed) 和无符号整数(unsigned) 中存在显著差异。这种差异源于计算机对整数的存储方式(补码)和语言标准对操作的规定,理解这些差异是编写正确位运算代码的关键。
一、位移操作的基本概念
位移操作的本质是将整数的二进制位向指定方向(左或右)移动指定的位数,空出的位由特定规则填充。在C++中,位移操作符的语法为:
- 左移:
a << n
表示将a
的二进制位向左移动n
位,右侧空出的位补0
; - 右移:
a >> n
表示将a
的二进制位向右移动n
位,左侧空出的位填充规则因整数是否有符号而不同。
需要注意的是:
- 位移的位数
n
必须是非负整数,且不能大于等于操作数的位数(如32位整数位移32位及以上属于未定义行为); - 位移操作的结果类型与左操作数的类型一致(如
int
位移后仍为int
)。
二、无符号整数(unsigned)的位移:逻辑移位
无符号整数的位移是逻辑移位(Logical Shift),即不考虑符号位,仅根据“空位补0”的规则处理,行为在C++标准中是完全明确的。
1. 无符号左移(unsigned << n
)
左移时,二进制位整体向左移动n
位,右侧空出的n
位全部补0
,左侧超出类型位数的高位被直接丢弃。
举例:假设unsigned int
为32位,分析unsigned int a = 0x0000000F
(二进制00000000 00000000 00000000 00001111
)的左移:
a << 1
:左移1位后,右侧补0,结果为0x0000001E
(二进制00000000 00000000 00000000 00011110
);a << 4
:左移4位后,右侧补4个0,结果为0x000000F0
(二进制00000000 00000000 00000000 11110000
);a << 28
:左移28位后,高位超出32位的部分被丢弃,结果为0xF0000000
(二进制11110000 00000000 00000000 00000000
)。
规律:无符号左移n
位等价于“a * 2^n
”(若结果未超出类型范围)。
2. 无符号右移(unsigned >> n
)
右移时,二进制位整体向右移动n
位,左侧空出的n
位全部补0
,右侧超出的n
位被直接丢弃。
举例:仍以32位unsigned int a = 0xF0000000
(二进制11110000 00000000 00000000 00000000
)为例:
a >> 1
:右移1位后,左侧补0,结果为0x78000000
(二进制01111000 00000000 00000000 00000000
);a >> 4
:右移4位后,左侧补4个0,结果为0x0F000000
(二进制00001111 00000000 00000000 00000000
);a >> 28
:右移28位后,左侧补28个0,结果为0x0000000F
(二进制00000000 00000000 00000000 00001111
)。
规律:无符号右移n
位等价于“a / 2^n
”(向下取整)。
三、有符号整数(signed)的位移:算术移位为主
有符号整数(如int
、long long
)在内存中以补码形式存储(最高位为符号位:0表示正数,1表示负数)。其位移行为与无符号不同,尤其是右移,C++标准将其定义为“实现定义”(implementation-defined),但主流编译器(如GCC、Clang、MSVC)均采用算术移位(Arithmetic Shift)规则。
1. 有符号左移(signed << n
)
有符号左移的行为与无符号左移基本一致:二进制位向左移动n
位,右侧空出的n
位补0
,左侧超出类型位数的高位(包括符号位)被丢弃。
注意:若左移后符号位发生变化(如正数左移后符号位变为1),结果可能超出该类型能表示的范围,此时属于未定义行为(undefined behavior)。
举例:32位int
(范围-2^31 ~ 2^31-1
):
- 正数左移:
int a = 0x0000000F
(15),a << 1
结果为0x0000001E
(30),符号位仍为0(合法); - 负数左移:
int b = -0x0000000F
(-15,补码0xFFFFFFF1
),b << 1
结果为0xFFFFFFE2
(-30),符号位仍为1(合法); - 未定义行为:
int c = 0x40000000
(1073741824),c << 1
结果为0x80000000
(-2147483648),符号位从0变为1,属于未定义行为(不同编译器可能有差异)。
2. 有符号右移(signed >> n
)
有符号右移是最容易产生差异的操作。主流编译器采用算术移位:右侧超出的n
位被丢弃,左侧空出的n
位补符号位(正数补0,负数补1)。
这种规则的目的是保持位移后数值的“符号一致性”,尤其对负数而言,右移后仍为负数。
举例1:正数右移
int a = 0x0000000F
(15,二进制00000000 00000000 00000000 00001111
):
- 符号位为0,右移时左侧补0;
a >> 1
:结果为0x00000007
(7,二进制00000000 00000000 00000000 00000111
);a >> 4
:结果为0x00000000
(0,二进制00000000 00000000 00000000 00000000
)。
举例2:负数右移
int b = -0x0000000F
(-15,补码0xFFFFFFF1
,二进制11111111 11111111 11111111 11110001
):
- 符号位为1,右移时左侧补1;
b >> 1
:右移1位后,左侧补1,结果为0xFFFFFFF8
(-8,二进制11111111 11111111 11111111 11111000
);b >> 4
:右移4位后,左侧补4个1,结果为0xFFFFFFF0
(-16,二进制11111111 11111111 11111111 11110000
)。
规律:有符号右移n
位对正数等价于“a / 2^n
”(向零舍入);对负数等价于“a / 2^n
”(向下取整,如-15 >> 1 = -8
,而-15 / 2 = -7
)。
四、有符号与无符号位移的核心差异对比
操作类型 | 无符号整数(unsigned) | 有符号整数(signed) |
---|---|---|
左移(<<) | 逻辑移位,右侧补0,高位丢弃 | 逻辑移位(同无符号),但可能触发未定义行为 |
右移(>>) | =逻辑移位,左侧补0,低位丢弃 | 算术移位(主流编译器),左侧补符号位 |
符号影响 | 无符号位,位移后仍为非负 | 符号位不变(算术移位),负数位移后仍为负 |
应用场景 | 位运算(如哈希、编码) | 带符号的数值计算(如除法近似) |
五、实际开发中的注意事项
-
避免对有符号整数进行右移依赖
由于有符号右移是“实现定义”,若代码需要跨编译器兼容,应避免依赖其行为。如需逻辑右移,可先转换为无符号类型(如(unsigned int)a >> n
)。 -
负数转换为无符号的妙用
如之前的toHex
函数中,负数转换为unsigned int
后,右移变为逻辑移位(高位补0),可正常处理补码的所有位,避免无限循环(若用有符号右移,负数会因补1而永远不为0)。 -
警惕位移后的未定义行为
- 位移位数为负或大于等于类型位数(如32位
int
移32位); - 有符号左移后符号位改变(超出表示范围)。
- 位移位数为负或大于等于类型位数(如32位