"C語言結(jié)合了匯編的強大功能和可移植性" -- 無名氏托酸,暗指比爾.薩克。
可移植代碼的好處是有目共睹的醉箕。這一節(jié)將闡述一些編寫可移植代碼的指導(dǎo)原則羽德。這里"可移植的"是指一個源碼文件能夠在不同機器上被編譯和執(zhí)行,其 前提僅僅是在不同平臺上可能包含不同的頭文件神郊,使用不同的編譯器開關(guān)選項罷了肴裙。頭文件包含的#define和typedef可能因機器而異。一般 來說涌乳,一個新"機器"是指一種不同的硬件蜻懦,一種不同的操作系統(tǒng),一個不同的編譯器夕晓,或者是這些的任意組合宛乃。參考1包含了很多關(guān)于風(fēng)格和可移植 性方面的有用信息。下面是一個隱患列表蒸辆,當(dāng)你設(shè)計可移植代碼時應(yīng)該考慮避免這些隱患:
編寫可移植的代碼征炼。只有當(dāng)被證明是必要的情況下才考慮優(yōu)化的細節(jié)。優(yōu)化后的代碼往往是模糊不清躬贡、難以理解的谆奥。在一臺機器上經(jīng)過優(yōu)化后的代碼,在其他機器上 可能變得更加糟糕拂玻。將采用的性能優(yōu)化手段記錄下來并盡可能多地本地化酸些。文檔應(yīng)該解釋這些手段的工作原理以及引入它們的原因(例如:"循環(huán)執(zhí)行了無 數(shù)次")
要意識到很多東西天生就是不可移植的。比如處理類似程序狀態(tài)字這樣的特定硬件寄存器的代碼檐蚜,以及被設(shè)計用于支持某特定硬件部件的代碼魄懂,諸如匯編器以及 I/O驅(qū)動。即使在這種情況下闯第,許多例程和數(shù)據(jù)仍然可以被設(shè)計成機器無關(guān)的逢渔。
組織源文件時將機器無關(guān)與機器相關(guān)的代碼分別放在不同文件中。之后如果這個程序需要被移植到一個新機器上時乡括,我們就可以很容易判斷出來哪些需要被改變肃廓。為 一些文件的頭文件中機器依賴相關(guān)的代碼添加注釋智厌。
任何"實現(xiàn)相關(guān)"的行為都應(yīng)該作為機器(編譯器)依賴對待。假設(shè)編譯器或硬件以一種十分古怪的方式實現(xiàn)它盲赊。
-
注意機器字長铣鹏。對象的大小可能不直觀,指針大小也不總是與整型大小相同哀蘑,也不總是彼此大小相同诚卸,或者可相互自由轉(zhuǎn)換。下面的表中列舉了C語言基本類型在不 同機器和編譯器下的大小(以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
有些機器針對某一類型可能有不止一個大小合溺。其類型大小取決于編譯器和不同的編譯期標(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類型可以保證有足夠位精度來表示一個指向任意數(shù)據(jù)對象的指針。void()()類型可以保證表示一個指向任意函數(shù)的指針膛腐。當(dāng)你需要通用指針時 可以使用這些類型(在一些舊的編譯器里睛约,分別用char和char()()表示)。確保在使用這些指針類型之前將其轉(zhuǎn)換回正確的類型哲身。
-
即使說一個int和一個char類型大小相同辩涝,它們?nèi)钥赡芫哂胁煌母袷健@缈碧欤旅胬釉谝恍﹕izeof(int)等于 sizeof(char)的機器上可能失敗怔揩。其原因在與free函數(shù)期望一個char,但卻傳入了一個int脯丝。
int *p = (int *) malloc (sizeof(int)); free (p);
注意商膊,一個對象的大小不能保證這個對象的精度。Cray-2可能使用64位來存儲一個整型巾钉,但一個長整型轉(zhuǎn)換為一個整型并且再轉(zhuǎn)換回長整型后可能會被截斷 為32位翘狱。
整型常量0可以強制轉(zhuǎn)型為任何指針類型。轉(zhuǎn)換后的指針稱為對應(yīng)那個類型的空指針砰苍,并且與那個類型的其他指針不同潦匈。空指針比較總是與常量0相當(dāng)赚导〔缢酰空指針不應(yīng) 該與一個值為0的變量比較『鹁桑空指針不總是使用全0的位模式表示凰锡。兩個不同類型的空指針有些時候可能不同。某個類型的空指針被強制轉(zhuǎn)換為另外一個類 型的指針,其結(jié)果是該指針轉(zhuǎn)換為第二個類型的空指針掂为。
-
對于ANSI編譯器裕膀,當(dāng)兩個類型相同的指針訪問同一塊存儲區(qū)時,則它們比較是相等的勇哗。當(dāng)一個非0整型常量被轉(zhuǎn)換為指針類型時昼扛,它們可能與其他指針相等。對 于非ANSI編譯器欲诺,訪問同一塊存儲區(qū)的兩個指針比較可能并不相同抄谐。例如,下面兩個指針比較可能相等或不相等扰法,并且他們可能或可能沒有訪問同一塊 存儲區(qū)域蛹含。
((int *) 2 ) ((int *) 3 )
如果你需要'magic'指針而不是NULL,要么分配一些內(nèi)存塞颁,要么將指針視為機器相關(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" */
浮點數(shù)字既包含精度也包含范圍。這些都是數(shù)據(jù)對象大小無關(guān)的殴边。但是憎茂,一個32位浮點數(shù)在不同機器上溢出時的值有所不同珍语。同時锤岸,4.9乘以5.1在不同的機 器上可能產(chǎn)生兩個不同的數(shù)字。在圓整(rounding)和截斷方面的差異將給出特別不同的答案板乙。
在一些機器上是偷,一個雙精度浮點數(shù)在精度或范圍方面可能比一個單精度浮點數(shù)還要低。
在一些機器上募逞,double值的前半部分可能是一個具有相同值的float類型蛋铆。千萬不要依賴于此。
提防帶符號字符放接。例如刺啦,在某些VAX系統(tǒng)上,用在表達式中的字符是符號擴展的纠脾,但在其他一些機器上并非如此玛瘸。對有符號和無符號有依賴的代碼是不可移植的。 例如苟蹈,如果假設(shè)c是正值糊渊,arrayc在c為有符號且為負值時將無法正常工作。如果你一定要假設(shè)signed或unsigned字符的話慧脱,請 用SIGNED或UNSIGNED為其加上注釋渺绒。無符號字符的行為可由unsigned char保證。
避免對ASCII做假設(shè)。如果你必須假設(shè)宗兼,那么請將其記錄下來并本地化躏鱼。請記住字符很可能用不止8位表示。
大多數(shù)機器采用2的補碼表示數(shù)殷绍,但我們在代碼中不應(yīng)該利用這一特點挠他。使用等價移位操作替代算術(shù)運算的優(yōu)化尤其值得懷疑。如果必須這么做篡帕,那么機器相關(guān)的代 碼應(yīng)該用#ifdef定義殖侵,或者操作應(yīng)該在#ifdef宏判定下執(zhí)行。你應(yīng)該衡量一下使用這種難以理解的代碼所節(jié)省的時間與做代碼移植時找bug 所花費的時間相比孰多孰少镰烧。
一般情況下拢军,如果字長或值范圍非常重要,應(yīng)該使用typedef定義具有特定大小的類型怔鳖。大型程序應(yīng)該具有一個統(tǒng)一的頭文件用于提供通用的茉唉、大小 (size)敏感的類型的typedef定義,這樣更加便于修改以及在緊急修復(fù)時查找大小敏感的代碼结执。無符號類型比有符號整型更加編譯器無關(guān)度陆。如 果既可以用16bit也可以用32bit標(biāo)識一個簡單for循環(huán)的計數(shù)器,我們應(yīng)該使用int献幔。因為對于當(dāng)前機器來說懂傀,通過整型可以獲取更高效 (自然)的存儲單元。
數(shù)據(jù)對齊也很重要蜡感。例如蹬蚁,在不同的機器上,一個四字節(jié)的整型數(shù)的可能以任意地址作為起始地址郑兴,也可能只允許以偶數(shù)地址作為起始地址犀斋,或者只能以4的整數(shù)倍 的地址作為起始地址。因此情连,一個特定的結(jié)構(gòu)體的各個元素在不同的機器上的偏移量有不同叽粹,即使給定的這些元素在所有機器上的大小相同。事實上却舀,一個 包含一個32位指針和一個8位字符的結(jié)構(gòu)提在三個不同的機器上可能有三個不同的大小虫几。作為一個推論,對象指針可能無法自由互換禁筏;通過一個指向起始 地址為奇數(shù)地址長度為4個字節(jié)的指針保存一個整型數(shù)有時可以正常工作持钉,但有時則會導(dǎo)致產(chǎn)生core,有些時候靜悄悄地失敗了(在這個過程中會破壞 其他數(shù)據(jù))篱昔。在那些不按字節(jié)尋址的機器上每强,字符指針更是"事故高發(fā)地區(qū)"始腾。對齊考慮以及加載器的特殊性使得很容易輕率地認為兩個連續(xù)聲明的變量在 內(nèi)存中也是連在一起的,或者某個類型的變量已經(jīng)被適當(dāng)對齊并可以用作其他類型變量使用了空执。
在一些機器上浪箭,諸如VAX(小端),一個字的字節(jié)隨著地址的增加辨绊,其重要性提高奶栖;而另外一些機器上,諸如68000(大端)门坷,隨著地址的增加宣鄙,其重要性降 低。字或更大數(shù)據(jù)對象(諸如一個雙精度字)的字節(jié)順序可能并不相同默蚌。因此冻晤,任何依賴對象內(nèi)從左到右方向位模式的代碼都值得特別細致的審查。只有當(dāng) 結(jié)構(gòu)體中兩個不同的位字段不被連接以及不被當(dāng)作一個單元時绸吸,這些位字段才具備可移植性鼻弧。事實上,連接任意兩個變量都是不可移植的行為锦茁。
結(jié)構(gòu)體中有一些未使用的空洞攘轩。猜想聯(lián)合體用于類型欺騙。尤其是码俩,一個值不應(yīng)該在存儲時使用一個類型度帮,而在讀取時使用另外一種類型。對聯(lián)合體來說握玛,一個顯式 的標(biāo)簽(tag)字段可能會很有用够傍。
不同的編譯器在返回結(jié)構(gòu)體時使用不同的約定甫菠。這就會導(dǎo)致代碼在接受從不同編譯器編譯的庫代碼中返回的結(jié)構(gòu)體值時會出現(xiàn)錯誤挠铲。結(jié)構(gòu)體指針不是問題。
-
不要假設(shè)參數(shù)傳遞機制寂诱。特別是指針大小以及參數(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ù)在傳入時可能被擴大连霉,例如一個char可能以int型被傳 入榴芳。參數(shù)可能以從左到右,從右到左跺撼,或以任意順序壓入棧窟感,或直接放在寄存器中(根本無需壓棧)。參數(shù)求值的順序也可能與壓棧的次序有所不同歉井。一個 編譯器可能使用多種(不兼容的)調(diào)用約定柿祈。
在某些機器上,空字符指針((char *)0)常被當(dāng)作指向空字符串的指針對待哩至。不要依賴于此躏嚎。
-
不要修改字符串常量。下面就是一個臭名昭著的例子
s = "/dev/tty??"; strcpy (&s[8], ttychars);
地址空間可能有空洞菩貌。簡單計算一個數(shù)組中未分配空間的元素(在數(shù)組實際存儲區(qū)域之前或之后)的地址可能會導(dǎo)致程序崩潰紧索。如果這個地址被用于比較,有時程序 可以運行菜谣,但會破壞數(shù)據(jù)珠漂,報錯,或陷入死循環(huán)尾膊。在ANSI C中媳危,指向一個對象數(shù)組的指針指向數(shù)組結(jié)尾后的第一個元素是合法的,這在一些老編譯器上通常是安全的冈敛。不過這個"在外邊"不可以被解引用待笑。
只有==和!=比較可用于某給定類型的所有指針。當(dāng)兩個指針指向同一個數(shù)組內(nèi)的元素(或數(shù)組后第一個元素)時抓谴,使用<<暮蹂、<=、& gt;或>=對兩個指針進行比較是可移植的癌压。同樣仰泻,僅僅對指向同一個數(shù)組內(nèi)的元素(或數(shù)組后第一個元素)的兩個指針使用算術(shù)操作符才是可移 植的。
字長(word size)也影響移位和掩碼滩届。下面代碼在一些68000機器上只會將一個整型數(shù)的最右三個位清0集侯,而在其他機器上它還會將高地址的兩個字節(jié)清零。x &= 0177770 使用 x &= ~07可以在所有機器上正常工作帜消。位字段(bitfield)沒有這些問題棠枉。
-
表達式內(nèi)的副作用可能導(dǎo)致代碼語義是編譯器相關(guān)的,因為在大多數(shù)情況下C語言的求值順序是沒有顯式定義的泡挺。下面是一個臭名昭著的例子:
a[i] = b[i++];
在上面的例子中辈讶,我們只知道b的下標(biāo)值沒有被增加。a的下標(biāo)i值可能是自增后的值也可能是自增前的值娄猫。
struct bar_t { struct bar_t *next; } bar; bar->next = bar = tmp;
在第二個例子中贱除,bar->next的地址很可能在bar被賦值之前被計算使用咳促。
bar = bar->next = tmp;
第三個例子中,bar可能在bar->next之前被賦值勘伺。雖然這可能有悖于"賦值從右到左處理"的規(guī)則跪腹,但這確是一個合法的解析》勺恚考慮下 面的例子:
long i;
short a[N];
i = old
i = a[i] = new;
賦給i的值必須是一個按照從右到左的處理順序進行賦值處理后的值冲茸。但是i可能在ai被賦值前而被賦值為"(long) (short)new"。不同編譯器作法不同缅帘。
質(zhì)疑代碼中出現(xiàn)的數(shù)值(“魔數(shù)”)轴术。
-
避免使用預(yù)處理器技巧。一些諸如使用/ /粘和字符串以及依賴參數(shù)字符串展開的宏會破壞代碼可靠性钦无。
#define FOO(string) (printf("string = %s",(string))) ... FOO(filename);
只是在有些時候會擴展為
(printf("filename = %s",(filename)))
小心逗栽。詭異的預(yù)處理器在一些機器上可能導(dǎo)致宏異常中斷。下面是一個宏的兩種不同實現(xiàn)版本:
#define LOOKUP(chr) (a['c'+(chr)]) /* Works as intended. */
#define LOOKUP(c) (a['c'+(c)]) /* Sometimes breaks. */
第二個版本的LOOKUP可能以兩種不同的方式擴展失暂,并且會導(dǎo)致代碼異常中斷彼宠。
熟悉現(xiàn)有的庫函數(shù)和定義(但不用太熟悉。與其外部接口相反弟塞,庫基礎(chǔ)設(shè)施的內(nèi)部細節(jié)常會改變并且沒有警告凭峡,這些細節(jié)常常也是不可移植的)。你不應(yīng)該再自己重 新編寫字符串比較例程决记、終端控制例程或為系統(tǒng)結(jié)構(gòu)編寫你自己的定義摧冀。自己動手實現(xiàn)既浪費你的時間,又使得你的代碼可讀性變差系宫,因為另外一個讀者需 要知道你是否在新的實現(xiàn)中做了什么特殊的事情索昂,并嘗試證實它們的存在。同時這樣做會使得你無法充分利用一些輔助的微代碼或其他有助于提高系統(tǒng)例程 性能的方法扩借。更進一步椒惨,它將是一個bug的高產(chǎn)源頭。如果可能的話往枷,要知道公共庫之間的差異(如ANSI框产、POSIX等等)。
如果lint可用错洁,請使用lint。這個工具對于查找代碼中機器相關(guān)的構(gòu)造戒突、其他不一致性以及順利通過編譯器檢查的程序bug時具有很高價值屯碴。如果你的編 譯器具備打開警告的開關(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)用時今艺,請對NULL進行轉(zhuǎn)換韵丑。不要讓函數(shù)調(diào)用成為類型欺騙發(fā)生的地方。C 語言的類型提升規(guī)則很是讓人費解虚缎,所以盡量小心撵彻。例如,如果一個函數(shù)接受一個32位長的長整型做為參數(shù)实牡,但實際傳入的卻是一個16位長的整型數(shù)陌僵, 函數(shù)棧可能會無法對齊创坞,這個值也可能會被錯誤提升碗短。
在混用有符號和無符號值的算術(shù)計算時請使用顯式類型轉(zhuǎn)換
應(yīng)該謹慎使用跨程序的goto、longjmp题涨。很多實現(xiàn)"忘記"恢復(fù)寄存器中的值了偎谁。盡可能將關(guān)鍵的值聲明為volatile,或?qū)⑺鼈冏⑨尀?VOLATILE纲堵。
一些鏈接器將名字轉(zhuǎn)換為小寫搭盾,并且一些鏈接器只識別前六個字母作為唯一標(biāo)識。在這些系統(tǒng)上程序可能會悄悄地中斷運行婉支。
當(dāng)心編譯器擴展鸯隅。如果使用了編譯器擴展,請將他們視為機器依賴并用文檔記錄下來向挖。
通常程序無法在數(shù)據(jù)段執(zhí)行代碼或者無法將數(shù)據(jù)寫入代碼段蝌以。即使程序可以這么做,也無法保證這么做是可靠的何之。