1. 原码和真值
现在假设用32个bit来表示一个数字,为了区分正数和负数,将32位里的最高位设置为符号位,负数该位为1,正数该位为0,其余位表示数值的大小,这就是原码的概念,比如:
最高位1表示这个数是负的,其余位的绝对值是3,那么这个数在原码的解释下表示-3。
但是要注意的是,为了方便计算机进行运算,数字在内存里是用补码的形式的保存的,而非原码。以一段c++程序作为例子:
#include <iostream>
using namespace std;
int main() {
int x = 0b00000000000000000000000000000011;
int y = 0b10000000000000000000000000000011;
cout << x << endl; // 输出3
cout << y << endl; // 输出-2147483645
return 0;
}
这里引入真值的概念,即二进制比特串在计算机中真正数学意义上的值。
可以看到 \([00000000000000000000000000000011]_{32}\)的真值仍然表示正数3,而\([10000000000000000000000000000011]_{32}\)的真值是一个很小的负数,原因是计算机以补码的形式保存有符号数。
2. 有符号数的补码
有符号数的补码形式可以根据其原码推出来:
(1)正数的补码与原码一致,比如3的补码仍然是\([00000000000000000000000000000011]_{32}\),与原码一致,这就是为什么输入3的原码比特串,输出的真值任然是3;
(2)负数的补码为:保持符号位不变,其余为取反再加1。根据这个规则,我们可以通过-3的原码得到其补码形式,也就是计算机真正保存-3的方式。首先保持最高位符号位不变,其余位取反,得到\([11111111111111111111111111111100]_{32}\),然后再加1,得到补码\([11111111111111111111111111111101]_{32}\),我们测试一下这个补码
#include <iostream>
using namespace std;
int main() {
int z = 0b11111111111111111111111111111101;
cout << z << endl; // 输出-3
return 0;
}
这里加1就是正常的加法,需要正常进位,不用单独考虑符号位,比如-4的原码为\([10000000000000000000000000000100]_{32}\),保持符号位不变,其余位取反得到\([11111111111111111111111111111011]_{32}\),再加1得到-4的补码\([11111111111111111111111111111100]_{32}\)。
在C++里存在带符号数和无符号数,int定义的是有符号数,unsigned定义的是无符号数。需要注意的是不要将负数和无符号数相比较,因为无符号数不需要表示负数,所以最高位不被看作符号位,而是数值的一部分,当负数和无符号数比较时,负数类型从int强转为unsigned(意思就是同一个二进制串的解释方式由补码解释方式转为无符号数解释方式),而负数的最高位是1(符号位),所以负数与无符号数相比时,会被解释成一个非常大的无符号正数,这样可能会违背我们的本意。
3. 补码的移位运算
左移运算:将最左边的位舍弃,右边空出来的位补0,以8比特为例,将\([11011011]_{8}\)左移一位,得到\([10110110]_{8}\),最高位的1被舍弃,最低位补0(这里正数移位后变成了负数)
右移运算:右移运算分为逻辑右移和算术右移。逻辑右移将空出来的高位补0,算术右移的高位填充与符号有关,正数填充0,负数填充1
补码的移位运算有坑,在LeetCode里写C++,负数不能左移,要不然会报错,本地g++编译器可以正常左移;而有符号数的右移运算是逻辑右移还是算术右移取决于编译器,因此最好不要写有符号数的右移运算
4. 通过二进制串计算有符号数的真值
以下介绍一种计算补码真值的方式,这种方式不用区分符号位,正负数可以统一按照这种方式计算。
以4位有符号数为例,最高位系数取负,其余位系数取正