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位有符號數為例,最高位系數取負,其余位系數取正