1. 程式人生 > >都是型別惹的禍——小心unsigned

都是型別惹的禍——小心unsigned

都是型別惹的禍——小心unsigned

2013年08月27日 00:39:05 ljianhui 閱讀數:35062 標籤: C程式設計資料表示程式設計陷阱unsigned優化 更多

個人分類: 程式設計中的陷阱及優化

版權宣告:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/ljianhui/article/details/10367703

正如我們所知道的,程式設計語句都有很多的基本資料型別,如char,inf,float等等,而在C和C++中還有一個特殊的型別就是無符號數,它由unsigned修飾,如unsigned int等。大家有沒想過,就是因為這些不同的型別,而使大家編寫的看似非常正確的程式出現了預想不到的錯誤呢?

 

一、迷惑人的有符號下無符號數的比較操作

廢話不多說,馬上來看一下例子,讓你先來體驗一下這個奇妙的旅程,原始碼檔名為unsigned.c,原始碼如下:

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int a = -1;
    unsigned int b = 1;
 
    if(a > b)
        printf("a > b, a = %d, b = %u\n", a, b);
    else
        printf("a <= b, a = %d, b = %u\n", a, b);
    exit(0);
}

輸出結果為:

 

 

看到輸出結果之後,你可能會大吃一驚,-1竟然大於1,你沒有看錯,從輸出結果上來看的確是這樣。為什麼會產生這樣的結果呢?這還得從C語言對同時包含有符號數和無符號數表示式的處理方式講起。

 

二、有符號數與無符號運算時數強制型別轉換方式及底層表示

 

當執行一個運算時(如這裡的a>b),如果它的一個運算數是有符號的而另一個數是無符號的,那麼C語言會隱式地將有符號 引數強制型別為無符號數,並假設這兩個數都是非負的,來執行這個運算。這種方法對於標準的算術運算來說並無多大差異,但是對於像<和>這樣的運算就可能產生非直觀的結果。

 

所以對應回上面的例子,就是它先把-1(變數a的值)這個有符號數強制轉換成無符號數,然後再與1(變數b)的值,來進行比較,並假設這兩個數原本都是非負的,然後進行比較。那麼-1轉換為無符號數後,其值為多少呢?你可以寫一個小小的程式來驗證一下,在32和64位的機子上,-1對應的無符號數應該是4 294 967 295,即32位的無符號數的最大值(UMax),所以if中的條件總是為真。

 

要想這段程式碼正常執行,我們需要怎麼辦呢?很簡單,把if語句改為if(a > (int)b)即可。這樣程式就會認為是兩個有符號數在進行比較,-1就不會隱式地轉換為無符號數而變成UMax。

 

可能你已經有一個問題,為什麼使用強制型別,把變數b的型別變成int程式就能正常,而-1轉換成無符號數為什麼會是4 294 967 295呢?這就得從整型資料在計算機中的表示和C語言對待強制型別轉換的方式說起。

 

我們知道,整數在計算機中通常是以補碼的形式存在的,而-1的補碼(用4個位元組儲存)為1111,1111,1111,1111。而C語言對於強制型別轉換是怎麼處理的呢?對大多數C語言的實現,處理同樣字長的有符號數和無符號數之間的相互轉換的一般規則是:數值可能會改變,但是位模式不變。也就是說,將unsigned int強制型別轉換成int,或將int轉換成unsigned int底層的位表示保持不變。

 

也就是說,即使是-1轉換成unsigned int之後,它在記憶體中的表示還是沒有改變,即1111,1111,1111,1111。我們知道在計算機的底層,資料是沒有型別可言的,所有的資料非0即1。資料型別只有在高層的應用程式才有意義,也就是說,同樣的儲存表示對於應用程式而言可能對應著不同的資料,例如1111,1111,1111,1111對於有符號數而言它表示-1,但對於無符號數而言,它表示UMax,但是它們的底層儲存都是一樣的。現在你應該明白為什麼-1轉換成無符號數之後,就成了UMax了吧。

 

三、檢視資料的底層表示

 

為了證明上面所說的內容,請再看下面的程式碼,裡面有個函式show_byte,它可以把從指標start開始的len個位元組的值以16進位制數的形式打印出來。原始檔為showbyte.c,程式碼如下:

#include <stdio.h>
#include <stdlib.h>
 
