構造函數、析構函數與賦值函數是每個類最基本的函數。它們太普通以致讓人容易麻痺大意,其實這些貌似簡單的函數就象沒有頂蓋的下水道那樣危險。 每個類只有一個析構函數和一個賦值函數,但可以有多個構造函數(包含一個拷貝構造函數,其它的稱為普通構造函數)。對於任意一個類A,如果不想編寫上述函數,C++編譯器將自動為A產生四個缺省的函數,如
A(void); // 缺省的無參數構造函數
A(const A &a); // 缺省的拷貝構造函數
~A(void); // 缺省的析構函數
A & operate =(const A &a); // 缺省的賦值函數
這不禁讓人疑惑,既然能自動生成函數,為什麼還要程序員編寫?
原因如下:
(1)如果使用“缺省的無參數構造函數”和“缺省的析構函數”,等於放棄了自主“初始化”和“清除”的機會,C++發明人Stroustrup的好心好意白費了。
(2)“缺省的拷貝構造函數”和“缺省的賦值函數”均采用“位拷貝”而非“值拷貝”的方式來實現,倘若類中含有指針變量,這兩個函數注定將出錯。
對於那些沒有吃夠苦頭的C++程序員,如果他說編寫構造函數、析構函數與賦值函數很容易,可以不用動腦筋,表明他的認識還比較膚淺,水平有待於提高。
本章以類String的設計與實現為例,深入闡述被很多教科書忽視了的道理。String的結構如下:
class String
{
public:
String(const char *str = NULL); // 普通構造函數
String(const String &other); // 拷貝構造函數
~ String(void); // 析構函數
String & operate =(const String &other); // 賦值函數
private:
char *m_data; // 用於保存字符串
};
9.1 構造函數與析構函數的起源
作為比C更先進的語言,C++提供了更好的機制來增強程序的安全性。C++編譯器具有嚴格的類型安全檢查功能,它幾乎能找出程序中所有的語法問題,這的確幫了程序員的大忙。但是程序通過了編譯檢查並不表示錯誤已經不存在了,在“錯誤”的大家庭裡,“語法錯誤”的地位只能算是小弟弟。級別高的錯誤通常隱藏得很深,就象狡猾的罪犯,想逮住他可不容易。
根據經驗,不少難以察覺的程序錯誤是由於變量沒有被正確初始化或清除造成的,而初始化和清除工作很容易被人遺忘。Stroustrup在設計C++語言時充分考慮了這個問題並很好地予以解決:把對象的初始化工作放在構造函數中,把清除工作放在析構函數中。當對象被創建時,構造函數被自動執行。當對象消亡時,析構函數被自動執行。這下就不用擔心忘了對象的初始化和清除工作。
構造函數與析構函數的名字不能隨便起,必須讓編譯器認得出才可以被自動執行。Stroustrup的命名方法既簡單又合理:讓構造函數、析構函數與類同名,由於析構函數的目的與構造函數的相反,就加前綴‘~’以示區別。
除了名字外,構造函數與析構函數的另一個特別之處是沒有返回值類型,這與返回值類型為void的函數不同。構造函數與析構函數的使命非常明確,就象出生與死亡,光溜溜地來光溜溜地去。如果它們有返回值類型,那麼編譯器將不知所措。為了防止節外生枝,干脆規定沒有返回值類型。(以上典故參考了文獻[Eekel, p55-p56])