淺談 C 語言的型態轉換

前言

最近公司專案導入 static code check tool 並且採用較嚴格的 coding rule 來檢查,團隊內不乏許多資深的工程師,但檢查出相當多型態轉換的問題,促使我想寫下這篇文章釐清 C 語言型態轉換的規則及可能的問題。

可能的安全問題

C語言包含不同的型態,不同型態之間做運算就會適用不同規則,這也是 C 語言型態轉換困難的地方,進行型態轉換,可能會發生以下問題是值得我們留意的

  • loss of value: 如果今天我們將 integer 轉換到 char 型態,由於 int 型態大小遠大於 char,就會造成這種狀況
  • loss of sign: 如果我們將 unsigned int 與 signed int 進行運算,原本 signed int 被迫轉換成 unsigned int,就會造成這種狀況
  • loss of precision: 從 floating 型態轉換到 integer 型態
  • loss of layout: 型態轉換發生在從 pointer to struct 到 pointer to char

型態轉換又分為兩種:

  1. explicit conversion: 就是大家會在教科書上看到的 cast,開發者可以強制轉換任何資料的 data type。
  2. implicit conversion: 這是大部分開發者都會疏忽的,由 compiler 來幫你轉換型態,以下會描述什麼情況下會發生 implicit conversion,我們並不需要牢記這些規則,但必須要在開發時候隨時留意。

implicit conversion

以下是以 C90 標準來介紹。

integral promotion

在一個 expression 中可以使用 integer 的地方,你也可以使用 enum,或 unsigned or signed 的 char、short、integer,但在這種狀況下就會發生 intergral promotion ,compiler 強制將這些資料轉換成 int(signed int) 或是 unsigned int。如果 integer 就可以表示上述這些類型,則轉換為 int 類型 ; 否則就用 unsigned int 來表示。這樣的轉換其實是為了符合 CPU 實際運作,我節錄以下來自 wikipedia 頁面的解釋:

表達式的整型運算要在CPU的相應運算器件內執行,CPU內整型運算器(ALU)的操作數的字節長度一般就是int的字節長度,同時也是CPU的通用暫存器的長度。因此,即使兩個char類型的相加,在CPU執行時實際上也要先轉換為CPU內整型操作數的標準長度。通用CPU(general-purpose CPU)是難以直接實現兩個8比特字節直接相加運算(雖然機器指令中可能有這種字節相加指令)。所以,表達式中各種長度可能小於int長度的整型值,都必須先轉換為int或unsigned int,然後才能送入CPU去執行運算。

unsigned char uc1 = '1';
int si1 = 1;
int si2 = si1 + uc1;    // uc1 會被 integral promotion 成 int

Usual arithmetic conversions

兩個不同型態的 data 進行運算(運算元 a + 運算元 b),Compiler 會執行 usual arithmetic conversion 以便讓 data 變為相同的 datatype 再進行運算,規則如下且依照順序來判斷:

  1. 其中一個運算元是 long double,另外一個運算元會被轉成 long double。
  2. 其中一個運算元是 double,另外一個運算元會被轉成 double。
  3. 其中一個運算元是 float,另外一個運算元會被轉成 float。
  4. 如果兩個運算元都是 integer 類 (integral promotion 已經發生在運算元上所以兩個運算元都是 integer),下列規則有需要的話就會適用:
    1. 其中一個運算元是 unsigned long int,另外一個運算元會被轉成 unsigned long int。
    2. 其中一個運算元是 long int,另外一個運算元是 unsigned int。如果 long int 可以代表 unsigned int,則都轉成 long int;否則都轉成 unsigned long int。
    3. 其中一個運算元是 long int,另外一個運算元會被轉成 long int。
    4. 其中一個運算元是 unsigned int,另外一個運算元會被轉成 unsigned int。
    5. 兩個運算元都轉為 int。
unsigned int ui1 = 0;
unsigned char uc1 = '1';
/* 
 * uc1 先經過 integral promotion 變成 signed int,
 * signed int 的 uc1 又會透過 usual arithmetic conversions 轉換為 unsigned int
 */
unsigned int ui2 = ui1 + uc1;

Assignment

如果你 assign 值的時候等號兩邊型態不相等,會先把右邊的型態轉換成跟左邊一樣,再 assign 給左邊變數。

signed int si1 = 0;
unsigned int ui1 = si1  // si1 會被轉換為 unsigned int

最後用一個例子來展示以上三種 implicit conversion 同時發生的情況:

unsigned int ui1 = 0;
unsigned char uc1 = '0';
/*
 * uc1 透過 integral promotion 成為 signed int.
 * 再透過 usual arithmetic conversion 成為 unsigned int.
 * 最後由於 assignment 會被轉成 signed int 然後 assign 給 si1.
 */
signed int si1 = ui1 + uc1;