案件
最近在看一個項目的時候,遇到了一段代碼,去處掉一些不相關的東西以后,剩下的大概就是這些了吧。
#include <stdio.h>
int array[] = {1, 2, 3, 4, 5, 6, 7};
#define LEN (sizeof(array) / sizeof(array[0]))
int main(void)
{
int i, sum = 0;
for(i = -1; i < LEN - 1; i++)
sum += array[i+1];
printf("%d\n", sum);
return 0;
}
執行效果是這個樣子的:
hdd@hdd:~/cprogram(master)$ ./a.out
0
這個執行結果相當詭異,雖然循環的初始變量是-1,但是下面取數組元素的時候,也把i加上了1啊,理論上不會是這個結果啊。
調查
任何詭異的事情必然就有原因,不能錯過這樣一個毀三觀的機會。
鎖定問題
N久沒動手寫過C語言,但是從-1開始賦值這種作死的寫法,還是應該避免,所以我修改了一下循環,成了下面這個樣子。
for(i = 0; i < LEN; i++)
sum += array[i]
運行結果是:
hdd@hdd:~/cprogram(master)$ ./a.out
28
居然對了!多運行了幾次,結果都是對的,排除了程序其他部分的問題,就鎖定在循環體本身或者是累加變量出錯了。于是用gdb將斷點打在了
sum += array[i+1]
希望能夠觀察到運行過程中,局部變量sum的值的變化。
但是斷點設置好了以后,執行運行,直接就執行完畢了,這個結果有點讓人意外,但是還是說明了問題所在,這個循環體根本就沒有進去啊!
嘗試解釋
既然現在知道了是循環體沒有進去,那么問題就應該很簡單了,要么就是循環變量初始化錯了,要么就是循環條件出錯了。
首先本著哪里復雜,哪里更有可能出錯的基礎上,懷疑循環條件出錯了,因為這個循環條件里面,主要是牽涉到一個宏展開,而且兩份代碼,一個運行錯誤,一個運行正常,其中一個差異就是條件中是否是LEN - 1
,于是我懷疑是宏展開的時候產生了歧義,因為宏展開是在預處理階段做的,于是使用gcc -E
,看了一下預處理以后的結果,省略去無關代碼以后,只看展開后的循環體:
for(i = -1; i < (sizeof(array) / sizeof(array[0])) - 1; i++)
sum += array[i + 1];
看了一下,好像就是我們想要他表達的意思,當然代碼很多問題是看不出來的,得運行,于是我在代碼中加上了這樣兩行:
int len = (sizeof(array) / sizeof(array[0])) - 1;
printf("%d\n", len);
執行以后,準確的輸出了6,完美的避開了我們的猜想。
那就只剩下最后一個可能性了,有人說的好。排除掉所有的可能性以后,最不可能的就是答案,那問題就出在了循環變量初始化上了,就是i = -1
出錯了。
可是問題到這里,我實在想不出有什么問題能讓這句出錯,一句簡單的賦值。
求助
這個時候我求助了 @lyc 同學,事實證明,我問對人了,我給他運行了一下程序,他問了一句,這個LEN的展開結果,是有符號數還是無符號數啊?我恍然大悟,sizeof
的返回值是size_t
,而size_t從剛剛gcc -E
中這句可以看出類型:
typedef long unsigned int size_t;
是一個無符號整數。
結論
這樣子,問題就清晰了,因為sizeof返回的是無符號數,所以LEN是一個無符號數,當無符號數和-1做<操作的時候,根據類型轉換規則,會將-1轉換成對應的無符號數,因為機器碼中-1最高位是1,當按照無符號數解釋的時候,就相當大了,所以<返回False,循環體不會執行
刨根
本著死磕到底的決心,我決定弄明白c語言的類型轉換規則,于是參考了《一站式學習C編程》中的這部分內容
數據類型系統
首先為了弄明白這個問題,得弄明白的另外一個問題是C語言中的基本數據類型,首先申明,由于不同體系結構之間的差異,這里默認講的是x86/Linux/gcc。
整型
在C語言中char型占了一個字節的存儲空間,一個字節通常是8個bit,如果按照無符號數來解釋,就是0~255, 如果按照有符號來解釋,就是-128~127。C語言規定了signed和unsigned兩個關鍵字來表示是否是無符號數。
那么常用的char是無符號還是有符號呢?在C標準中規定是Implementation Defined
,設么意思呢,就是編譯器在實現的時候,可以按照是無符號,也可以按照是有符號,在不同平臺上,那種效率高,就采用哪種實現,但是具體的實現方式,需要在文檔中明確寫出。這里面提到的Implementation Define還有Unspecifiled、Undefined這三種的區別,后續會用一篇文章來講,這里僅僅是提到一下。
除了char以外,整型還包括short int、int、long int、long long int等,每種都可以加上signed和unsigned修飾。
浮點數
C標準中規定的浮點型有float、double、long double
類型轉換
簡單的講完了類型以后,就可以講一下類型轉換規則了。
Interger Promotion
這條轉換規則規定:
- 在一個表達式中,凡是可以使用int和unsigned int作為右值的,也都可以使用有符號和無符號的char型、short型和Bit-filed。
- 如果原始類型的取值范圍都能用int型表示,則類型被提升為int,如果表示不了,則提升為unsigned int
主要適用于以下情況:
- 如果函數的形參類型未知,比如Old Style C的函數聲明,或者參數列表中有...
- 算數運算中的類型轉換。有符號和無符號的char、short和Bit-filed做算數運算的時候
Usual Arithmetic Conversion
這條規則規定,連個算數類型的操作數做算數運算,如果兩邊操作數的類型不同,編譯器自動做類型轉換,是的兩邊類型相同之后,再繼續運算。轉換規則如下:
- 如果一邊的類型是long double,則把另外一邊也轉換成long double
- 否則,如果一邊的類型是double,則把另外一邊也轉成double
- 否則,如果有一邊是float,則把另外一邊也轉換成float
- 否則,兩邊都是整數的話,按照Integer Promotion做類型轉換
如果類型還是不相同,則需要繼續轉換,我們規定char、short、int、long、long long的轉換級別一個比一個高,相同類型的unsigned和signed具有相同rank,則繼續轉換規則如下:
- 如果兩邊都是有符號的,或者都是無符號的,那么腳底rank轉換成較高rank
- 否則,如果一邊是無符號,一邊有符號,且無符號的rank不低于有符號的rank,則把有符號轉換成無符號類型
- 否則,一邊有符號,一邊無符號,且無符號的rank低于有符號rank。如果有符號類型可以覆蓋這個無符號數的取值,則把無符號數轉換成有符號數的類型
- 否則,則把兩邊都轉成有符號數的rank對應的無符號類型
賦值操作
除了算數運算,在賦值或者初始化時,等號兩邊的類型不相同,則編譯器會把等號右邊的類型,轉換成等號左邊的類型,再賦值。
這個規則比較簡單,但是這里面有兩個很容易被忽略的情況。函數調用傳參的過程,相當于定義形參,并且用實參對形參初始化,函數的返回過程,相當于定義一個臨時變量,并且用return的表達式對其初始化,所以上述規則也適用于這兩種情況。
強制類型轉換
除了上面講到三種稱為隱式轉換規則,我們自己也可以通過類型轉換運算符,規定某個表達式的類型
以上,洋洋灑灑的把類型轉換的基本規則差不多寫完了,但是給人的感覺就是復雜的令人發指。越是復雜的東西,就越是很難憑借記憶去掌握,所以上面的轉換規則我覺得干脆不需要掌握,只是讓自己知道,類型之間的混用會很麻煩很容易引發我們開篇講到的bug,所以需要的是盡量避免,并且在程序出錯的時候能夠想起來從這上面找到原因,就OK了。
好了,就先扯這么多了,最后感謝宋勁彬先生的《一站式學習C編程》以及 @雨痕 老師的推薦