都是型別惹的禍 小心unsigned

2021-09-09 03:04:55 字數 3938 閱讀 6469

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

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

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

#include #include int main()

輸出結果為:

看到輸出結果之後,你可能會大吃一驚,-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 #include void show_bytes(unsigned char *start, int len)

int main()

輸出為:

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

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

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

**1是乙個求陣列中前length個資料的和的函式,陣列中元素的個數由引數length給出,**如下:

float sum_elements(float a, unsigned length)

如果我告訴你這是一段有錯的**,可能你也不太相信,因為這個函式的一切看起來是這麼的自然,因為資料的長度(或個數)肯定是乙個非負數,所以把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)

如果我又跟你說這段**是有bug,你現在找不找得出來呢,還是認為這段**是沒有任何問題的呢?說真的就這麼看這個函式好像的確是沒有什麼問題,但是如果你知道了strlen函式的原型,可能你就會有點明白了,在linux下可用man 3 strlen命令檢視,strlen函式的原型為:

size_t strlen(const char *s);
注意這裡有乙個資料型別size_t,它被定義在stdio.**件中,其實它就是unsigned int,乙個字串的長度當然不可能為負,這樣的定義顯然是合理的,但是有時卻因為這樣,而存在不少的問題,如函式strlonger的實現。當s1的長度大於等於s2時,這個函式並沒有什麼問題,但是你可以想像,當s1的長度小於s2的長度時,這個函式會返回什麼嗎?沒錯,因為此時strlen(s1) - strlen(s2)為負(從數學的角度來解釋的話),而又由於程式把它作為unsigned為處理,則此時的值肯定是乙個比0大的值。換句話來說,這個函式只有在strlen(s1) == strlen(s2)時返回假,其他情況都返回真。

下面是我的測試**:

#include #include #include int strlonger(char *s1, char *s2)

int main()

執行結果如下:

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

int strlonger(char *s1, char *s2)

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

五、建議

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

都是型別惹的禍 小心unsigned

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

都是型別惹的禍 小心unsigned

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

都是型別惹的禍 小心unsigned

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