3. 類型轉換

如果有人問C語法規則中最複雜的是哪一部分,我一定會說是類型轉換。從上面兩節可以看出,有符號、無符號整數和浮點數加起來有那麼多種類型,每兩種類型之間都要定義一個轉換規則,轉換規則的數量自然很龐大,更何況由於各種體繫結構對於整數和浮點數的實現很不相同,很多類型轉換的情況都是C標準未做明確規定的陰暗角落。雖然我們寫代碼時不會故意去觸碰這些陰暗角落,但有時候會不小心犯錯,所以瞭解一些未明確規定的情況還是有必要的,可以在出錯時更容易分析錯誤原因。本節分成幾小節,首先介紹哪些情況下會發生類型轉換,會把什麼類型轉成什麼類型,然後介紹編譯器如何處理這樣的類型轉換。

3.1. Integer Promotion

在一個表達式中,凡是可以使用intunsigned int類型做右值的地方也都可以使用有符號或無符號的char型、short型和Bit-field。如果原始類型的取值範圍都能用int型表示,則其類型被提升為int,如果原始類型的取值範圍用int型表示不了,則提升為unsigned int型,這稱為Integer Promotion。做Integer Promotion只影響上述幾種類型的值,對其它類型無影響。C99規定Integer Promotion適用於以下幾種情況:

1、如果一個函數的形參類型未知,例如使用了Old Style C風格的函數聲明(詳見第 2 節 “自定義函數”),或者函數的參數列表中有...,那麼調用函數時要對相應的實參做Integer Promotion,此外,相應的實參如果是float型的也要被提升為double型,這條規則稱為Default Argument Promotion。我們知道printf的參數列表中有...,除了第一個形參之外,其它形參的類型都是未知的,比如有這樣的代碼:

char ch = 'A';
printf("%c", ch);

ch要被提升為int型之後再傳給printf

2、算術運算中的類型轉換。有符號或無符號的char型、short型和Bit-field在做算術運算之前首先要做Integer Promotion,然後才能參與計算。例如:

unsigned char c1 = 255, c2 = 2;
int n = c1 + c2;

計算表達式c1 + c2的過程其實是先把c1c2提升為int型然後再相加(unsigned char的取值範圍是0~255,完全可以用int表示,所以提升為int就可以了,不需要提升為unsigned int),整個表達式的值也是int型,最後的結果是257。假如沒有這個提升的過程,c1 + c2就溢出了,溢出會得到什麼結果是Undefined,在大多數平台上會把進位截掉,得到的結果應該是1。

除了+號之外還有哪些運算符在計算之前需要做Integer Promotion呢?我們在下一小節先介紹Usual Arithmetic Conversion規則,然後再解答這個問題。

3.2. Usual Arithmetic Conversion

兩個算術類型的操作數做算術運算,比如a + b,如果兩邊操作數的類型不同,編譯器會自動做類型轉換,使兩邊類型相同之後才做運算,這稱為Usual Arithmetic Conversion。轉換規則如下:

  1. 如果有一邊的類型是long double,則把另一邊也轉成long double

  2. 否則,如果有一邊的類型是double,則把另一邊也轉成double

  3. 否則,如果有一邊的類型是float,則把另一邊也轉成float

  4. 否則,兩邊應該都是整型,首先按上一小節講過的規則對ab做Integer Promotion,然後如果類型仍不相同,則需要繼續轉換。首先我們規定charshortintlonglong long的轉換級別(Integer Conversion Rank)一個比一個高,同一類型的有符號和無符號數具有相同的Rank。轉換規則如下:

    1. 如果兩邊都是有符號數,或者都是無符號數,那麼較低Rank的類型轉換成較高Rank的類型。例如unsigned intunsigned long做算術運算時都轉成unsigned long

    2. 否則,如果一邊是無符號數另一邊是有符號數,無符號數的Rank不低於有符號數的Rank,則把有符號數轉成另一邊的無符號類型。例如unsigned longint做算術運算時都轉成unsigned longunsigned longlong做算術運算時也都轉成unsigned long

    3. 剩下的情況是:一邊有符號另一邊無符號,並且無符號數的Rank低於有符號數的Rank。這時又分為兩種情況,如果這個有符號數類型能夠覆蓋這個無符號數類型的取值範圍,則把無符號數轉成另一邊的有符號類型。例如遵循LP64的平台上unsigned intlong在做算術運算時都轉成long

    4. 否則,也就是這個有符號數類型不足以覆蓋這個無符號數類型的取值範圍,則把兩邊都轉成有符號數的Rank對應的無符號類型。例如在遵循ILP32的平台上unsigned intlong在做算術運算時都轉成unsigned long

可見有符號和無符號整數的轉換規則是十分複雜的,雖然這是有明確規定的,不屬於陰暗角落,但為了程序的可讀性不應該依賴這些規則來寫代碼。我講這些規則,不是為了讓你用,而是為了讓你瞭解有符號數和無符號數混用會非常麻煩,從而避免觸及這些規則,並且在程序出錯時記得往這上面找原因。所以這些規則不需要牢記,但要知道有這麼回事,以便在用到的時候能找到我書上的這一段。

