正確編寫C++程式碼的十大要訣(1)

veldts發表於2011-11-06

《深入淺出C++(第2版)》作者原文:Top Ten Tips for Correct C++ Coding

感謝李鬆峰,部分用詞參考了正確編寫C++程式碼的10個要點(2-1)一文。

enter image description here By Brian Overland @May 17, 2011

Brian Overland,曾在微軟工作十年,《深入淺出C++(第2版)》C++ Without Fear: A Beginner's Guide That Makes You Feel Smart, 2nd Edition)一書作者,分享了自己數十年來編寫和除錯C++程式碼悟到的,10條來之不易、省時省力的要訣。

我第一次接觸C語言是在幾十年前(好吧,我知道這讓我很顯老)。後來我又學了C++。真希望那個時候有人能帶著我避開那些顯而易見的陷阱,這樣也許我可以免受不少煎熬。

至少現在我可以提供幾條這類必需的(抱歉用這個詞)指引。本文不是C++教程,而是面向C++學習者的指南。不過老實說,對於C++,總有學不完的東西。

要想編寫優美專業的C++程式碼,且容易維護,也不大需要除錯,務請牢記以下十條要訣,為此本文的要求有些比我在《深入淺出C++(第2版)》(C++ Without Fear (2/e))裡寫的還要嚴格。

這些準則不分先後(抱歉,David Letterman),不過前幾條準則更多是針對困擾初學者的錯誤。


1:不要搞混賦值(=)和相等測試(==)

這一條非常基礎,儘管可能難倒過福爾摩斯。下面這段程式碼看似正常,而且C++要是更像BASIC的話,編譯和執行也沒問題。

if (a = b)
    cout << "a is equal to b.";

正因為上面的程式碼表面上看起來沒什麼問題,在大型程式裡,它引起的邏輯錯誤需要耗費數小時才能定位,除非你事先對它有所警覺。(因此我在除錯程式時第一時間會排查這類錯誤。)在C和C++裡,下面的表示式並非測試兩個變數是否相等:

a = b

這個表示式實則是將b的值賦給a,然後再求出所賦的值。

問題在於a = b的求值結果並不總是合理的真/假條件,除了我後面將提到的一大例外。但在C和C++裡,任意數值都可以用作if或while語句的條件。

假定a和b的值都是0。前面給出的if語句效果等同於將b的值傳給a;然後表示式a = b求值為0。而值0等於false。結果,a和b相等,卻列印了錯誤的訊息:

if (a = b)     // 這樣a和b顯然相等...
    cout << "a and b are equal.";
else
    cout << "a and b are not equal.";  // 結果列印的卻是這條訊息!

解決辦法自然是根據需要使用相等測試。注意要用兩個等號(==),在條件裡用這個運算子才對。

// 正確版本:
if (a == b)
    cout << "a and b are equal.";

2:杜絕“幻數”

所謂幻數(又稱魔數、魔術數字)是指散落於程式中未加註釋的字面值(literal number)。多數老到的程式設計師更樂見程式裡只有諸如MAX_ROWS、SCREEN_WIDTH的符號名。

總之,專業的計算機程式設計師——包括一些對數學情有獨鍾的人——其實討厭數字!

箇中緣由與歷史有點關係。遙想上世紀40年代,那時只能用各種位元位組合形成的機器碼程式設計。程式設計師生不如死,必須不停地轉換這些組合。隨後組合語言出現,程式設計用上了符號名,比之前要容易千百倍。

即使到了今天,程式設計師們也不喜歡這樣的宣告:

char input_string[81];

其中81是個“幻數”。它從哪兒來?更好的做法是用#define或enum語句限制數字的使用。

#define SCREEN_WIDTH 80

比起81,SCREEN_WIDTH更加一目瞭然,如果將來打算重新設定這個寬度,你只需修改一行程式碼。隨後,寬度修改會自動體現到如下語句:

int input_string[SCREEN_WIDTH + 1];

3:不要依賴整數除法(除非有意為之)

不需要儲存小數部分時,優先選用整型(在C/C++裡,即int,以及short、long還有long long),有許多充足的理由,這裡就不具體展開了。

不過,有時整數是一個含有小數的更長表示式的一部分。下面的程式碼來自拙作《深入淺出C++(第2版)》(C++ Without Fear):

cout << results / (n / 10);

這個程式會生成0到9之間的隨機數,每個數字出現的概率應該是1/10。這裡是用每個數字實際出現的次數除以期望出現次數N/10。比如,如果3的results(實際出現的次數)是997,而總共進行了10 000次測試,那麼就是用997除以期望出現的次數1000。

但results、n和10都是整數。結果,997除以1000得到零!

等等,我們本來預想的是0.997,到底怎麼回事?

整數除法向下舍入,得到一個最接近的整數。餘數則扔掉了。這也不一定是壞事。C++提供了兩個獨立的除法運算:除和求餘。

int dividend = n / m;   // 求比例.
int remainder = n % m;  // 求餘數.

對了,前面那行程式碼使用了一個“幻數”——10,我暈!不過,下面我們就來說說這個更麻煩的問題:資料丟失。

4:利用資料型別提升控制結果

在混有整數和浮點數的表示式裡,整型運算元會被提升為double型。因此,下面的表示式會產生我們想要的結果:

cout << results / (n / 10.0);

注意10.0的小數部分雖然為0,但仍以double型儲存,在C++中,這會導致n和results也提升為double型,然後執行浮點除法。

利用資料強制轉型也可取得以上效果:

cout << results / (n / (double) 10);
cout << results / (n / static_cast<double>(10));

5:不要使用非布林條件(除非格外小心)

C語言的設計初衷是為了編寫作業系統,因此它賦予程式設計師很大的自由,不僅可以在機器碼層面(藉助指標)操控資料,還可以寫出簡寫形式。簡寫形式對初學者很危險,但對知道該怎麼用的程式設計師來說,有時非常便捷。但願他們心甘情願地活在危險之中。

下面這段程式碼展示了其中最優雅的一項技巧,也是實現“某個動作執行N遍”的捷徑:

while (n--) {
    do_something();
}

還可以進一步簡寫如下:

while (n--) do_something();

同樣,不管怎麼寫,我們都是在利用C和C++裡任意數字值都可用作條件這一規則。迴圈每執行一次n都會遞減1,直到n為0,迴圈結束。但問題在於:要是n初值為負會怎麼樣?我剛才展示的程式碼就會陷入死迴圈,或者至少一直迴圈至最小的負值,直到溢位為止。那會讓你鬱悶透頂。

總之,只有那些嚴格意義上的布林(即真/假)表示式才能用作條件。

while (n-- > 0) {
    do_something();
}

不過也有一大例外。在面對操作失敗指標被置為NULL(也即0)時,這種簡寫形式很有用處。NULL實際上等同於false。空指標還可用於連結表,指示連結串列結束,節點的next_node成員指向空(nowhere,上世紀五六十年代,說的是指向Nowheresville)。在下列程式碼中,空指標意味著檔案開啟失敗:

if (! file_pointer) {
    cout << "File open failed.";
    return ERROR_CODE;
}

順便提一句,有時你可能會在條件裡用到賦值操作:

if (! (file_pointer = open_the_file(args))) {
    cout << "File open failed.";
    return ERROR_CODE;
}

相關文章:

本文參加 Translate Geeks to Chinese 翻譯活動

相關文章