有符號數和無符號數


在計算機中,數值類型分為整數型或實數型,其中整型又分為無符類型或有符類型,而實型則只有符類型。 字符類型也分為有符和無符類型。在程序中,用戶可以自己定義是否需要一個非負整數;

一、無符號數和有符號數的表示方式

以一個字節(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

由上可看出:

  1. 同樣一個字節大小,有符號和無符號表示的范圍不同,但個數相同均為256個;
  2. 單純這樣存儲是存在問題:
    1. 針對有符號數,0在內存中存在兩種方式即+0和-0;
    2. 針對負數的大小,-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 +10000 0001(原碼)     ==> 0000 0001
3 -11111 1110(反碼) + 1 ==>  1111 1111
4 -21111 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 024 }

若符合正常邏輯則修改strlonger函數改為:

1 int strlonger(char *s1, char *s2) 
2 {
3     return strlen(s1) > strlen(s2); 
4 }

這樣就可以利用兩個無符號數進行直接的比較,而不會因為減法而出現負數(數學上來說)而影響比較結果。

 總結:當無符號數參與計算時,盡量避免減法,代之為比較邏輯

五、無符號使用的建議

  1.  除一定非使用無符號類型時,才進行使用unsigned類型;
  2.  使用unsigned類型時避免比較運算和減法運算操作;

六、補充部分:整數的溢出

從第二項:迷惑人的有符號下無符號數的比較操作的分析部分可以看出:對於一個固定長度的無符號數:

1 MAX_VALUE +1 == MIN_VALUE
2 MIN_VALUE –1 == MAX_VALUE

可以把無符號數看作是汽車的里程表,當達到它能表示的最大值時,會重新從起始點開始;即:

  1. 對於無符號數(unsigned int)超過最大值時,變量會從0開始;
  2. 對於有符號數(               int)超過最大值時,變量會從-2147483648開始;

注意:溢出的行為是未定義的行為,也就是說,在程序運行時,溢出可能會從起始點開始,也可能是其它的情況;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM