C語言中補碼的整數運算特性
前言
本篇部落格以“SSD6-Exercise2-Data Lab: Manipulating Bits”為例,分析在對C語言中的整數採用補碼(two’s-complement)編碼的機器上,其整數運算的特性。
補碼
定義
最常見的有符號數的計算機表示方式就是補碼(two’s-complement)形式。在這個定義中,將字的最高有效位解釋為負權(negative weight),我們用函式B2T(Binary to Two’s-complement的縮寫,長度為 w)來表示。
如:
B2T ([0001]) = -0 + 0 + 0 + 1 = 1
B2T ([0101]) = -0 + 4 + 0 + 1 = 5
B2T ([1011]) = -8 + 0 + 2 + 1 = -5
B2T ([1111]) = -8 + 4 + 2 + 1 = -1
定理1
B2T ([11···1]) = -1
證明:假設B2T ([11···1]) 共有w位,則其值為 -2^(w-1) + 2^(w-2) + ··· + 2^0
. 根據等比數列求和公式,易證該值為-1.
定理2
對於
w
位的補碼B2T來說,其邊界值Tmax與Tmin分別為:Tmax = B2T ([01···1]) = 2^(w-1) - 1
Tmin = B2T ([10···0]) = -2^(w-1)
即有:~Tmax = Tmin
整數運算
我們先以表格的形式,巨集觀介紹C語言中的位級運算、邏輯運算和移位運算。
運算種類 | 運算子 | 主要說明 |
---|---|---|
位級運算 | |, &, ~, ^ | 對應於布林運算中的OR, AND, NOT, EXCLUSIVE-OR |
邏輯運算 | ||, &&, ! | 對應於命題邏輯中的OR, AND, NOT |
移位運算 | <<, >> | 分為左移與右移,右移運算包括邏輯右移與算數右移 |
!
與~
有什麼區別?
注意:邏輯運算很容易和位級運算相混淆,但是它們的功能是完全不同的。
邏輯運算中認為所有非零的引數都表示
TRUE
,而引數0表示FALSE
.邏輯運算的結果為一個布林值,而位級運算的結果依然為一個數
邏輯運算的運算子常稱為
與
、或
、非
,而位級運算的運算子常稱為與
、或
、取反
、異或
.
因此,!
是邏輯運算中的非
運算子,而~
是位級運算中的取反
運算子。
邏輯右移與算術右移
我們先來看左移運算<<
.
對運算元
x
執行x<<k
運算,即x
向左移動k
位。此運算會丟棄最高的k
位,並在右端補k
個0
.
相應而言的右移運算>>
.
對運算元
x
執行x>>k
運算,即x
向右移動k
位。此運算會丟棄最低的k
位,那麼在左端需要補充的k
個位是什麼呢?若執行邏輯右移,則補充
k
個0
,這類似於左移運算.若執行算術右移,則補充
k
個最高有效位
的值。
且幾乎所有的編譯器/機器組合都對有符號數使用算術右移,對無符號數採用邏輯右移。
運算特性
我們通過完成這下面這10個函式,來體會補碼的整數運算特性。
/*
* bitAnd - x&y using only ~ and |
* Example: bitAnd(6, 5) = 4
* Legal ops: ~ |
* Max ops: 8
* Rating: 1
*/
int bitAnd(int x, int y) {
return ;
}
/*
* bitOr - x|y using only ~ and &
* Example: bitOr(6, 5) = 7
* Legal ops: ~ &
* Max ops: 8
* Rating: 1
*/
int bitOr(int x, int y) {
return ;
}
/*
* isZero - returns 1 if x == 0, and 0 otherwise
* Examples: isZero(5) = 0, isZero(0) = 1
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 2
* Rating: 1
*/
int isZero(int x) {
return ;
}
/*
* minusOne - return a value of -1
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 2
* Rating: 1
*/
int minusOne(void) {
return ;
}
/*
* TMax - return maximum two's complement integer
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 4
* Rating: 1
*/
int tmax(void) {
return ;
}
/*
* bitXor - x^y using only ~ and &
* Example: bitXor(4, 5) = 1
* Legal ops: ~ &
* Max ops: 14
* Rating: 2
*/
int bitXor(int x, int y) {
return ;
}
/*
* getByte - Extract byte n from word x
* Bytes numbered from 0 (LSB) to 3 (MSB)
* Examples: getByte(0x12345678,1) = 0x56
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 6
* Rating: 2
*/
int getByte(int x, int n) {
return ;
}
/*
* isEqual - return 1 if x == y, and 0 otherwise
* Examples: isEqual(5,5) = 1, isEqual(4,5) = 0
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 5
* Rating: 2
*/
int isEqual(int x, int y) {
return );
}
/*
* negate - return -x
* Example: negate(1) = -1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 5
* Rating: 2
*/
int negate(int x) {
return ;
}
/*
* isPositive - return 1 if x > 0, return 0 otherwise
* Example: isPositive(-1) = 0.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 8
* Rating: 3
*/
int isPositive(int x) {
return ;
}
下面則分模組討論:每個函式所代表的補碼的整數運算特性。
邏輯運算與位級運算
這兩個函式要分別實現位級運算中的與
和或
操作。
由於二進位制表示的數位只有0與1,所以我們在思考位級運算的時候,可以藉助邏輯運算/命題邏輯中的一些重要定律,即:把位級運算中的0想象成邏輯運算中的FALSE,把1想象成TRUE.
在命題邏輯中,有重要的德摩根律:
對命題p、q,有:
(1)p
∧
q↔
﹁
(﹁
p∨
﹁
q)
(2)p∨
q↔
﹁
(﹁
p∧
﹁
q)
相應地,可以很快地推匯出位級運算中的與
和或
操作。
/*
* bitAnd - x&y using only ~ and |
* Example: bitAnd(6, 5) = 4
* Legal ops: ~ |
* Max ops: 8
* Rating: 1
*/
int bitAnd(int x, int y) {
return ~(~x | ~y);
}
/*
* bitOr - x|y using only ~ and &
* Example: bitOr(6, 5) = 7
* Legal ops: ~ &
* Max ops: 8
* Rating: 1
*/
int bitOr(int x, int y) {
return ~(~x & ~y);
}
那麼對於異或操作,命題邏輯中又是怎麼定義的呢?
對命題p、q,有:
p⊕
q↔
(﹁
p∧
q)∨
(p∧
﹁
q)
故相應的:
/*
* bitXor - x^y using only ~ and &
* Example: bitXor(4, 5) = 1
* Legal ops: ~ &
* Max ops: 14
* Rating: 2
*/
int bitXor(int x, int y) {
return (x & ~y) | (~x & y);
}
異或的用途
從上面關於異或
的定義中我們也可以看到:
p
⊕
q↔
(﹁
p∧
q)∨
(p∧
﹁
q)
即:只有當p
和q
取值不同時,p ⊕ q
才為1(TRUE).
那麼同樣地,在位級運算中,我們可以通過異或的這一性質,用來判斷兩個數值是否相等。
/*
* isZero - returns 1 if x == 0, and 0 otherwise
* Examples: isZero(5) = 0, isZero(0) = 1
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 2
* Rating: 1
*/
int isZero(int x) {
return !(x^0);
}
/*
* isEqual - return 1 if x == y, and 0 otherwise
* Examples: isEqual(5,5) = 1, isEqual(4,5) = 0
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 5
* Rating: 2
*/
int isEqual(int x, int y) {
return !(x^y);
}
需要注意的是:這裡的!
不能改為~
,因為這個函式所做的是一個邏輯運算:判斷某個數是不是
0(或x與y是不是
相等).(從函式的名字isZero
、isEqual
就可以看的出來:最外層進行的必須是一個邏輯運算)
特別的數:-1
在最上方的時候我們已經提過了補碼中的一個重要定理:
B2T ([11···1]) = -1
那麼如何取到[11···1]
呢,很簡單,因為數0
可以表示為[00···0]
,所以對0
進行按位取反操作即可。
/*
* minusOne - return a value of -1
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 2
* Rating: 1
*/
int minusOne(void) {
return ~0;
}
特別的數:0
我們來看isPositive
函式:判斷一個數是否為正,對正數,返回值為1;對非正數,返回值為0.
根據補碼的定義,我們很容易知道:最高有效位為1的數是負數。
那麼最高有效位是0的數是正數嗎?
不然,因為對於0
來說,它的每一位都是0.
“數x
最高有效位是否為1”很好判斷:讓x
的最高有效位先跑到最右邊,也就是x>>31
,然後在與1
按位取或,若最終結果為1,說明最高有效位就是1.
所以isPositive
函式需要滿足兩個命題:
(1)x的最高有效位是0
,即((x>>31) & 1) == 0
(2)x不是0,即x != 0
即:函式的返回值為:(((x>>31) & 1) == 0) && x
(記為式*),由於具有運算子號的限制,我們還要對它繼續進行轉化。
對於((x>>31) & 1) == 0
來說,由於(x>>31) & 1
的結果只有1位,所以這個邏輯運算可以表達成:!((x>>31) & 1)
.
那麼式*就變為了:!((x>>31) & 1) && x
,由於命題!((x>>31) & 1)
的值是0或1,命題x
的值也是0或1,所以邏輯運算子&&
可退化為:兩個只有一個數位的數值的按位取與運算。
/*
* isPositive - return 1 if x > 0, return 0 otherwise
* Example: isPositive(-1) = 0.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 8
* Rating: 3
*/
int isPositive(int x) {
return (!((x>>31) & 1)) & x;
}
邊界值:Tmax
與Tmin
由補碼的定義我們可以知道:
對於
w
位的補碼B2T來說,其邊界值Tmax與Tmin分別為:Tmax = B2T ([01···1]) = 2^(w-1) - 1
Tmin = B2T ([10···0]) = -2^(w-1)
即有:~Tmax = Tmin
那麼我們需要得到Tmin
,即[10···0
]呢?
只需要[10···0] = 1<<31
即可,再對其按位取反,便得到了Tmax
.
/*
* TMax - return maximum two's complement integer
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 4
* Rating: 1
*/
int tmax(void) {
return ~(1<<31);
}
移位運算的潛在含義
下面我們來看getByte函式:
/*
* getByte - Extract byte n from word x
* Bytes numbered from 0 (LSB) to 3 (MSB)
* Examples: getByte(0x12345678,1) = 0x56
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 6
* Rating: 2
*/
int getByte(int x, int n) {
return ;
}
其中,LSB(Least Significant Bit)是“最低有效位”,MSB(Most Significant Bit)是“最高有效位”。
舉個例子:對於十六進位制數0x12345678
,其二進位制表示為[0001 0010 ··· 1000]
,其最低有效位是排列在最右那個0,而最高有效位是排列在最左邊的那個0.
因此,這個函式想要表達的意思就是說:n
的值從0到3,且0代表最低有效位(可以理解為排在最右邊的那個位元組,也就是0x78
),3代表最高有效位(可以理解為排在最左邊的那個位元組,也就是0x12
),同理:1代表的就是0x56
.
那麼我們該如何實現這個函式的功能呢?
我們可以把這個問題分三個步驟考慮:
通過
n
的值,我們就得到了其所代表的兩個數位(比如:當n
為1時,我們就得到,這兩個數位是5 6
;當n
為2時,這兩個數位就是3 4
)我們又知道,最終得到的這兩個數位,其實是在“最右邊”的。依然拿
n
為1來舉例子,我們在第一步得到了5 6
,但我們得把這兩個數放在最右邊啊,否則不就成了0x5600
嗎,它一點都不等於0x56
,即0x0056
.第三步,我們把這兩個數位放到最右邊以後,還得保證它的左側全部是0。這要怎麼做呢——只需要讓它和
0x000000ff
進行按位與操作即可。
這樣轉化了問題之後,我們的難點只剩下一個了,也就是上述過程中的第二步:我們要把這兩個數位向右移動幾位呢?
由於 1 byte = 8 bits
,所以這個問題也很簡單了:當n=1時,向右8位;當n=2時,向右16位…也就是說,我們只需要向右移動8n
位就好了。
那麼這個函式就很好寫了:
/*
* getByte - Extract byte n from word x
* Bytes numbered from 0 (LSB) to 3 (MSB)
* Examples: getByte(0x12345678,1) = 0x56
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 6
* Rating: 2
*/
int getByte(int x, int n) {
int offset = 8 * n;
int a = x >> offset;
int b = 0x000000ff;
return a & b;
}
細心的讀者可能發現了,我們這個函式的實現是不符合題目要求的。題目中還有一個額外的要求,即:我們能夠使用的運算子,只有! ~ & ^ | + << >>
,這其中沒有乘號*
.
那麼,8 * n
又該怎麼表示呢?
這個問題也很簡單。我們都喜歡拿十進位制來思考問題,就比如說:100 * x
是多少呢?小學生都知道:在x
的右邊添上兩個0啊!那1000 * x
呢?添3個0啊!
好了,那麼回到我們的二進位制。2 * x
是多少呢?大學生應該可以知道了:在x
的右邊添1個0啊!那8 * x
呢?添3個0啊!
這也就是移位運算的潛在含義了,我們把x
向左移k
位,其實就是在說:把x * 2^k
.
經過修正後的函式如下:
/*
* getByte - Extract byte n from word x
* Bytes numbered from 0 (LSB) to 3 (MSB)
* Examples: getByte(0x12345678,1) = 0x56
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 6
* Rating: 2
*/
int getByte(int x, int n) {
int offset = n << 3;
int a = x >> offset;
int b = 0x000000ff;
return a & b;
}
補碼中的“相反數”
/*
* negate - return -x
* Example: negate(1) = -1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 5
* Rating: 2
*/
int negate(int x) {
return ~x+1;
}
在補碼中有這樣一個定理:
對於數
x
來說,-x = ~x + 1
.
我們考慮用數學歸納法來證明這個式子:
(1)考慮只有2位的補碼。此時,只有[00] = 0
、[01] = 1
、[10] = -2
、[11] = -1
這四個數,容易發現-x = ~x + 1
.
(2)現假設此結論對於擁有k
位的補碼成立.
(3) 下面證明此結論對於擁有k + 1
位的補碼成立。
由於篇幅與表達所限,只提供證明思路如下:
需要利用假設(2)的條件。
需分別討論:對於
k + 1
位的補碼,當其最高有效位(即符號位)分別為0、1時的情況。
參考資料
[1]《深入理解計算機系統》(第3版). Randal E. Bryant, David R.O’Hallaron 著.