我們知道,在C語言中char
型占一個位元組的存儲空間,一個位元組通常是8個bit。如果這8個bit按無符號整數來解釋,取值範圍是0~255,如果按有符號整數來解釋,採用2's Complement表示法,取值範圍是-128~127。C語言規定了signed
和unsigned
兩個關鍵字,unsigned char
型表示無符號數,signed char
型表示有符號數。
那麼以前我們常用的不帶signed
或unsigned
關鍵字的char
型是無符號數還是有符號數呢?C標準規定這是Implementation Defined,編譯器可以定義char
型是無符號的,也可以定義char
型是有符號的,在該編譯器所對應的體繫結構上哪種實現效率高就可以採用哪種實現,x86平台的gcc
定義char
型是有符號的。這也是C標準的Rationale之一:優先考慮效率,而可移植性尚在其次。這就要求程序員非常清楚這些規則,如果你要寫可移植的代碼,就必須清楚哪些寫法是不可移植的,應該避免使用。另一方面,寫不可移植的代碼有時候也是必要的,比如Linux內核代碼使用了很多只有gcc
支持的語法特性以得到最佳的執行效率,在寫這些代碼的時候就沒打算用別的編譯器編譯,也就沒考慮可移植性的問題。如果要寫不可移植的代碼,你也必須清楚代碼中的哪些部分是不可移植的,以及為什麼要這樣寫,如果不是為了效率,一般來說就沒有理由故意寫不可移植的代碼。從現在開始,我們會接觸到很多Implementation Defined的特性,C語言與平台和編譯器是密不可分的,離開了具體的平台和編譯器討論C語言,就只能討論到本書第一部分的程度了。注意,ASCII碼的取值範圍是0~127,所以不管char
型是有符號的還是無符號的,存一個ASCII碼都沒有問題,一般來說,如果用char
型存ASCII碼字元,就不必明確寫是signed
還是unsigned
,如果用char
型表示8位的整數,為了可移植性就必須寫明是signed
還是unsigned
。
在C標準中沒有做明確規定的地方會用Implementation-defined、Unspecified或Undefined來表述,在本書中有時把這三種情況統稱為“未明確定義”的。這三種情況到底有什麼不同呢?
我們剛纔看到一種Implementation-defined的情況,C標準沒有明確規定char
是有符號的還是無符號的,但是要求編譯器必須對此做出明確規定,並寫在編譯器的文檔中。
而對於Unspecified的情況,往往有幾種可選的處理方式,C標準沒有明確規定按哪種方式處理,編譯器可以自己決定,並且也不必寫在編譯器的文檔中,這樣即便用同一個編譯器的不同版本來編譯也可能得到不同的結果,因為編譯器沒有在文檔中明確寫它會怎麼處理,那麼不同版本的編譯器就可以選擇不同的處理方式,比如下一章我們會講到一個函數調用的各個實參表達式按什麼順序求值是Unspecified的。
Undefined的情況則是完全不確定的,C標準沒規定怎麼處理,編譯器很可能也沒規定,甚至也沒做出錯處理,有很多Undefined的情況編譯器是檢查不出來的,最終會導致運行時錯誤,比如數組訪問越界就是Undefined的。
初學者看到這些規則通常會很不舒服,覺得這不是在學編程而是在啃法律條文,結果越學越泄氣。是的,C語言並不像一個數學定理那麼完美,現實世界裡的東西總是不夠完美的。但還好啦,C程序員已經很幸福了,只要嚴格遵照C標準來寫代碼,不要去觸碰那些陰暗角落,寫出來的代碼就有很好的可移植性。想想那些可憐的JavaScript程序員吧,他們甚至連一個可以遵照的標準都沒有,一個瀏覽器一個樣,甚至同一個瀏覽器的不同版本也差別很大,程序員不得不為每一種瀏覽器的每一個版本分別寫不同的代碼。
除了char
型之外,整型還包括short int
(或者簡寫為short
)、int
、long int
(或者簡寫為long
)、long long int
(或者簡寫為long long
)等幾種[25],這些類型都可以加上signed
或unsigned
關鍵字表示有符號或無符號數。其實,對於有符號數在計算機中的表示是Sign and Magnitude、1's Complement還是2's Complement,C標準也沒有明確規定,也是Implementation Defined。大多數體繫結構都採用2's Complement表示法,x86平台也是如此,從現在開始我們只討論2's Complement表示法的情況。還有一點要注意,除了char
型以外的這些類型如果不明確寫signed
或unsigned
關鍵字都表示signed
,這一點是C標準明確規定的,不是Implementation Defined。
除了char
型在C標準中明確規定占一個位元組之外,其它整型占幾個位元組都是Implementation Defined。通常的編譯器實現遵守ILP32或LP64規範,如下表所示。
ILP32這個縮寫的意思是int
(I)、long
(L)和指針(P)類型都占32位,通常32位計算機的C編譯器採用這種規範,x86平台的gcc
也是如此。LP64是指long
(L)和指針占64位,通常64位計算機的C編譯器採用這種規範。指針類型的長度總是和計算機的位數一致,至于什麼是計算機的位數,指針又是一種什麼樣的類型,我們到第 17 章 計算機體繫結構基礎和第 23 章 指針再分別詳細解釋。從現在開始本書做以下約定:在以後的陳述中,預設平台是x86/Linux/gcc,遵循ILP32,並且char
是有符號的,我不會每次都加以說明,但說到其它平台時我會明確指出是什麼平台。
在第 2 節 “常量”講過C語言的常量有整數常量、字元常量、枚舉常量和浮點數常量四種,其實字元常量和枚舉常量的類型都是int
型,因此前三種常量的類型都屬於整型。整數常量有很多種,不全是int
型的,下面我們詳細討論整數常量。
以前我們只用到十進制的整數常量,其實在C語言中也可以用八進制和十六進制的整數常量[26]。八進制整數常量以0開頭,後面的數字只能是0~7,例如022,因此十進制的整數常量就不能以0開頭了,否則無法和八進制區分。十六進制整數常量以0x或0X開頭,後面的數字可以是0~9、a~f和A~F。在第 6 節 “字元類型與字元編碼”講過一種轉義序列,以\或\x加八進制或十六進制數字表示,這種表示方式相當於把八進制和十六進制整數常量開頭的0替換成\了。
整數常量還可以在末尾加u或U表示“unsigned”,加l或L表示“long”,加ll或LL表示“long long”,例如0x1234U,98765ULL等。但事實上u、l、ll這幾種尾碼和上面講的unsigned
、long
、long long
關鍵字並不是一一對應的。這個對應關係比較複雜,準確的描述如下表所示(出自[C99]條款6.4.4.1)。
表 15.2. 整數常量的類型
尾碼 | 十進制常量 | 八進制或十六進制常量 |
---|---|---|
無 | int | int |
u或U | unsigned int | unsigned int |
l或L | long int | long int |
既有u或U,又有l或L | unsigned long int | unsigned long int |
ll或LL | long long int | long long int |
既有u或U,又有ll或LL | unsigned long long int | unsigned long long int |
給定一個整數常量,比如1234U,那麼它應該屬於“u或U”這一行的“十進制常量”這一列,這個表格單元中列了三種類型unsigned int
、unsigned long int
、unsigned long long int
,從上到下找出第一個足夠長的類型可以表示1234這個數,那麼它就是這個整數常量的類型,如果int
是32位的那麼unsigned int
就可以表示。
再比如0xffff0000,應該屬於第一行“無”的第二列“八進制或十六進制常量”,這一列有六種類型int
、unsigned int
、long int
、unsigned long int
、long long int
、unsigned long long int
,第一個類型int
表示不了0xffff0000這麼大的數,我們寫這個十六進制常量是要表示一個正數,而它的MSB(第31位)是1,如果按有符號int
類型來解釋就成了負數了,第二個類型unsigned int
可以表示這個數,所以這個十六進制常量的類型應該算unsigned int
。所以請注意,0x7fffffff和0xffff0000這兩個常量雖然看起來差不多,但前者是int
型,而後者是unsigned int
型。
講一個有意思的問題。我們知道x86平台上int
的取值範圍是-2147483648~2147483647,那麼用printf("%d\n", -2147483648);
打印int
類型的下界有沒有問題呢?如果用gcc main.c -std=c99
編譯會有警告信息:warning: format ‘%d’ expects type ‘int’, but argument 2 has type ‘long long int’
。這是因為,雖然-2147483648這個數值能夠用int
型表示,但在C語言中卻沒法寫出對應這個數值的int
型常量,C編譯器會把它當成一個整數常量2147483648和一個負號運算符組成的表達式,而整數常量2147483648已經超過了int
型的取值範圍,在x86平台上int
和long
的取值範圍相同,所以這個常量也超過了long
型的取值範圍,根據上表第一行“無”的第一列十進制常量
,這個整數常量應該算long long
型的,前面再加個負號組成的表達式仍然是long long
型,而printf
的%d
轉換說明要求後面的參數是int
型,所以編譯器報警告。之所以編譯命令要加-std=c99
選項是因為C99以前對於整數常量的類型規定和上表有一些出入,即使不加這個選項也會報警告,但警告信息不准確,讀者可以試試。如果改成printf("%d\n", -2147483647-1);
編譯器就不會報警告了,-號運算符的兩個操作數-2147483647和1都是int
型,計算結果也應該是int
型,並且它的值也沒有超出int
型的取值範圍;或者改成printf("%lld\n", -2147483648);
也可以,轉換說明%lld
告訴printf
後面的參數是long long
型,有些轉換說明格式目前還沒講到,詳見第 2.9 節 “格式化I/O函數”。
怎麼樣,整數常量沒有你原來想的那麼簡單吧。再看一個不簡單的問題。long long i = 1234567890 * 1234567890;
編譯時會有警告信息:warning: integer overflow in expression
。1234567890是int
型,兩個int
型相乘的表達式仍然是int
型,而乘積已經超過int
型的取值範圍了,因此提示計算結果溢出。如果改成long long i = 1234567890LL * 1234567890;
,其中一個常量是long long
型,另一個常量也會先轉換成long long
型再做乘法運算,兩數相乘的表達式也是long long
型,編譯器就不會報警告了。有關類型轉換的規則將在第 3 節 “類型轉換”詳細介紹。
[25] 我們在第 4 節 “結構體和聯合體”還要介紹一種特殊的整型--Bit-field。
[26] 有些編譯器(比如gcc
)也支持二進制的整數常量,以0b或0B開頭,比如0b0001111,但二進制的整數常量從未進入C標準,只是某些編譯器的擴展,所以不建議使用,由於二進制和八進制、十六進制的對應關係非常明顯,用八進制或十六進制常量完全可以代替使用二進制常量。