1. 補碼
在計算機中無符號數用原碼表示,有符號數用補碼表示。w位補碼表示的值為:
最高位 也稱符號位,1表示負數,0表示正數,符號位為0時,和無符號數的表示是相同的,以下是4位補碼的示例:
0101 = -0*23 + 1*22 + 0*21 + 1*20 = 5
1101 = -1*23 + 1*22 + 0*21 + 1*20 = -3
w位的補碼表示的數值范圍是[-2w-1, 2w-1-1],如4位的補碼表示的最小值是-8(1000),最大值是7(0111)。
只有理解了有符號數的補碼表示,才能真正理解無符號數和有符號數的轉換、有符號數的截斷和溢出等問題。
2. 無符號數和有符號數的轉換
C語言中的強制類型轉換保持二進制位值不變,只是改變解釋位的方式。看以下代碼:
short int v = -12345;
unsigned short uv = (unsigned short)v;
printf(“v = %d, uv = %u\n”, u, uv);
輸出如下:
v = -12345, uv = 53191
由於-12345的16位補碼表示與53191的16位無符號表示是完全一樣的,所以會得到以上輸出。
無符號數和有符號數之間的轉換是一一對應的關系,w位的有符號數s轉換無符號數u的對應關系為:
如4位有符號數7(0111)轉換為無符號數也是7,而4位有符號數-1(1111)轉換為無符號數是15。
類似地,w位的無符號數u轉換為有符號數s的對應關系為:
如4位無符號數5(0101)轉換為無符號數也是5,而4位無符號數13(1101)轉換為無符號數為-3。
其實只要知道無符號數和有符號數對二進制位的解釋方式,無需記住上述的對應關系,也能算出轉換后的值。
3. 陷阱
在C語言中,如果一個運算包含一個有符號數和一個無符號數,那么C語言會隱式地將有符號數轉換為無符號數,這對於標准的算術運算沒什么問題,但是對於 < 和 > 這樣的關系運算符來說,它會出現非直觀的結果,這種非直觀的特性經常會導致程序中難以察覺的錯誤。看下面的例子:
int strlonger(char *s, char *t)
{
return strlen(s) - strlen(t) > 0;
}
上面的函數看起來似乎沒什么問題,實際上當s比t短時,函數的返回值也是1,為什么會出現這種情況呢?原來strlen的返回值類型為size_t,C語言中將size_t定義為unsigned int,當s比t短時,strlen(s) - strlen(t)為負數,但無符號數的運算結果隱式轉換為無符號數就變成了很大的無符號數。為了讓函數正確工作,代碼應該修改如下:
return strlen(s) > strlen(t);
2002年,從事FreeBSD開源操作系統項目的程序員意識到,他們對getpeername函數的實現存在安全漏洞。代碼的簡化版本如下:
void *memcpy(void *dest, void *src, size_t n);
#define KSIZE 1024
char kbuf[KSIZE];
int copy_from_kernel(void *user_dest, int maxlen)
{
int len = KSIZE < maxlen ? KSIZE : maxlen;
memcpy(user_dest, kbuf, len);
retn len;
}
你看出了問題所在嗎?
4. 擴展、截斷和溢出
將無符號數轉換為更大的數據類型時,只需簡單地在開頭添加0,這種運算稱為0擴展。將有符號數轉換為更大的數據類型需要執行符號擴展,規則是將符號位擴展至所需要的位數。如將4位的二進制數1001(-7)擴展為8位的結果為11111001(-7)。
將一個大的數據類型轉換為小的數據類型時,不管是無符號數還是有符號數都是簡單地進行位截斷。無符號數的數值大小可能因截斷而變化,而有符號數不僅數值大小可能變化,符號位也可能發生改變,如8位二進制數00011001(25)轉換為4位數截斷的結果是1001(-7)。
在進行整數的算術運算時,當結果變量的位數不足以存放實際實際結果的位數時,運算的結果就會因截斷而產生溢出,如果4位二進制數運算1011(-5) + 1011(-5) = 10110(-10),但如果結果也采用4位二進制存放就會截斷為0110(6),產生溢出。
當數據類型轉換時,同時需要在不同數據大小,以及無符號和有符號之間轉換時,C語言標准要求先進行數據大小的轉換,之后再進行無符號和有符號之間的轉換。