條款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/t // 為了方便,使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;/t/t // 定義未被初始化的指針
Date d(1, *pm, 1997); // 使用未被初始化的指針!
但這種情況所涉及的是另一個問題,即通過未被初始化的指針取值,其結果是不可確定的。(參見條款3,看看我對 "不確定行為" 的感受)遺憾的是,我沒有辦法來防止或檢查這種異端行為。但是,如果假設這種情況永遠不會發生,或者如果我們不考慮這種情況下軟件的行為,Date構造函數對它的Month參數就可以免于合法性檢查。另一方面,構造函數還是必須檢查day參數的合法性 ---- 九月,四月,六月和十一月各有多少天呢?
Date的例子將運行時檢查用編譯時檢查來取代。你可能想知道什么時候可以使用鏈接時檢查。實際上,不是經常這么做。C++用鏈接器來保證所需要的函數只被定義一次(參見條款45,"需要" 一個函數會帶來什么)。它還使用鏈接器來保證靜態對象(參見條款47)只被定義一次。你可以用同樣的方法使用鏈接器。例如,條款27說明,對于一個顯式聲明的函數,如果想有意禁止對它進行定義,鏈接器檢查就很有用。
但不要過于強求。想消除所有的運行檢查是不切實際的。例如,任何允許交互式輸入的程序都要進行輸入驗證。同樣地,某個類中如果包含需要執行上下限檢查的數組,每次訪問數組時就要對數組下標進行檢查。盡管如此,將檢查從運行時轉移到編譯或鏈接時一直是值得努力的目標,只要實際可行,就要追求這一目標。這樣做的獎賞是,程序會更小,更快,更可靠。
|
新聞熱點
疑難解答
圖片精選