1. 程式人生 > >C語言中補碼的整數運算特性

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位,並在右端補k0.

相應而言的右移運算>>.

對運算元x執行x>>k運算,即x向右移動k位。此運算會丟棄最低的k位,那麼在左端需要補充的k個位是什麼呢?

若執行邏輯右移,則補充k0,這類似於左移運算.

若執行算術右移,則補充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)

即:只有當pq取值不同時,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是不是相等).(從函式的名字isZeroisEqual就可以看的出來:最外層進行的必須是一個邏輯運算)

特別的數:-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;
}

邊界值:TmaxTmin

由補碼的定義我們可以知道:

對於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.

那麼我們該如何實現這個函式的功能呢?

我們可以把這個問題分三個步驟考慮:

  1. 通過n的值,我們就得到了其所代表的兩個數位(比如:當n為1時,我們就得到,這兩個數位是5 6;當n為2時,這兩個數位就是3 4

  2. 我們又知道,最終得到的這兩個數位,其實是在“最右邊”的。依然拿n為1來舉例子,我們在第一步得到了5 6,但我們得把這兩個數放在最右邊啊,否則不就成了0x5600嗎,它一點都不等於0x56,即0x0056.

  3. 第三步,我們把這兩個數位放到最右邊以後,還得保證它的左側全部是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 著.