"C語言結(jié)合了匯編的強(qiáng)大功能和可移植性" -- 無名氏,暗指比爾.薩克。
可移植代碼的好處是有目共睹的。這一節(jié)將闡述一些編寫可移植代碼的指導(dǎo)原則。這里"可移植的"是指一個(gè)源碼文件能夠在不同機(jī)器上被編譯和執(zhí)行,其 前提僅僅是在不同平臺上可能包含不同的頭文件,使用不同的編譯器開關(guān)選項(xiàng)罷了。頭文件包含的#define和typedef可能因機(jī)器而異。一般 來說,一個(gè)新"機(jī)器"是指一種不同的硬件,一種不同的操作系統(tǒng),一個(gè)不同的編譯器,或者是這些的任意組合。參考1包含了很多關(guān)于風(fēng)格和可移植 性方面的有用信息。下面是一個(gè)隱患列表,當(dāng)你設(shè)計(jì)可移植代碼時(shí)應(yīng)該考慮避免這些隱患:
編寫可移植的代碼。只有當(dāng)被證明是必要的情況下才考慮優(yōu)化的細(xì)節(jié)。優(yōu)化后的代碼往往是模糊不清、難以理解的。在一臺機(jī)器上經(jīng)過優(yōu)化后的代碼,在其他機(jī)器上 可能變得更加糟糕。將采用的性能優(yōu)化手段記錄下來并盡可能多地本地化。文檔應(yīng)該解釋這些手段的工作原理以及引入它們的原因(例如:"循環(huán)執(zhí)行了無 數(shù)次")
要意識到很多東西天生就是不可移植的。比如處理類似程序狀態(tài)字這樣的特定硬件寄存器的代碼,以及被設(shè)計(jì)用于支持某特定硬件部件的代碼,諸如匯編器以及 I/O驅(qū)動(dòng)。即使在這種情況下,許多例程和數(shù)據(jù)仍然可以被設(shè)計(jì)成機(jī)器無關(guān)的。
組織源文件時(shí)將機(jī)器無關(guān)與機(jī)器相關(guān)的代碼分別放在不同文件中。之后如果這個(gè)程序需要被移植到一個(gè)新機(jī)器上時(shí),我們就可以很容易判斷出來哪些需要被改變。為 一些文件的頭文件中機(jī)器依賴相關(guān)的代碼添加注釋。
任何"實(shí)現(xiàn)相關(guān)"的行為都應(yīng)該作為機(jī)器(編譯器)依賴對待。假設(shè)編譯器或硬件以一種十分古怪的方式實(shí)現(xiàn)它。
-
注意機(jī)器字長。對象的大小可能不直觀,指針大小也不總是與整型大小相同,也不總是彼此大小相同,或者可相互自由轉(zhuǎn)換。下面的表中列舉了C語言基本類型在不 同機(jī)器和編譯器下的大小(以bit為單位)。
type pdp11 VAX/11 68000 Cray-2 Unisys Harris 80386 series family 1100 H800 char 8 8 8 8 9 8 8 short 16 16 8/16 64(32) 18 24 8/16 int 16 32 16/32 64(32) 36 24 16/32 long 32 32 32 64 36 48 32 char* 16 32 32 64 72 24 16/32/48 int* 16 32 32 64(24) 72 24 16/32/48 int(*)() 16 32 32 64 576 24 16/32/48
有些機(jī)器針對某一類型可能有不止一個(gè)大小。其類型大小取決于編譯器和不同的編譯期標(biāo)志。下面表展示了大多數(shù)系統(tǒng)的"安全"類型大小。無符號與帶符 號數(shù)具有相同的大小(單位:bit)。
Type Minimum No Smaller
# Bits Than
char 8
short 16 char
int 16 short
long 32 int
float 24
double 38 float
any * 14
char * 15 any *
void * 15 any *
void類型可以保證有足夠位精度來表示一個(gè)指向任意數(shù)據(jù)對象的指針。void()()類型可以保證表示一個(gè)指向任意函數(shù)的指針。當(dāng)你需要通用指針時(shí) 可以使用這些類型(在一些舊的編譯器里,分別用char和char()()表示)。確保在使用這些指針類型之前將其轉(zhuǎn)換回正確的類型。
-
即使說一個(gè)int和一個(gè)char類型大小相同,它們?nèi)钥赡芫哂胁煌母袷健@纾旅胬釉谝恍﹕izeof(int)等于 sizeof(char)的機(jī)器上可能失敗。其原因在與free函數(shù)期望一個(gè)char,但卻傳入了一個(gè)int。
int *p = (int *) malloc (sizeof(int)); free (p);
注意,一個(gè)對象的大小不能保證這個(gè)對象的精度。Cray-2可能使用64位來存儲一個(gè)整型,但一個(gè)長整型轉(zhuǎn)換為一個(gè)整型并且再轉(zhuǎn)換回長整型后可能會被截?cái)?為32位。
整型常量0可以強(qiáng)制轉(zhuǎn)型為任何指針類型。轉(zhuǎn)換后的指針稱為對應(yīng)那個(gè)類型的空指針,并且與那個(gè)類型的其他指針不同。空指針比較總是與常量0相當(dāng)。空指針不應(yīng) 該與一個(gè)值為0的變量比較。空指針不總是使用全0的位模式表示。兩個(gè)不同類型的空指針有些時(shí)候可能不同。某個(gè)類型的空指針被強(qiáng)制轉(zhuǎn)換為另外一個(gè)類 型的指針,其結(jié)果是該指針轉(zhuǎn)換為第二個(gè)類型的空指針。
-
對于ANSI編譯器,當(dāng)兩個(gè)類型相同的指針訪問同一塊存儲區(qū)時(shí),則它們比較是相等的。當(dāng)一個(gè)非0整型常量被轉(zhuǎn)換為指針類型時(shí),它們可能與其他指針相等。對 于非ANSI編譯器,訪問同一塊存儲區(qū)的兩個(gè)指針比較可能并不相同。例如,下面兩個(gè)指針比較可能相等或不相等,并且他們可能或可能沒有訪問同一塊 存儲區(qū)域。
((int *) 2 ) ((int *) 3 )
如果你需要'magic'指針而不是NULL,要么分配一些內(nèi)存,要么將指針視為機(jī)器相關(guān)的。
extern int x_int_dummy; /* in x.c */
#define X_FAIL (NULL)
#define X_BUSY (&x_int_dummy)
#define X_FAIL (NULL)
#define X_BUSY MD_PTR1 /* MD_PTR1 from "machdep.h" */
浮點(diǎn)數(shù)字既包含精度也包含范圍。這些都是數(shù)據(jù)對象大小無關(guān)的。但是,一個(gè)32位浮點(diǎn)數(shù)在不同機(jī)器上溢出時(shí)的值有所不同。同時(shí),4.9乘以5.1在不同的機(jī) 器上可能產(chǎn)生兩個(gè)不同的數(shù)字。在圓整(rounding)和截?cái)喾矫娴牟町悓⒔o出特別不同的答案。
在一些機(jī)器上,一個(gè)雙精度浮點(diǎn)數(shù)在精度或范圍方面可能比一個(gè)單精度浮點(diǎn)數(shù)還要低。
在一些機(jī)器上,double值的前半部分可能是一個(gè)具有相同值的float類型。千萬不要依賴于此。
提防帶符號字符。例如,在某些VAX系統(tǒng)上,用在表達(dá)式中的字符是符號擴(kuò)展的,但在其他一些機(jī)器上并非如此。對有符號和無符號有依賴的代碼是不可移植的。 例如,如果假設(shè)c是正值,arrayc在c為有符號且為負(fù)值時(shí)將無法正常工作。如果你一定要假設(shè)signed或unsigned字符的話,請 用SIGNED或UNSIGNED為其加上注釋。無符號字符的行為可由unsigned char保證。
避免對ASCII做假設(shè)。如果你必須假設(shè),那么請將其記錄下來并本地化。請記住字符很可能用不止8位表示。
大多數(shù)機(jī)器采用2的補(bǔ)碼表示數(shù),但我們在代碼中不應(yīng)該利用這一特點(diǎn)。使用等價(jià)移位操作替代算術(shù)運(yùn)算的優(yōu)化尤其值得懷疑。如果必須這么做,那么機(jī)器相關(guān)的代 碼應(yīng)該用#ifdef定義,或者操作應(yīng)該在#ifdef宏判定下執(zhí)行。你應(yīng)該衡量一下使用這種難以理解的代碼所節(jié)省的時(shí)間與做代碼移植時(shí)找bug 所花費(fèi)的時(shí)間相比孰多孰少。
一般情況下,如果字長或值范圍非常重要,應(yīng)該使用typedef定義具有特定大小的類型。大型程序應(yīng)該具有一個(gè)統(tǒng)一的頭文件用于提供通用的、大小 (size)敏感的類型的typedef定義,這樣更加便于修改以及在緊急修復(fù)時(shí)查找大小敏感的代碼。無符號類型比有符號整型更加編譯器無關(guān)。如 果既可以用16bit也可以用32bit標(biāo)識一個(gè)簡單for循環(huán)的計(jì)數(shù)器,我們應(yīng)該使用int。因?yàn)閷τ诋?dāng)前機(jī)器來說,通過整型可以獲取更高效 (自然)的存儲單元。
數(shù)據(jù)對齊也很重要。例如,在不同的機(jī)器上,一個(gè)四字節(jié)的整型數(shù)的可能以任意地址作為起始地址,也可能只允許以偶數(shù)地址作為起始地址,或者只能以4的整數(shù)倍 的地址作為起始地址。因此,一個(gè)特定的結(jié)構(gòu)體的各個(gè)元素在不同的機(jī)器上的偏移量有不同,即使給定的這些元素在所有機(jī)器上的大小相同。事實(shí)上,一個(gè) 包含一個(gè)32位指針和一個(gè)8位字符的結(jié)構(gòu)提在三個(gè)不同的機(jī)器上可能有三個(gè)不同的大小。作為一個(gè)推論,對象指針可能無法自由互換;通過一個(gè)指向起始 地址為奇數(shù)地址長度為4個(gè)字節(jié)的指針保存一個(gè)整型數(shù)有時(shí)可以正常工作,但有時(shí)則會導(dǎo)致產(chǎn)生core,有些時(shí)候靜悄悄地失敗了(在這個(gè)過程中會破壞 其他數(shù)據(jù))。在那些不按字節(jié)尋址的機(jī)器上,字符指針更是"事故高發(fā)地區(qū)"。對齊考慮以及加載器的特殊性使得很容易輕率地認(rèn)為兩個(gè)連續(xù)聲明的變量在 內(nèi)存中也是連在一起的,或者某個(gè)類型的變量已經(jīng)被適當(dāng)對齊并可以用作其他類型變量使用了。
在一些機(jī)器上,諸如VAX(小端),一個(gè)字的字節(jié)隨著地址的增加,其重要性提高;而另外一些機(jī)器上,諸如68000(大端),隨著地址的增加,其重要性降 低。字或更大數(shù)據(jù)對象(諸如一個(gè)雙精度字)的字節(jié)順序可能并不相同。因此,任何依賴對象內(nèi)從左到右方向位模式的代碼都值得特別細(xì)致的審查。只有當(dāng) 結(jié)構(gòu)體中兩個(gè)不同的位字段不被連接以及不被當(dāng)作一個(gè)單元時(shí),這些位字段才具備可移植性。事實(shí)上,連接任意兩個(gè)變量都是不可移植的行為。
結(jié)構(gòu)體中有一些未使用的空洞。猜想聯(lián)合體用于類型欺騙。尤其是,一個(gè)值不應(yīng)該在存儲時(shí)使用一個(gè)類型,而在讀取時(shí)使用另外一種類型。對聯(lián)合體來說,一個(gè)顯式 的標(biāo)簽(tag)字段可能會很有用。
不同的編譯器在返回結(jié)構(gòu)體時(shí)使用不同的約定。這就會導(dǎo)致代碼在接受從不同編譯器編譯的庫代碼中返回的結(jié)構(gòu)體值時(shí)會出現(xiàn)錯(cuò)誤。結(jié)構(gòu)體指針不是問題。
-
不要假設(shè)參數(shù)傳遞機(jī)制。特別是指針大小以及參數(shù)求值順序,大小等。例如,下面的代碼就不具備可移植性。
c = foo (getchar(), getchar()); char foo (c1, c2, c3) char c1, c2, c3; { char bar = *(&c1 + 1); return (bar); /* often won't return c2 */ }
上面的例子有諸多問題。棧可能向上增長,也可能向下增長(事實(shí)上,甚至都不需要一個(gè)棧)。參數(shù)在傳入時(shí)可能被擴(kuò)大,例如一個(gè)char可能以int型被傳 入。參數(shù)可能以從左到右,從右到左,或以任意順序壓入棧,或直接放在寄存器中(根本無需壓棧)。參數(shù)求值的順序也可能與壓棧的次序有所不同。一個(gè) 編譯器可能使用多種(不兼容的)調(diào)用約定。
在某些機(jī)器上,空字符指針((char *)0)常被當(dāng)作指向空字符串的指針對待。不要依賴于此。
-
不要修改字符串常量。下面就是一個(gè)臭名昭著的例子
s = "/dev/tty??"; strcpy (&s[8], ttychars);
地址空間可能有空洞。簡單計(jì)算一個(gè)數(shù)組中未分配空間的元素(在數(shù)組實(shí)際存儲區(qū)域之前或之后)的地址可能會導(dǎo)致程序崩潰。如果這個(gè)地址被用于比較,有時(shí)程序 可以運(yùn)行,但會破壞數(shù)據(jù),報(bào)錯(cuò),或陷入死循環(huán)。在ANSI C中,指向一個(gè)對象數(shù)組的指針指向數(shù)組結(jié)尾后的第一個(gè)元素是合法的,這在一些老編譯器上通常是安全的。不過這個(gè)"在外邊"不可以被解引用。
只有==和!=比較可用于某給定類型的所有指針。當(dāng)兩個(gè)指針指向同一個(gè)數(shù)組內(nèi)的元素(或數(shù)組后第一個(gè)元素)時(shí),使用<<、<=、& gt;或>=對兩個(gè)指針進(jìn)行比較是可移植的。同樣,僅僅對指向同一個(gè)數(shù)組內(nèi)的元素(或數(shù)組后第一個(gè)元素)的兩個(gè)指針使用算術(shù)操作符才是可移 植的。
字長(word size)也影響移位和掩碼。下面代碼在一些68000機(jī)器上只會將一個(gè)整型數(shù)的最右三個(gè)位清0,而在其他機(jī)器上它還會將高地址的兩個(gè)字節(jié)清零。x &= 0177770 使用 x &= ~07可以在所有機(jī)器上正常工作。位字段(bitfield)沒有這些問題。
-
表達(dá)式內(nèi)的副作用可能導(dǎo)致代碼語義是編譯器相關(guān)的,因?yàn)樵诖蠖鄶?shù)情況下C語言的求值順序是沒有顯式定義的。下面是一個(gè)臭名昭著的例子:
a[i] = b[i++];
在上面的例子中,我們只知道b的下標(biāo)值沒有被增加。a的下標(biāo)i值可能是自增后的值也可能是自增前的值。
struct bar_t { struct bar_t *next; } bar; bar->next = bar = tmp;
在第二個(gè)例子中,bar->next的地址很可能在bar被賦值之前被計(jì)算使用。
bar = bar->next = tmp;
第三個(gè)例子中,bar可能在bar->next之前被賦值。雖然這可能有悖于"賦值從右到左處理"的規(guī)則,但這確是一個(gè)合法的解析。考慮下 面的例子:
long i;
short a[N];
i = old
i = a[i] = new;
賦給i的值必須是一個(gè)按照從右到左的處理順序進(jìn)行賦值處理后的值。但是i可能在ai被賦值前而被賦值為"(long) (short)new"。不同編譯器作法不同。
質(zhì)疑代碼中出現(xiàn)的數(shù)值(“魔數(shù)”)。
-
避免使用預(yù)處理器技巧。一些諸如使用/ /粘和字符串以及依賴參數(shù)字符串展開的宏會破壞代碼可靠性。
#define FOO(string) (printf("string = %s",(string))) ... FOO(filename);
只是在有些時(shí)候會擴(kuò)展為
(printf("filename = %s",(filename)))
小心。詭異的預(yù)處理器在一些機(jī)器上可能導(dǎo)致宏異常中斷。下面是一個(gè)宏的兩種不同實(shí)現(xiàn)版本:
#define LOOKUP(chr) (a['c'+(chr)]) /* Works as intended. */
#define LOOKUP(c) (a['c'+(c)]) /* Sometimes breaks. */
第二個(gè)版本的LOOKUP可能以兩種不同的方式擴(kuò)展,并且會導(dǎo)致代碼異常中斷。
熟悉現(xiàn)有的庫函數(shù)和定義(但不用太熟悉。與其外部接口相反,庫基礎(chǔ)設(shè)施的內(nèi)部細(xì)節(jié)常會改變并且沒有警告,這些細(xì)節(jié)常常也是不可移植的)。你不應(yīng)該再自己重 新編寫字符串比較例程、終端控制例程或?yàn)橄到y(tǒng)結(jié)構(gòu)編寫你自己的定義。自己動(dòng)手實(shí)現(xiàn)既浪費(fèi)你的時(shí)間,又使得你的代碼可讀性變差,因?yàn)榱硗庖粋€(gè)讀者需 要知道你是否在新的實(shí)現(xiàn)中做了什么特殊的事情,并嘗試證實(shí)它們的存在。同時(shí)這樣做會使得你無法充分利用一些輔助的微代碼或其他有助于提高系統(tǒng)例程 性能的方法。更進(jìn)一步,它將是一個(gè)bug的高產(chǎn)源頭。如果可能的話,要知道公共庫之間的差異(如ANSI、POSIX等等)。
如果lint可用,請使用lint。這個(gè)工具對于查找代碼中機(jī)器相關(guān)的構(gòu)造、其他不一致性以及順利通過編譯器檢查的程序bug時(shí)具有很高價(jià)值。如果你的編 譯器具備打開警告的開關(guān),請打開它。
質(zhì)疑在代碼塊內(nèi)部的與代碼塊外部switch或goto有關(guān)聯(lián)的標(biāo)簽(Label)。
無論類型在哪里,參數(shù)都應(yīng)該被轉(zhuǎn)換為適當(dāng)?shù)念愋汀.?dāng)NULL用在沒有原型的函數(shù)調(diào)用時(shí),請對NULL進(jìn)行轉(zhuǎn)換。不要讓函數(shù)調(diào)用成為類型欺騙發(fā)生的地方。C 語言的類型提升規(guī)則很是讓人費(fèi)解,所以盡量小心。例如,如果一個(gè)函數(shù)接受一個(gè)32位長的長整型做為參數(shù),但實(shí)際傳入的卻是一個(gè)16位長的整型數(shù), 函數(shù)棧可能會無法對齊,這個(gè)值也可能會被錯(cuò)誤提升。
在混用有符號和無符號值的算術(shù)計(jì)算時(shí)請使用顯式類型轉(zhuǎn)換
應(yīng)該謹(jǐn)慎使用跨程序的goto、longjmp。很多實(shí)現(xiàn)"忘記"恢復(fù)寄存器中的值了。盡可能將關(guān)鍵的值聲明為volatile,或?qū)⑺鼈冏⑨尀?VOLATILE。
一些鏈接器將名字轉(zhuǎn)換為小寫,并且一些鏈接器只識別前六個(gè)字母作為唯一標(biāo)識。在這些系統(tǒng)上程序可能會悄悄地中斷運(yùn)行。
當(dāng)心編譯器擴(kuò)展。如果使用了編譯器擴(kuò)展,請將他們視為機(jī)器依賴并用文檔記錄下來。
通常程序無法在數(shù)據(jù)段執(zhí)行代碼或者無法將數(shù)據(jù)寫入代碼段。即使程序可以這么做,也無法保證這么做是可靠的。