在計算機中,數值類型分為整數型或實數型,其中整型又分為無符類型或有符類型,而實型則只有符類型。 字符類型也分為有符和無符類型。在程序中,用戶可以自己定義是否需要一個非負整數;
一、無符號數和有符號數的表示方式
以一個字節(char類型)為例:若想要表示正負號,一般需要一個位來標記,如取最高代表正負號,則有符號和無符號的數值最大值對比如下:
1 有符號:0111 1111 = 2^6+2^5+2^4+2^3+2^2+2^1+2^0 = 127; ==> 范圍是 -128 ~ 127 2 3 無符號:1111 1111 = 2^7+2^6+2^5+2^4+2^3+2^2+2^1+2^0 = 255;==> 范圍是 0 ~ 255
由上可看出:
- 同樣一個字節大小,有符號和無符號表示的范圍不同,但個數相同均為256個;
- 單純這樣存儲是存在問題:
- 針對有符號數,0在內存中存在兩種方式即+0和-0;
- 針對負數的大小,-1(1000 0001)和-2(1000 0010)單純的從二進制存儲來比較,應該是-2(1000 0010) > -1(1000 0001)這與實際邏輯不吻合;
二進制補碼避免了這個問題,這也是當今最常用的系統存儲方式:即高位段0代表正數,1代表負數,表示正數為原碼,而表示負數的方式采用:補碼 = 反碼+1
PS:
原碼:一個整數,按照絕對值大小轉換成的二進制數,最高為為符號位,稱為原碼。
反碼: 將二進制除符號位數按位取反,所得的新二進制數稱為原二進制數的反碼。 正數的反碼為原碼,負數的反碼是原碼符號位外按位取反。取反操作指:原為1,得0;原為0,得1。(1變0; 0變1)
補碼: 反碼加1稱為補碼。
1 例如針對有符號數的±1內存存儲形式為: 2 +1:0000 0001(原碼) ==> 0000 0001 3 -1:1111 1110(反碼) + 1 ==> 1111 1111 4 -2:1111 1101(反碼) + 1 ==> 1111 1110 5 這樣做也符合了正常邏輯:-1 > -2....
注意: 單純從一個字節8位二進制存儲上來看,1111 1111 既可以表示有符號的-1又可以表示無符號的255
1 //最高位是否表示正負號示例 2 //0x80 = 1000 0000 &按位與操作,按位比較兩數字,相同為1,不同為0; 3 #include <stdio.h> 4 int main() 5 { 6 char c = 1; 7 short s = -2; 8 int i = 3; 9 printf("%d\n", ( (c & 0x80) != 0) ); //0 10 printf("%d\n", ( (s & 0x8000) != 0) ); //1 11 printf("%d\n", ( (c & 0x80000000) != 0) ); //0 12 return 0; 13 }
二、迷惑人的有符號下無符號數的比較操作
無符號數與有符號數間的比較
1 #include <stdio.h> 2 int main() 3 { 4 int a = -1; 5 unsigned int b = 1; 6 if(a > b) 7 printf("a > b, a = %d, b = %u\n", a, b); 8 else 9 printf("a <= b, a = %d, b = %u\n", a, b); 10 return 0; 11 } 12 //print: a > b, a = -1, b = 1
當執行一個運算時(如這里的a>b),如果它的一個運算數是有符號的而另一個數是無符號的,那么C語言會隱式地將有符號 參數強制類型為無符號數,並假設這兩個數都是非負的,來執行這個運算。這種方法對於標准的算術運算(四則運算)來說並無多大差異,但是對於像<和>這樣的比較運算就可能產生非直觀的結果。
對大多數C語言的實現,處理同樣字長的有符號數和無符號數之間的相互轉換的一般規則是:數值可能會改變,但是位模式不變。也就是說,將unsigned int強制類型轉換成int,或將int轉換成unsigned int底層的位表示保持不變。
示例中
-1(變量a的值:1111 1111 1111 1111 1111 1111 1111 1111)這個有符號數強制轉換成無符號數(1111 1111 1111 1111 1111 1111 1111 1111= 2^32-1= 4294967295,從二進制存儲上來看,無符號數所有位都為1時表示的時最大值)然后再與 1(變量b的值:0000 0000 0000 0000 0000 0000 0000 0001)來進行比較;
總結:當有符號數遇見無符號數參與計算時,則有符號數進行轉換為無符號數
三、查看驗證結果(顯示存儲的形式)
為了證明上面所說的內容,寫段代碼將內存中的存儲形式顯示出來:代碼中函數show_byte,它可以把從指針start開始的len個字節的值以16進制數的形式打印出來。
1 #include <stdio.h> 2 void show_byte(unsigned char *start, int len) 3 { 4 int i = 0; 5 for(; i < len; ++i) 6 printf(" %.2x", start[i]); 7 printf("\n"); 8 } 9 10 int main() 11 { 12 int a = -1; 13 unsigned int b = 4294967295; 14 printf("a = %d, a = %u\n", a, a); 15 printf("b = %d, b = %u\n", b, b); 16 show_byte((unsigned char*)&a, sizeof(int)); 17 show_byte((unsigned char*)&b, sizeof(unsigned int)); 18 return 0; 19 } 20 /*print: 21 a = -1, a = 4294967295 22 b = -1, b = 4294967295 23 ff ff ff ff 24 ff ff ff ff 25 printf函數中,%u表示以無符號數十進制的形式輸出,%d表示以有符號十進制的形式輸出。通過show_byte函數,我們可以看到,-1與4 294 967 295的底層表示是一樣的,它們的位全部都是全1,即每個字節表示為ff。 26 */
四、由無符號數值參與減法運算的錯誤
1 //求某個數組中前length個元素的和的代碼段: 2 int sum_elements(float a[], unsigned length) 3 { 4 int i = 0; 5 int sum = 0; 6 for(i = 0; i <= length -1; ++i) 7 sum += a[i]; 8 return sum; 9 }
因為數據的長度(或個數)肯定是一個非負數,所以把length聲明為一個unsigned很合理,計算的數據個數和返回類型也正確。的確如此,但是這都是在length不為0的情況,試想,當調用函數時,把0作為參數傳遞給length會發生什么事情?回想一下前面我們所說的知識,因為length是unsigned類型,所以所有的運算都被隱式地被強制轉換為unsigned類型,所以length-1(即0-1 = -1),-1對應的無符號類型的值為最大值,所以for循環將會循環(4 294 967 295)次,另一方面,當0-1操作結束時數組也會越界,發生錯誤。
那么如何優化上面的代碼呢?其實答案非常簡單,你也可以自己想一想,這里就給出答案吧,就是把for循環改為:for(i = 0; i < length; ++i)
1 //比較兩個字符串長度:判斷第一個字符串是否長於第二個字符串,若是,返回1,若否返回0; 2 int strlonger(char *s1, char *s2) 3 { 4 return strlen(s1) - strlen(s2) > 0; 5 }
在Linux下可用man 3 strlen命令查看,strlen函數的原型為:
size_t strlen(const char *s);
該函數原型返回一個數據類型size_t,它被定義在stdio.h文件中,是一種機器相關的無符號類型,他被設計得足夠大以便能夠表示內存中任意對象的大小,即本質上屬於unsigned int,
另一方面,一個字符串的長度當然不可能為負,這樣的定義顯然是合理的,但是有時卻因為這樣,而存在不少的問題,如函數strlonger的實現。當s1的長度大於等於s2時,這個函數並沒有什么問題,但是你可以想像,當s1的長度小於s2的長度時,這個函數會返回什么嗎?沒錯,因為此時strlen(s1) - strlen(s2)為負(從數學的角度來解釋的話),而又由於程序把它作為unsigned為處理,則此時的值肯定是一個比0大的值,即永遠為真,返回1。換句話來說,這個函數只有在strlen(s1) == strlen(s2)時返回假,其他情況都返回真。
1 //測試無符號數減法 2 #include <stdio.h> 3 #include <string.h> 4 5 int strlonger(char *s1, char *s2) 6 { 7 return strlen(s1) - strlen(s2) > 0; 8 } 9 10 int main() 11 { 12 char s1[] = "abc"; 13 char s2[] = "cd"; 14 if(strlonger(s1, s2)) 15 printf("s1 is longer than s2, s1 = %s, s2 = %s\n", s1, s2); 16 else 17 printf("s1 is shorter than s2, s1 = %s, s2 = %s\n", s1, s2); 18 19 if(strlonger(s2, s1)) 20 printf("s2 is longer than s1, s2 = %s, s1 = %s\n", s2, s1); 21 else 22 printf("s2 is shorter than s1, s2 = %s, s1 = %s\n", s2, s1); 23 return 0; 24 }
若符合正常邏輯則修改strlonger函數改為:
1 int strlonger(char *s1, char *s2) 2 { 3 return strlen(s1) > strlen(s2); 4 }
這樣就可以利用兩個無符號數進行直接的比較,而不會因為減法而出現負數(數學上來說)而影響比較結果。
總結:當無符號數參與計算時,盡量避免減法,代之為比較邏輯
五、無符號使用的建議
- 除一定非使用無符號類型時,才進行使用unsigned類型;
- 使用unsigned類型時避免比較運算和減法運算操作;
六、補充部分:整數的溢出
從第二項:迷惑人的有符號下無符號數的比較操作的分析部分可以看出:對於一個固定長度的無符號數:
1 MAX_VALUE +1 == MIN_VALUE 2 MIN_VALUE –1 == MAX_VALUE
可以把無符號數看作是汽車的里程表,當達到它能表示的最大值時,會重新從起始點開始;即:
- 對於無符號數(unsigned int)超過最大值時,變量會從0開始;
- 對於有符號數( int)超過最大值時,變量會從-2147483648開始;
注意:溢出的行為是未定義的行為,也就是說,在程序運行時,溢出可能會從起始點開始,也可能是其它的情況;
