Recommended C Style and Coding Standards中文翻譯版
16. 可移植性
"C語言結(jié)合了匯編的強(qiáng)大功能和可移植性" — 無名氏,暗指比爾.薩克。
可移植代碼的好處是有目共睹的。這一節(jié)將闡述一些編寫可移植代碼的指導(dǎo)原則。這里"可移植的"是指一個(gè)源碼文件能夠在不同機(jī)器上被編譯和執(zhí)行,其 前提僅僅是在不同平臺(tái)上可能包含不同的頭文件,使用不同的編譯器開關(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)化后的代碼往往是模糊不清、難以理解的。在一臺(tái)機(jī)器上經(jīng)過優(yōu)化后的代碼,在其他機(jī)器上 可能變得更加糟糕。將采用的性能優(yōu)化手段記錄下來并盡可能多地本地化。文檔應(yīng)該解釋這些手段的工作原理以及引入它們的原因(例如:"循環(huán)執(zhí)行了無 數(shù)次")
* 要意識(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)的"安全"類型大小。無符號(hào)與帶符 號(hào)數(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)钥赡芫哂胁煌母袷?。例如,下面例子在一些sizeof(int)等于 sizeof(char)的機(jī)器上可能失敗。其原因在與free函數(shù)期望一個(gè)char,但卻傳入了一個(gè)int。
int *p = (int *) malloc (sizeof(int));
free (p);
* 注意,一個(gè)對象的大小不能保證這個(gè)對象的精度。Cray-2可能使用64位來存儲(chǔ)一個(gè)整型,但一個(gè)長整型轉(zhuǎn)換為一個(gè)整型并且再轉(zhuǎn)換回長整型后可能會(huì)被截?cái)?為32位。
* 整型常量0可以強(qiáng)制轉(zhuǎn)型為任何指針類型。轉(zhuǎn)換后的指針稱為對應(yīng)那個(gè)類型的空指針,并且與那個(gè)類型的其他指針不同。空指針比較總是與常量0相當(dāng)??罩羔槻粦?yīng) 該與一個(gè)值為0的變量比較??罩羔槻豢偸鞘褂萌?的位模式表示。兩個(gè)不同類型的空指針有些時(shí)候可能不同。某個(gè)類型的空指針被強(qiáng)制轉(zhuǎn)換為另外一個(gè)類 型的指針,其結(jié)果是該指針轉(zhuǎn)換為第二個(gè)類型的空指針。
* 對于ANSI編譯器,當(dāng)兩個(gè)類型相同的指針訪問同一塊存儲(chǔ)區(qū)時(shí),則它們比較是相等的。當(dāng)一個(gè)非0整型常量被轉(zhuǎn)換為指針類型時(shí),它們可能與其他指針相等。對 于非ANSI編譯器,訪問同一塊存儲(chǔ)區(qū)的兩個(gè)指針比較可能并不相同。例如,下面兩個(gè)指針比較可能相等或不相等,并且他們可能或可能沒有訪問同一塊 存儲(chǔ)區(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類型。千萬不要依賴于此。
* 提防帶符號(hào)字符。例如,在某些VAX系統(tǒng)上,用在表達(dá)式中的字符是符號(hào)擴(kuò)展的,但在其他一些機(jī)器上并非如此。對有符號(hào)和無符號(hào)有依賴的代碼是不可移植的。 例如,如果假設(shè)c是正值,arrayc在c為有符號(hào)且為負(fù)值時(shí)將無法正常工作。如果你一定要假設(shè)signed或unsigned字符的話,請 用SIGNED或UNSIGNED為其加上注釋。無符號(hào)字符的行為可由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í)查找大小敏感的代碼。無符號(hào)類型比有符號(hào)整型更加編譯器無關(guān)。如 果既可以用16bit也可以用32bit標(biāo)識(shí)一個(gè)簡單for循環(huán)的計(jì)數(shù)器,我們應(yīng)該使用int。因?yàn)閷τ诋?dāng)前機(jī)器來說,通過整型可以獲取更高效 (自然)的存儲(chǔ)單元。
* 數(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í)則會(huì)導(dǎo)致產(chǎn)生core,有些時(shí)候靜悄悄地失敗了(在這個(gè)過程中會(huì)破壞 其他數(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)該在存儲(chǔ)時(shí)使用一個(gè)類型,而在讀取時(shí)使用另外一種類型。對聯(lián)合體來說,一個(gè)顯式 的標(biāo)簽(tag)字段可能會(huì)很有用。
* 不同的編譯器在返回結(jié)構(gòu)體時(shí)使用不同的約定。這就會(huì)導(dǎo)致代碼在接受從不同編譯器編譯的庫代碼中返回的結(jié)構(gòu)體值時(shí)會(huì)出現(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 */
}
* 上面的例子有諸多問題。??赡芟蛏显鲩L,也可能向下增長(事實(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í)際存儲(chǔ)區(qū)域之前或之后)的地址可能會(huì)導(dǎo)致程序崩潰。如果這個(gè)地址被用于比較,有時(shí)程序 可以運(yùn)行,但會(huì)破壞數(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í),使用<<、<=、& amp; gt;或>=對兩個(gè)指針進(jìn)行比較是可移植的。同樣,僅僅對指向同一個(gè)數(shù)組內(nèi)的元素(或數(shù)組后第一個(gè)元素)的兩個(gè)指針使用算術(shù)操作符才是可移 植的。
* 字長(word size)也影響移位和掩碼。下面代碼在一些68000機(jī)器上只會(huì)將一個(gè)整型數(shù)的最右三個(gè)位清0,而在其他機(jī)器上它還會(huì)將高地址的兩個(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ù)字符串展開的宏會(huì)破壞代碼可靠性。
#define FOO(string) (printf("string = %s",(string)))
…
FOO(filename);
只是在有些時(shí)候會(huì)擴(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ò)展,并且會(huì)導(dǎo)致代碼異常中斷。
* 熟悉現(xiàn)有的庫函數(shù)和定義(但不用太熟悉。與其外部接口相反,庫基礎(chǔ)設(shè)施的內(nèi)部細(xì)節(jié)常會(huì)改變并且沒有警告,這些細(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í)這樣做會(huì)使得你無法充分利用一些輔助的微代碼或其他有助于提高系統(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ù)棧可能會(huì)無法對齊,這個(gè)值也可能會(huì)被錯(cuò)誤提升。
* 在混用有符號(hào)和無符號(hào)值的算術(shù)計(jì)算時(shí)請使用顯式類型轉(zhuǎn)換
* 應(yīng)該謹(jǐn)慎使用跨程序的goto、longjmp。很多實(shí)現(xiàn)"忘記"恢復(fù)寄存器中的值了。盡可能將關(guān)鍵的值聲明為volatile,或?qū)⑺鼈冏⑨尀?VOLATILE。
* 一些鏈接器將名字轉(zhuǎn)換為小寫,并且一些鏈接器只識(shí)別前六個(gè)字母作為唯一標(biāo)識(shí)。在這些系統(tǒng)上程序可能會(huì)悄悄地中斷運(yùn)行。
* 當(dāng)心編譯器擴(kuò)展。如果使用了編譯器擴(kuò)展,請將他們視為機(jī)器依賴并用文檔記錄下來。
* 通常程序無法在數(shù)據(jù)段執(zhí)行代碼或者無法將數(shù)據(jù)寫入代碼段。即使程序可以這么做,也無法保證這么做是可靠的。
17. 標(biāo)準(zhǔn)C
現(xiàn)代C編譯器支持一些或全部的ANSI提議的標(biāo)準(zhǔn)C。無論何時(shí)可能的話,盡量用標(biāo)準(zhǔn)C編寫和運(yùn)行程序,并且使用諸如函數(shù)原型,常量存儲(chǔ)以及 volatile(易失性)存儲(chǔ)等特性。標(biāo)準(zhǔn)C通過給優(yōu)化器提供有有效的信息以提升程序的性能。標(biāo)準(zhǔn)C通過保證所有編譯器接受同樣的輸入語言以及提供相關(guān) 機(jī)制隱藏機(jī)器相關(guān)內(nèi)容或?qū)τ谀切C(jī)器相關(guān)代碼提供警告的方式提升代碼的可移植性。
17.1 兼容性
編寫很容易移植到老編譯器上的代碼。例如,有條件地在global.h中定義一些新(標(biāo)準(zhǔn)中的)關(guān)鍵字,比如const和volatile。標(biāo)準(zhǔn)編譯器預(yù) 定義了預(yù)處理器符號(hào)STDC(見腳注8)。void類型很難簡單地處理正確,因?yàn)楹芏嗬暇幾g器只理解void,但不認(rèn)識(shí)void。最簡單的方法就是定義一 個(gè)新類型VOIDP(與機(jī)器和編譯器相關(guān)),通常在老編譯器下該類型被定義為char*。
#if __STDC__
typedef void *voidp;
# define COMPILER_SELECTED
#endif
#ifdef A_TARGET
# define const
# define volatile
# define void int
typedef char *voidp;
# define COMPILER_SELECTED
#endif
#ifdef …
…
#endif
#ifdef COMPILER_SELECTED
# undef COMPILER_SELECTED
#else
{ NO TARGET SELECTED! }
#endif
注意在ANSI C中,#必須是同一行中預(yù)處理器指示符的第一個(gè)非空白字符。在一些老編譯器中,它必須是同一行中的第一個(gè)字符。
當(dāng)一個(gè)靜態(tài)函數(shù)具有前置聲明時(shí),前置聲明必須包含存儲(chǔ)修飾符。在一些老編譯器中,這個(gè)修飾符必須是"extern"。對于ANSI編譯器,這個(gè)存儲(chǔ)修飾符 必須為static,但全局函數(shù)依然必須聲明為extern。因此,靜態(tài)函數(shù)的前置聲明應(yīng)該使用一個(gè)#define,例如FWD_STATIC,并通 過#ifdef適當(dāng)定義。
一個(gè)"#ifdef NAME"應(yīng)該要么以"#endif"結(jié)尾,要么以"#endif / NAME /結(jié)尾,不應(yīng)該用"#endif NAME"結(jié)尾。對于短小的#ifdef不應(yīng)該使用注釋,因?yàn)橥ㄟ^代碼我們可以明確其含義。
ANSI的三字符組可能導(dǎo)致內(nèi)容包含??的字符串的程序神秘的中斷。
17.2 格式化
ANSI C的代碼風(fēng)格與常規(guī)C一樣,但有兩點(diǎn)意外:存儲(chǔ)修飾符(storage qualifiers)和參數(shù)列表。
由于const和volatile的綁定規(guī)則很奇怪,因此每個(gè)const或volatile對象都應(yīng)該單獨(dú)聲明。
int const *s; /* YES */
int const *s, *t; /* NO */
具備原型的函數(shù)將參數(shù)聲明和定義歸并在一個(gè)參數(shù)列表中了。應(yīng)該在函數(shù)的注釋中提供各個(gè)參數(shù)的注釋。
/*
* `bp': boat trying to get in.
* `stall': a list of stalls, never NULL.
* returns stall number, 0 => no room.
*/
int
enter_pier (boat_t const *bp, stall_t *stall)
{
…
17.3 原型
應(yīng)該使用函數(shù)原型使得代碼更加健壯并且運(yùn)行時(shí)性能更好。不幸地是原型的聲明
extern void bork (char c);
與定義不兼容。
void
bork (c)
char c;
…
原型中c應(yīng)該以機(jī)器上最自然的類型傳入,很可能是一個(gè)字節(jié)。而非原型化(向后兼容)的定義暗示c總是以一個(gè)整型傳入。如果一個(gè)函數(shù)具有可類型提升的參數(shù), 那么調(diào)用者和被調(diào)用者必須以相等地方式編譯。要么都必須使用函數(shù)原型,要么都不使用原型。如果在程序設(shè)計(jì)時(shí)參數(shù)就是可以提升類型的,那么問題就可以被避 免,例如bork可以定義成接受一個(gè)整型參數(shù)。
如果定義也是原型化的,上面的聲明將工作正常。
void
bork (char c)
{
…
不幸地是,原型化的語法將導(dǎo)致非ANSI編譯器拒絕這個(gè)程序。
但我們可以很容易地通過編寫外部聲明來同時(shí)適應(yīng)原型與老編譯器。
#if __STDC__
# define PROTO(x) x
#else
# define PROTO(x) ()
#endif
extern char **ncopies PROTO((char *s, short times));
注意PROTO必須使用雙層括號(hào)。
最后,最好只使用一種風(fēng)格編寫代碼(例如,使用原型)。當(dāng)需要非原型化的版本時(shí),可使用一個(gè)自動(dòng)轉(zhuǎn)換工具生成。
17.4 Pragmas
Pragmas用于以一種可控的方式引入機(jī)器相關(guān)的代碼。很顯然,pragma應(yīng)該被視為機(jī)器相關(guān)的。不幸地是,ANSI pragmas的語法使得我們無法將其隔離到機(jī)器相關(guān)的頭文件中了。
Pragmas分為兩類。優(yōu)化相關(guān)的可以被安全地忽略。而那些影響系統(tǒng)行為(需要pragmas)的Pragmas則不能忽略。需要的pragmas應(yīng)該結(jié)合#ifdef使用,這樣如果一個(gè)pragma都沒有選到,編譯過程將退出。
兩個(gè)編譯器可能通過兩個(gè)不同的方式使用同一個(gè)給定的pragma。例如,一個(gè)編譯器可能使用haggis發(fā)出一個(gè)優(yōu)化信號(hào)。而另一個(gè)可能使用它暗示一個(gè)特 定語句,一旦執(zhí)行到此,程序應(yīng)該退出。不過,一旦使用了pragma,它們必須總是被機(jī)器相關(guān)的#ifdef包圍。對于非ANSI編譯器,Pragmas 必須總是被#ifdef。確保對#pragma的#進(jìn)行縮進(jìn),否則一些較老的預(yù)處理器處理它時(shí)會(huì)掛起。
#if defined(__STDC__) && defined(USE_HAGGIS_PRAGMA)
#pragma (HAGGIS)
#endif
"ANSI標(biāo)準(zhǔn)中描述的'#pragma'命令具有任意實(shí)現(xiàn)定義的影響。在GNU C預(yù)處理中,'#pragma'首先嘗試運(yùn)行游戲'rogue';如果失敗,它將嘗試運(yùn)行游戲'hack';如果失敗,它將嘗試運(yùn)行GNU Emacs顯示漢諾塔;如果失敗,它將報(bào)告一個(gè)致命錯(cuò)誤。無論如何,預(yù)處理將不再繼續(xù)。"
— GNU CC 1.34 C預(yù)處理手冊。
18. 特殊考慮
這節(jié)包含一些雜項(xiàng):‘做'與'不做'。
* 不要通過宏替換來改變語法。這將導(dǎo)致程序?qū)τ谒腥硕际请y以理解的,除了那個(gè)肇事者。
* 不要在需要離散值的地方使用浮點(diǎn)變量。使用一個(gè)浮點(diǎn)數(shù)作為循環(huán)計(jì)數(shù)器無疑是搬起石頭砸自己的腳??偸怯?lt;=或>=測試浮點(diǎn)數(shù),對它們永遠(yuǎn)不要 用精確比較(==或!=)。
* 編譯器也有bug。常見且高發(fā)的問題包括結(jié)構(gòu)體賦值和位字段。你無法泛泛的預(yù)測一個(gè)編譯器都有哪些bug。但你可以在程序中避免使用那些已知的在所有編譯 器上都存在問題的結(jié)構(gòu)。你無法讓你寫的任何代碼都是有用的,你可能仍然會(huì)遇到bug,并且在這期間編譯器很可能會(huì)被修復(fù)。因此,只有當(dāng)你被強(qiáng)制使 用某個(gè)特定的充斥bug的編譯器時(shí),你才應(yīng)該"圍繞"著編譯器bug寫代碼。
* 不要依賴自動(dòng)代碼美化工具。良好代碼風(fēng)格的主要受益者就是代碼的編寫者,并且尤其在手寫算法或偽代碼的早期設(shè)計(jì)階段。自動(dòng)代碼美化工具只應(yīng)該用在那些已經(jīng) 完成、語法正確并且此后不能滿足當(dāng)空白和縮進(jìn)被更為關(guān)注的要求時(shí)。伴隨著對細(xì)致程序員的細(xì)節(jié)的關(guān)注,對于那些將函數(shù)或文件布局解釋清楚的工作,程 序員們會(huì)做得更好(換句話說,一些視覺布局是由意圖而不是語法決定的,美化工具無法了解到程序員的思想)。粗心的程序員應(yīng)該學(xué)習(xí)成為一個(gè)細(xì)致的程 序員,而不是依賴美化工具讓代碼可讀性更好。
* 意外地遺漏邏輯比較表達(dá)式中的第二個(gè)=是一個(gè)常犯的問題。使用顯式測試。避免對賦值使用隱式測試。
abool = bbool;
if (abool) { …
當(dāng)嵌入的賦值表達(dá)式使用時(shí),確保測試是顯式的,這樣后續(xù)它就無法被"修復(fù)"了。
while ((abool = bbool) != FALSE) { …
while (abool = bbool) { … /* VALUSED */
while (abool = bbool, abool) { …
顯式地注釋那些在正常控制流之外被修改的變量,或其他可能在維護(hù)過程中中斷的代碼。
現(xiàn)代編譯器會(huì)自動(dòng)將變量放到寄存器中。對于你認(rèn)為最關(guān)鍵的變量慎用寄存器。在極端情況下,用寄存器標(biāo)記2-4個(gè)最為關(guān)鍵的值,并且將剩余的標(biāo)記為 REGISTER。后者在那些具有較多寄存器的機(jī)器上可以#define為寄存器。
19. Lint
Lint是一個(gè)C程序檢查工具,用于檢查C語言源碼文件,探測和報(bào)告諸如類型不兼容、函數(shù)定義與調(diào)用不一致以及潛在的bug等情況。強(qiáng)烈建議在所 有程序上使用lint工具,并且期望大多數(shù)工程將lint作為官方驗(yàn)收程序的一部分。
應(yīng)該注意的是使用lint的最好方法不是將lint作為官方驗(yàn)收之前的一道必須跨過的柵欄,而是作為一個(gè)在代碼發(fā)生添加或變更之后使用的工具。 Lint可以發(fā)現(xiàn)一些隱藏的bug并且可以在問題發(fā)生前保證程序的可移植性。lint產(chǎn)生的許多信息確實(shí)暗示了一些事情是錯(cuò)誤的。一個(gè)有意思的故 事是關(guān)于一個(gè)漏掉了fprintf的一個(gè)參數(shù)的程序:
fprintf ("Usage: foo -bar <file>\n");
作者從未有過一個(gè)問題。但每當(dāng)一個(gè)正常用戶在命令行上犯錯(cuò),這個(gè)程序就會(huì)產(chǎn)生一個(gè)core。許多版本的lint工具都能發(fā)現(xiàn)這個(gè)問題。
大多l(xiāng)int選項(xiàng)都值得我們學(xué)習(xí)。一些選項(xiàng)可能在合法的代碼上給出警告,但它們也會(huì)捕捉到許多把事情搞遭的代碼。注意'–p'只能為庫的一個(gè)子 集檢查函數(shù)調(diào)用和類型的一致性,因此程序?yàn)榱俗畲蠡母采w檢查,應(yīng)該同時(shí)進(jìn)行帶–p和不帶–p的lint檢查。
Lint也可以識(shí)別代碼里的一些特殊注釋。這些注釋可以強(qiáng)制讓lint在發(fā)現(xiàn)問題時(shí)關(guān)閉警告輸出,還可以作為一些特殊代碼的文檔。
20. Make
另外一個(gè)非常有用的工具是make。在開發(fā)過程中,make只會(huì)重新編譯那些上次make后發(fā)生了改變的模塊。它也可以用于自動(dòng)化其他任務(wù)。一些 常見的約定包括:
all
執(zhí)行所有二進(jìn)制文件的構(gòu)建過程
clean
刪除所有中間文件
debug
構(gòu)建一個(gè)測試用二進(jìn)制文件a.out或debug
depend
制作可傳遞的依賴關(guān)系
install
安裝二進(jìn)制文件,庫等
deinstall
取消安裝
mkcat
安裝手冊
lint
運(yùn)行l(wèi)int工具
print/list
制作一個(gè)所有源文件的拷貝
shar
為所有源文件制作一個(gè)shar文件
spotless
執(zhí)行make clean,并將源碼存入版本控制工具。注意:不會(huì)刪除Makefile,即便它是一個(gè)源文件。
source
撤銷spotless所做的事情。
tags
運(yùn)行ctags(建議使用-t標(biāo)志)
rdist
分發(fā)源碼到其他主機(jī)
file.c
從版本控制系統(tǒng)中檢出這個(gè)文件
除此之外,通過命令行也可以定義Makefile使用的值(如"CFLAGS")或源碼中使用的值(如"DEBUG")。
21. 工程相關(guān)的標(biāo)準(zhǔn)
除了這里提到內(nèi)容外,每個(gè)獨(dú)立的工程都期望能建立附加標(biāo)準(zhǔn)。下面是每個(gè)工程程序管理組需要考慮的問題中的一部分:
* 哪些額外的命名約定需要遵守?尤其是,那些用于全局?jǐn)?shù)據(jù)的功能歸類以及結(jié)構(gòu)體或聯(lián)合體成員名字的系統(tǒng)化的前綴約定非常有用。
* 什么樣的頭文件組織適合于工程特定的數(shù)據(jù)體系結(jié)構(gòu)?
* 應(yīng)該建立什么樣的規(guī)程來審核lint警告?需要確立一個(gè)與lint選項(xiàng)一致的寬容度,保證lint不會(huì)針對一些不重要的問題給出警告,但同時(shí)保證真正的bug或不一致問題不被隱藏。
* 如果一個(gè)工程建立了自己的檔案庫,它應(yīng)該計(jì)劃向系統(tǒng)管理員提供一個(gè)lint庫文件。這個(gè)lint庫文件允許lint工具檢查對庫函數(shù)的兼容性使用。
* 需要使用哪種版本控制工具?
22. 結(jié)論
這里描述了一套C語言編程風(fēng)格的標(biāo)準(zhǔn)。其中最重要的幾點(diǎn)是:
* 合理使用空白和注釋,使得我們通過代碼布局就可以清楚地看出程序的結(jié)構(gòu)。使用簡單表達(dá)式、語句和函數(shù),使他們可以很容易地被理解。
* 記住,在將來某個(gè)時(shí)候你或其他人很可能會(huì)被要求修改代碼或讓代碼運(yùn)行在一臺(tái)不同的機(jī)器上。精心編寫代碼,使得其可以移植到尚不確定的機(jī)器上。局部化你的優(yōu)化,因?yàn)檫@些優(yōu)化經(jīng)常讓人困惑,并且對于該優(yōu)化措施是否適合其他機(jī)器我們持悲觀態(tài)度。
* 許多風(fēng)格選擇是主觀武斷的。保持代碼風(fēng)格一致比遵循這些絕對的風(fēng)格規(guī)則更重要(尤其是與組織內(nèi)部標(biāo)準(zhǔn)保持一致)?;煊蔑L(fēng)格比任何一種糟糕的風(fēng)格都更加糟糕。
無論采用哪種標(biāo)準(zhǔn),如果認(rèn)為該標(biāo)準(zhǔn)有用就必須遵循它。如果你覺得遵循某條標(biāo)準(zhǔn)時(shí)有困難,不要僅僅忽略它們,而是在和你當(dāng)?shù)氐拇髱熁蚪M織內(nèi)的有經(jīng)驗(yàn)的程序員討論后再做決定。
23. 參考資料
B.A. Tague, C Language Portability, Sept 22, 1977. This document issued by department 8234 contains three memos by R.C. Haight, A.L. Glasser, and T.L. Lyon dealing with style and portability.
S.C. Johnson, Lint, a C Program Checker, Unix Supplementary Documents, November 1986.
R.W. Mitze, The 3B/PDP-11 Swabbing Problem, Memorandum for File, 1273-770907.01MF, September 14, 1977.
R.A. Elliott and D.C. Pfeffer, 3B Processor Common Diagnostic Standards- Version 1, Memorandum for File, 5514-780330.01MF, March 30, 1978.
R.W. Mitze, An Overview of C Compilation of Unix User Processes on the 3B, Memorandum for File, 5521-780329.02MF, March 29, 1978.
B.W. Kernighan and D.M. Ritchie, The C Programming Language, Prentice Hall 1978, Second Ed. 1988, ISBN 0-13-110362-8.
S.I. Feldman, Make -- A Program for Maintaining Computer Programs, UNIXSupplementary Documents, November 1986.
Ian Darwin and Geoff Collyer, Can't Happen or / NOTREACHED / or Real Programs Dump Core, USENIX Association Winter Conference, Dallas 1985 Proceedings.
Brian W. Kernighan and P. J. Plauger The Elements of Programming Style. McGraw-Hill, 1974, Second Ed. 1978, ISBN 0-07-034-207-5.
J. E. Lapin Portable C and UNIX System Programming, Prentice Hall 1987, ISBN 0-13-686494-5.
Ian F. Darwin, Checking C Programs with lint, O'Reilly & Associates, 1989. ISBN 0-937175-30-7.
Andrew R. Koenig, C Traps and Pitfalls, Addison-Wesley, 1989. ISBN 0-201-17928-8.
相關(guān)文章
C++實(shí)現(xiàn)LeetCode(73.矩陣賦零)
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(73.矩陣賦零),本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07