void show_bytes(unsigned char *start, int len)  //一個形參是字元型別指標,一個形參是字元陣列的長度len
{
    int i = 0;
    for(; i < len; ++i)
        printf(" %.2x", start[i]);  //此處沒有用指標訪問,而是使用陣列元素訪問的
    printf("\n");
}
 
int main()
{
    int a = -1;
    unsigned int b = 4294967295;
 
    printf("a = %d, a = %u\n", a, a);
    printf("b = %d, b = %u\n", b, b);
 
    show_bytes((unsigned char*)&a, sizeof(int));
    show_bytes((unsigned char*)&b, sizeof(unsigned int));
    exit(0);
}

輸出為:

 

 

分析:printf函式中,%u表示以無符號數十進位制的形式輸出,%d表示以有符號十進位制的形式輸出。通過show_bytes函式,我們可以看到,-1與4 294 967 295的底層表示是一樣的,它們的位全部都是全1,即每個位元組表示為ff。

 

四、由於無符號數減法引起的錯誤

 

你可能會說,你不會用一個無符號數與一個有符號數作比較,所以你覺得你可以放心了,但是來看看下面的兩段程式碼。

程式碼1是一個求陣列中前length個數據的和的函式,陣列中元素的個數由引數length給出,程式碼如下:

float sum_elements(float a[], unsigned length)
{
    int i = 0;
    float sum = 0;
    for(i = 0; i <= length -1; ++i)
        sum += a[i];
    return sum;
}

如果我告訴你這是一段有錯的程式碼,可能你也不太相信,因為這個函式的一切看起來是這麼的自然,因為資料的長度(或個數)肯定是一個非負數,所以把length宣告為一個unsigned很合理,計算的資料個數和返回型別也正確。的確如此,但是這都是在length不為0的情況,試想,當呼叫函式時,把0作為引數傳遞給length會發生什麼事情?回想一下前面我們所說的知識,因為length是unsigned型別,所以所有的運算都被隱式地被強制轉換為unsigned型別,所以length-1(即0-1 = -1),-1對應的無符號型別的值為UMax,所以for迴圈將會迴圈UMax次,陣列也會越界,發生錯誤。那麼如何優化上面的程式碼呢?其實答案非常簡單,你也可以自己想一想,這裡就給出答案吧,就是把for迴圈改為:

for(i = 0; i < length; ++i)

因為去除了length-1,所以當length為0時也能正常比較。

 

接下來是程式碼2,它是一個判斷第一個字串是否長於第二個字串,若是,返回1,若否返回0,程式碼如下:

int strlonger(char *s1, char *s2)
{
    return strlen(s1) - strlen(s2) > 0;
}

如果我又跟你說這段程式碼是有bug,你現在找不找得出來呢,還是認為這段程式碼是沒有任何問題的呢?說真的就這麼看這個函式好像的確是沒有什麼問題,但是如果你知道了strlen函式的原型,可能你就會有點明白了,在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大的值。換句話來說,這個函式只有在strlen(s1) == strlen(s2)時返回假,其他情況都返回真。

 

下面是我的測試程式碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int strlonger(char *s1, char *s2)
{
    return strlen(s1) - strlen(s2) > 0;
}
 
int main()
{
    char s1[] = "abc";
    char s2[] = "cd";
 
    if(strlonger(s1, s2))
        printf("s1 is longer than s2, s1 = %s, s2 = %s\n", s1, s2);
    else
        printf("s1 is shorter than s2, s1 = %s, s2 = %s\n", s1, s2);
 
    if(strlonger(s2, s1))
        printf("s2 is longer than s1, s2 = %s, s1 = %s\n", s2, s1);
    else
        printf("s2 is shorter than s1, s2 = %s, s1 = %s\n", s2, s1);
}

執行結果如下:

 

 

從執行結果來看,確實如此,只要s1與s2長度不等,就返回真。那麼我們在怎麼樣改善這段程式碼呢?其實答案也是很簡單的,所函式改為如下即可:

int strlonger(char *s1, char *s2)
{
    return strlen(s1) > strlen(s2);
}

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

 

五、建議

這麼看來,unsigned還真是一個危險的東西,大家還是要謹慎使用啊。其實個人建議,沒有什麼必要的原因,就不要使用unsigned,即使有時它看起來是那麼的合理,因為有它在的運算,很多時候會產生非直觀的錯誤,而且這種錯誤還非常難發現。如果你要使用的話,則儘量避免有符號數與無符號數的比較運算和避免減法運算,在很多時候,在unsigned的世界裡,x-y>0與x>y都是不等價的。