天天看點

Effective C++ 2e Item46

條款46: 甯可編譯和連結時出錯,也不要運作時出錯

除了極少數情況下會使C++抛出異常(例如,記憶體耗盡 ---- 見條款7)外,運作時錯誤的概念和C++沒什麼關系,就象在C中一樣。沒有下溢,上溢,除零檢查;沒有數組越界檢查,等等。一旦程式通過了編譯和連結,你就得靠自己了 ---- 一切後果自負。這很象跳傘運動,一些人從中找到了刺激,另一些人則吓得摔成了殘廢。這一思想背後的動機當然在于效率:沒有運作時檢查,程式會更小更快。

處理這類事情有另一個不同的方法。一些語言如Smalltalk和LISP通常在編譯連結期間隻是檢查極少一些錯誤,但卻提供了強大的運作時系統來處理執行期間的錯誤。不象C++,這些語言幾乎都是解釋型的,在提供額外靈活性的同時,它們也帶來了性能上的損失。

不要忘了你是在用C++程式設計。即使發現Smalltalk/LISP的方法很吸引人,也要忘掉它們。常說要堅持黨的路線,現在的情況下,它的含義就是要避免運作時錯誤。隻要有可能,就要讓出錯檢查從運作時退回到連結時,或者,最理想的是,編譯時。

這種方法帶來的好處不僅僅在于程式的大小和速度,還有可靠性。如果程式通過了編譯和連結而沒有産生錯誤資訊,你就可以确信程式中沒有編譯器和連結器能檢查得到的任何錯誤,僅此而已。(當然,另一個可能性是,編譯器或連結器有問題,但不要拿這種可能性來困擾我們。)

對于運作時錯誤來說,情況大不一樣。在某次運作期間程式沒有産生任何運作時錯誤,你就能确信另一次不同的運作期内不會産生錯誤嗎?比如:在另一次運作中,你以不同的順序做事,或者采用不同的資料,或者運作更長或更短時間,等等。你可以不停地測試自己的程式直到面色發紫,但你還是不能覆寫所有的可能性。因而,運作時發現錯誤比在編譯連結期間檢查錯誤更不能讓人放心。

通常,對設計做一點小小的改動,就可以在編譯期間消除可能産生的運作時錯誤。這常常涉及到在程式中增加新的資料類型(參見條款M33)。例如,假設想寫一個類來表示時間中的日期,最初的做法可能象這樣:

class Date {

public:

  Date(int day, int month, int year);

  ...

};

準備實作這個構造函數,面臨的一個問題是對day和month值的合法性檢查。讓我們來看看,對于傳給month的值來說,怎麼做可以免于對它進行合法性檢查呢?

一個明顯的辦法是采用枚舉類型而不用整數:

enum Month { Jan = 1, Feb = 2, ... , Nov = 11, Dec = 12 };

class Date {

public:

  Date(int day, Month month, int year);

  ...

};

遺憾的是,這不會換來多少好處,因為枚舉類型不需要初始化:

Month m;

Date d(22, m, 1857);      // m是不确定的

是以,Date構造函數還是得驗證month參數的值。

既想免除運作時檢查,又要保證足夠的安全性,你就得用一個類來表示month,你就得保證隻有合法的month才被建立:

class Month {

public:

  static const Month Jan() { return 1; }

  static const Month Feb() { return 2; }

  ...

  static const Month Dec() { return 12; }

  int asInt() const           // 為了友善,使Month

  { return monthNumber; }     // 可以被轉換為int

private:

  Month(int number): monthNumber(number) {}

  const int monthNumber;

};

class Date {

public:

  Date(int day, const Month& month, int year);

  ...

};

這個設計在幾個方面的特點綜合确定了它的工作方式。首先,Month構造函數是私有的。這防止了使用者去建立新的month。可供使用的隻能是Month的靜态成員函數傳回的對象,再加上它們的拷貝。第二,每個Month對象為const,是以它們不能被改變(否則,很多地方會忍不住将一月轉換成六月,特别是在北半球)。最後一點,得到Month對象的唯一辦法是調用函數或拷貝現有的Month(通過隐式Month拷貝構造函數 ---- 見條款45)。這樣,就可以在任何時間任何地方使用Month對象;不必擔心無意中使用了沒有被初始化的對象。(否則就可能有問題。條款47進行了說明)

有了這些類,使用者幾乎不可能指定一個非法的month,甚至完全不可能 ---- 如果不出現下面這種可惡的情況的話:

Month *pm;                 // 定義未被初始化的指針

Date d(1, *pm, 1997);      // 使用未被初始化的指針!

但這種情況所涉及的是另一個問題,即通過未被初始化的指針取值,其結果是不可确定的。(參見條款3,看看我對 "不确定行為" 的感受)遺憾的是,我沒有辦法來防止或檢查這種異端行為。但是,如果假設這種情況永遠不會發生,或者如果我們不考慮這種情況下軟體的行為,Date構造函數對它的Month參數就可以免于合法性檢查。另一方面,構造函數還是必須檢查day參數的合法性 ---- 九月,四月,六月和十一月各有多少天呢?

Date的例子将運作時檢查用編譯時檢查來取代。你可能想知道什麼時候可以使用連結時檢查。實際上,不是經常這麼做。C++用連結器來保證所需要的函數隻被定義一次(參見條款45,"需要" 一個函數會帶來什麼)。它還使用連結器來保證靜态對象(參見條款47)隻被定義一次。你可以用同樣的方法使用連結器。例如,條款27說明,對于一個顯式聲明的函數,如果想有意禁止對它進行定義,連結器檢查就很有用。

但不要過于強求。想消除所有的運作檢查是不切實際的。例如,任何允許互動式輸入的程式都要進行輸入驗證。同樣地,某個類中如果包含需要執行上下限檢查的數組,每次通路數組時就要對數組下标進行檢查。盡管如此,将檢查從運作時轉移到編譯或連結時一直是值得努力的目标,隻要實際可行,就要追求這一目标。這樣做的獎賞是,程式會更小,更快,更可靠。