一. 編譯器何時為類產生合適的特殊預設函數
當聲明如下一個空類時:
class CA {};
一般認為C++編譯會在背後默默幫你產生5個函數:預設建構函式,拷貝建構函式,解構函式,賦值運算子多載函數,取地址運算子多載函數,結果類被擴充為如下形式:
class CA()
{
public:
CA() {...}
CA(const CA& other) {...}
~CA() {...}
CA& operator=(const CA& other) {...}
CA* operator&() {...}
}
但實際情況並非如此,編譯器只有認為需要的時候才產生相應函數,這體現了C++的效率至上理念,對於如上空類根本就不需要產生任何函數,因為這些預設函數沒有任何有意義的事可做,當你在類裡顯式聲明了任何類型的建構函式時(包括拷貝建構函式),編譯器便不會為你產生預設建構函式,同樣其他四個函數當在類裡有顯式聲明時,編譯器都不會產生預設的,這裡要特彆強調一下operator&()函數,可能很多人不知道它的存在,如果你不去顯式申明,確實沒有存在的必要,就是擷取對象的地址,但你在類裡顯式定義後,也許會改變它的預設意義,可能會造成使用者的困惑, 例如:
class CA { CA* operator&() {...}};
CA a;
CA* p = &a; //這裡&a等價與a.operator&()調用,其意義完全取決於設計者的實現,而未必就是取對象a的地址。
那麼除去operator&(),在沒有顯式申明其他函數的前提下,編譯器何時會為你產生這些特殊函數呢,以下列舉幾種常見情況(更多資訊可參考《深度探索C++物件模型》):
1. 類裡聲明了虛函數
2. 基類體系某一個裡申明了虛函數
3. 基類體系裡某一個裡顯式定義了相應函數
4. 含有類成員,類成員出現了以上情況之一
二. 拷貝構造/賦值操作注意事項
當一個類裡申明了一個引用成員或const成員,必須顯式定義operator=(...)才能處理對象賦值問題,否則編譯無法通過,如下所示:
class CB
{
public:
CB() : a(1) , b(a) {}
int a;
int& b;
};
CB b , a;
a = b; //無法通過編譯
當一個衍生類別顯式定義了拷貝建構函式或operator=(...),他們並不會預設的去調基類的相應函數,這可能與你的預期不一致,導致對象拷貝賦值的不完全,如下所示:
class Base
{
public:
Base() {...}
Base(const Base& other) : a(other.a) {}
Base& operator=(const Base& other) { a = other.a; }
int a;
};
class Derive : public Base
{
public:
Derive() {...}
Derive(const Derive& other) : b(other.b) {...} //不是調用Base的拷貝建構函式,而是調用無參建構函式Base(),導致Base部分沒有拷貝
Derive& operator=(const Derive& other) { b = other.b; } //沒有調用基類operator=(...)函數
int b;
}
要想實現完全拷貝,需要自己顯式申明:
class Derive : public Base
{
public:
Derive() {...}
Derive(const Derive& other) : Base(other), b(other.b) {...}
Derive& operator=((const Derive& other) { Base::operator=(other); b = other.b; }
int b;
}
三. 虛擬解構函式
在C++裡,建構函式是沒辦法被顯式調用的,只能由編譯器調用,但解構函式是可以被顯式調用的。一個類是否都應該有一個顯式的虛擬解構函式呢?不一定,只有當該類需要當作基類,而且需要delete一個該基類指標,而該基類指標實際指向其衍生類別時,才需要,目的是防止記憶體流失,當不存在如此情況時,若不需要在解構函式裡做什麼工作,可完全不聲明,由編譯器去決定是否需要產生一個預設的解構函式。解構函式有時會聲明為純虛的,這一般出現在定義一個不能執行個體化的類,但該類除了解構函式沒有其他的虛函數,此時可將解構函式聲明為純虛的。
四. 構造/解構函式裡原則上不應該有的操作
在構造/解構函式裡一般不要調用虛函數,其行為一般非你所預期,這依賴於實現,目前的實現來講,不會發生多態行為,而是像調用一個普通成員函數一樣直接產生編譯期綁定,如下例:
class Base
{
public:
Base() { test(); }
virtual void test() {cout << "Base::test" << endl; }
};
class Derive : public Base
{
public:
Derive() { test(); }
virtual void test() { cout << "Derive::test" << endl; }
};
Derive d;
輸出:Base::test
Derive::test
另不要在構造/析構完成過複雜容易引起異常的操作。
五. 限制對象建立與複製
當想讓一個對象只能在棧上分配,而禁止在堆上分配時,可以通過將opertor new(size_t size)申明為類的私人函數方式實現,反過來可通過申明類的建構函式為私人的,同時定義一個靜態工廠函數,實現裡通過new的方式返回對象指標即可,這種方式也能控制對象建立的數量,當然還有其他的方法,對對象建立的控制無論是建立方式還是數量都基本可通過顯式定義operator new(...)/operator delete(...), 控制構造/解構函式的存取權限,另外可能輔助一些靜態工廠函數來實現。當想禁止一個對象的複製時,可通過將拷貝建構函式及operator=(...)顯式申明為私人的。
六. 成員初始化列表機制
一個類裡成員的初始化有兩種方式,常見的是在建構函式裡初始化,但這並非真正的初始化,因為成員在進入建構函式體之前已完成了預設的初始化工作,在建構函式內都只能算賦值動作,如果想真正顯式執行特定的類成員初始化動作,可採用第二種初始化列表機制,如下類:
class CA
{
public:
CA() {...}
CA(int a) {...}
};
class CB
{
public:
CB(int a) : ca(a) {...} //通過初始化列表機制顯式指定相應ca的構造方法
CA ca;
}
需要注意的是初始化是按照成員在類裡申明順序初始化的,這有時可能會導致一些隱晦的錯誤,例如如下定義:
class CA
{
public:
CA(): j(1),i(j) {}
private:
int i;
int j;
};
這裡雖然j的初始化放在i之前,但由於聲明時i在j之前,所以i會先初始化,而此時j處於不確定狀態。
另外需要注意的是有些類成員只能通過成員初始化列表機制初始化,有如下幾種常見情況:
1. 類成員沒有預設建構函式或可不帶參數調用的建構函式(註:這裡沒有說無參建構函式是考慮到預設參數的存在);
2. 類成員為參考型別;
3. 類成員為const修飾型;
七. 類對象的構造過程
為了下面敘述方便,先提出一個擴充建構函式的概念,由編譯器在你編寫的實際建構函式前插入必要代碼構成,當你定義一個類對象時,實際上初始化是通過調用擴充建構函式完成,在進入你所編寫的建構函式之前,這段必要代碼會完成基類擴充建構函式,類類型成員擴充建構函式調用,虛表指標初始化等關鍵工作,那麼一個最具普遍意義的類對象的構造過程如下:
1. 調用類的擴充建構函式,當有繼承存在時進行步驟2,否則到步驟3;
2. 當不存在虛擬繼承時,調用基類的擴充建構函式,當存在多個基類時,按照申明順序依次調用,基類的構造行為同樣,這樣就保證了按照繼承樹從跟往下構造的順序;
存在虛擬繼承的情況比較複雜一點,平時很少用到,這裡不細說了,具體細節可參看《深度探索C++物件模型》,這裡指出一點技巧,可以利用虛擬繼承的相關特性設計一個不能被繼承使用的類。
3. 當存在自訂類型成員時,按照其在類裡的申明順序依次調用相應的擴充建構函式;
4. 進入你編寫的實際建構函式完成整個構造過程;
下面舉例說明:
class Base1
{
public:
Base1() { cout << "Base1 Constructor..." << endl; }
};
class Base2
{
public:
Base2() { cout << "Base2 Constructor..." << endl; }
};
class Member
{
public:
Member() { cout << "Member Constructor..." << endl; }
};
class Derive : public Base1 , pubic Base2
{
public:
Derive() { cout << "Derive Construcotor..." << endl; }
Member member;
};
Derive d;
輸出:
Base1 Constructor...
Base2 Constructor...
Member Constructor...
Derive Constructor...