- 編寫高質量代碼:改善C程序代碼的125個建議
- 馬偉 著
- 2425字
- 2019-01-01 01:33:13
建議5:使用有嚴格定義的數據類型
大家都知道,C語言是一種既具有高級語言的特點,又具有匯編語言特點的程序設計語言。它既可以作為系統設計語言來編寫系統應用程序,也可以作為應用程序設計語言來編寫不依賴計算機硬件的應用程序。因此,它是一種可移植性很高的語言,用它所寫的程序可以很方便地部署到不同的平臺之上。
盡管如此,C語言在可移植性方面實際上還是存在著許多重要的問題。除了不同的系統使用的C語言標準庫不同之外,預處理程序和語言本身在許多重要方面也會不盡相同。我們知道,ANSI委員會對C語言的大多數問題進行了標準化,從而使程序員可以很方便地寫出可移植的代碼。但是,ANSI標準卻并沒有準確定義像char、int和long這樣的內部數據類型,而是將這些重要的實現細節留給編譯程序的研制者來決定。
例如,某一個ANSI標準的編譯程序可能具有32位的int和char類型,它們在默認狀態下是有符號的;而另一個ANSI標準的編譯程序可能有16位的int和char類型,默認狀態下是無符號的。盡管如此不同,但這兩個編譯程序卻都是嚴格符合ANSI標準的。為了讓讀者更加深入地了解這種情況,我們來看下面一段示例代碼:
char ch; ch= (char)0xff; if(ch == 0xff) { }
在上面的代碼中,我們先將整數0xff賦給char類型的變量ch,然后再將ch變量與整數0xff進行比較。從表面上看,語句“if(ch==0xff)”應該返回真。但實際情況并非如此,語句“if(ch==0xff)”的具體返回值因系統而異,也就是說它有可能返回真,也有可能返回假。或許有人會疑惑,這么明顯的一個語句怎么會發生這種情況呢?
其實,原因很簡單。上面我們說過,ANSI標準確并沒有準確定義像char、int和long這樣的內部數據類型,而是將這些重要的實現細節留給了編譯程序。因此,它的結果完全依賴于編譯程序。如果默認字符是無符號的,則語句“if(ch==0xff)”的返回值肯定為真;但對字符為有符號的編譯程序而言,語句“if(ch==0xff)”的返回值卻會為假。
在上面的代碼中,字符ch要與整型數0xff進行比較。根據C語言的轉換規則,編譯程序必須首先將ch轉換為整型int,待兩者類型一致后再進行比較。這樣,如果int是32位的,則在轉換中會將其值從0xff擴充為0xffffffff。因此,語句“if(ch==0xff)”的返回值就為假了。
其實,對于上面的這些問題,ANSI委員會成員并非視而不見。實際上,他們考查了大量的C語言實現并得出了這樣的結論:由于各編譯程序之間的類型定義是如此不同,以致定義嚴格的標準將會使大量現存代碼無效。而這就恰恰違背了他們的一個重要指導原則:“現存代碼是非常重要的。”
除此之外,對類型進行嚴格約束也將違背委員會的另外一個指導原則:“保持C語言的活力,即使不能保證它具有可移植性,也要使其運行速度快。”因此,如果實現者感到有符號字符對給定的機器來說更有效,那么就使用有符號字符吧!同樣,硬件實現者可以將int選擇為16位、32位或別的位數。也就是說,在默認狀態下,用戶并不知道位域是有符號的還是無符號的。
當然,這種內部類型在其規格說明中存在著一個不足之處,在今后升級或改變編譯程序時,或者移到新的目標環境時,或者與其他單位共享代碼時,甚至在改變工作且所用編譯程序的規則全部改變時,這個不足就會體現出來。但是,這并不意味著用戶就不能安全地使用這些內部類型。其實,只要用戶不對ANSI標準沒有明確說明的類型再作假設,用戶就可以安全使用內部類型。例如,對于char數據類型,只要它能提供0~127的值(即有符號字符和無符號字符域的交集),一般就是可移植的。例如下面這段代碼:
int strcmp(const char *strLeft,const char *strRight) { assert(strLeft!=NULL&&strRight!=NULL); int ret=0; while(!(ret= *strLeft-*strRight) && *strRight) { strLeft++,strRight++; } if(ret<0) { ret=-1; } else if(ret>0) { ret=1; } else { ret=0; } return ret; }
在上面代碼中,strcmp函數用于比較兩個字符串。如果strLeft<strRight,則返回-1;如果strLeft==strRight,則返回0;如果strLeft>strRight,則返回1。從表面上看,strcmp函數并沒有什么大的問題,但如果仔細觀察,你會發現strcmp函數在可移植方面存在問題。因此,我們需要將strLeft和strRight參數聲明為無符號字符指針,如下面的代碼所示:
int strcmp(const unsigned char *strLeft, const unsigned char *strRight) { assert(strLeft!=NULL&&strRight!=NULL); int ret=0; while(!(ret= *strLeft-*strRight) && *strRight) { strLeft++,strRight++; } if(ret<0) { ret=-1; } else if(ret>0) { ret=1; } else { ret=0; } return ret; }
當然,我們也可以直接在函數里對其進行修改,如下面的代碼所示:
int strcmp(const char *strLeft,const char *strRight) { assert(strLeft!=NULL&&strRight!=NULL); int ret=0; while(!(ret= *(unsigned char*)strLeft -*(unsigned char*)strRight) && *strRight) { strLeft++,strRight++; } if(ret<0) { ret=-1; } else if(ret>0) { ret=1; } else { ret=0; } return ret; }
其實,面對上面的問題時,只需要記住一個簡單的原則,就是不要在表達式中使用“簡單的”字符。當然,位域也有同樣的問題,因此也有一個類似的原則:任何時候都不要使用“簡單的”位域。例如,下面的代碼在任何編譯程序上都可以工作,因為它沒有對域作假定。
char* strcpy(char *strDst,const char *strSrc) { char *ret = strDst; assert(strDst!=NULL&&strSrc!=NULL); while((*strDst++=*strSrc++)&&strlen(strDst)!=0) NULL; return ret; }
最后,對于編寫可移植程序還有這樣一個問題:有些程序員可能會認為使用可移植的類型比使用“自然的”類型效率更低。例如,假定int類型的物理字長對目標硬件是最有效的。這就意味著這種“自然的”位數可能大于16位,所保持的值也可能大于32767。現在假定用戶的編譯程序使用的是32位的int,且要求使用0至40000的值域。那么,是為了使機器可以在int內有效地處理40000個值而使用int呢,還是堅持使用可移植類型,而用long代替int呢?
其實這要具體情況具體分析。
對于對效率要求比較高的程序。大家一致認為如果能夠使用char定義的變量,就不要使用int定義的變量;能夠使用int定義的變量,就不要用long變量來定義;能不使用浮點型變量就不要使用浮點型變量。當然,在定義變量后不要讓變量超過其作用范圍,如果超過變量的范圍賦值,C語言編譯器并不會報錯,但程序的運行結果卻錯了,而且這樣的錯誤很難發現。
如果基于可移植性來考慮,要是機器使用的是32位int,那么也可以使用32位long,因為這兩者產生的代碼即使不相同也很相似,所以我們可以使用long。用戶即便擔心在將來必須支持的機器上使用long可能會效率低一些,那也應該堅持使用可移植類型。
總之,不論怎樣,我們都應該堅持這樣一個原則:那就是盡量使用嚴格形式定義的、可移植的數據類型,盡量不要使用與具體硬件或軟件環境關系密切的變量。