到目前為止我們學過的+ - * / % > < >= <= == !=運算符都需要做Usual Arithmetic Conversion,因為都要求兩邊操作數的類型一致,在下一章會介紹幾種新的運算符也需要做Usual Arithmetic Conversion。單目運算符+ - ~只有一個操作數,移位運算符<< >>兩邊的操作數類型不要求一致,這些運算不需要做Usual Arithmetic Conversion,但也需要做Integer Promotion,運算符~ << >>將在下一章介紹。

3.3. 由賦值產生的類型轉換

如果賦值或初始化時等號兩邊的類型不相同,則編譯器會把等號右邊的類型轉換成等號左邊的類型再做賦值。例如int c = 3.14;,編譯器會把右邊的double型轉成int型再賦給變數c

我們知道,函數調用傳參的過程相當於定義形參並且用實參對其做初始化,函數返回的過程相當於定義一個臨時變數並且用return的表達式對其做初始化,所以由賦值產生的類型轉換也適用於這兩種情況。例如一個函數的原型是int foo(int, int);,則調用foo(3.1, 4.2)時會自動把兩個double型的實參轉成int型賦給形參,如果這個函數定義中有返回語句return 1.2;,則返回值1.2會自動轉成int型再返回。

在函數調用和返回過程中發生的類型轉換往往容易被忽視,因為函數原型和函數調用並沒有寫在一起。例如char c = getchar();,看到這一句往往會想當然地認為getchar的返回值是char型,而事實上getchar的返回值是int型,這樣賦值會引起類型轉換,可能產生Bug,我們在第 2.5 節 “以位元組為單位的I/O函數”詳細討論這個問題。

3.4. 強制類型轉換

以上三種情況通稱為隱式類型轉換(Implicit Conversion,或者叫Coercion),編譯器根據它自己的一套規則將一種類型自動轉換成另一種類型。除此之外,程序員也可以通過類型轉換運算符(Cast Operator)自己規定某個表達式要轉換成何種類型,這稱為顯式類型轉換(Explicit Conversion)或強制類型轉換(Type Cast)。例如計算表達式(double)3 + i,首先將整數3強制轉換成double型(值為3.0),然後和整型變數i相加,這時適用Usual Arithmetic Conversion規則,首先把i也轉成double型,然後兩者相加,最後整個表達式也是double型的。這裡的(double)就是一個類型轉換運算符,這種運算符由一個類型名套()括號組成,屬於單目運算符,後面的3是這個運算符的操作數。注意操作數的類型必須是標量類型,轉換之後的類型必須是標量類型或者void型。

3.5. 編譯器如何處理類型轉換

以上幾小節介紹了哪些情況下會發生類型轉換,並且明確了每種情況下會把什麼類型轉成什麼類型,本節介紹編譯器如何處理任意兩種類型之間的轉換。現在要把一個M位的類型(值為X)轉換成一個N位的類型,所有可能的情況如下表所示。

表 15.3. 如何做類型轉換

待轉換的類型M > N的情況M == N的情況M < N的情況
signed integer to signed integer如果X在目標類型的取值範圍內則值不變,否則Implementation-defined值不變值不變
unsigned integer to signed integer如果X在目標類型的取值範圍內則值不變,否則Implementation-defined如果X在目標類型的取值範圍內則值不變,否則Implementation-defined值不變
signed integer to unsigned integerX % 2NX % 2NX % 2N
unsigned integer to unsigned integerX % 2N值不變值不變
floating-point to signed or unsigned integerTruncate toward Zero,如果X的整數部分超出目標類型的取值範圍則Undefined
signed or unsigned integer to floating-point如果X在目標類型的取值範圍內則值不變,但有可能損失精度,如果X超出目標類型的取值範圍則Undefined
floating-point to floating-point如果X在目標類型的取值範圍內則值不變,但有可能損失精度,如果X超出目標類型的取值範圍則Undefined值不變值不變

注意上表中的“X % 2N”,我想表達的意思是“把X加上或者減去2N的整數倍,使結果落入[0, 2N-1]的範圍內”,當X是負數時運算結果也得是正數,即運算結果和除數同號而不是和被除數同號,這不同於C語言%運算的定義。寫程序時不要故意用上表中的規則,尤其不要觸碰Implementation-defined和Undefined的情況,但程序出錯時可以借助上表分析錯誤原因。

下面舉幾個例子說明上表的用法。比如把double型轉換成short型,對應表中的“floating-point to signed or unsigned integer”,如果原值在(-32769.0, 32768.0)之間則截掉小數部分得到轉換結果,否則產生溢出,結果是Undefined,例如對於short s = 32768.4;這個語句gcc會報警告。

比如把int型轉換成unsigned short型,對應表中的“signed integer to unsigned integer”,如果原值是正的,則把它除以216取模,其實就是取它的低16位,如果原值是負的,則加上216的整數倍,使結果落在[0, 65535]之間。

比如把int類型轉換成short類型,對應表中的“signed integer to signed integer”,如果原值在[-32768, 32767]之間則值不變,否則產生溢出,結果是Implementation-defined,例如對於short s = -32769;這個語句gcc會報警告。

最後一個例子,把short型轉換成int型,對應表中的“signed integer to signed integer”,轉換之後應該值不變。那怎麼維持值不變呢?是不是在高位補16個0就行了呢?如果原值是-1,十六進製表示就是ffff,要轉成int型的-1需要變成ffffffff,因此需要在高位補16個1而不是16個0。換句話說,要維持值不變,在高位補1還是補0取決於原來的符號位,這稱為符號擴展(Sign Extension)