我們知道,爲了能夠正確的調用對象的析構函數,一般要求具有層次結構的頂級類定義其析構函數爲虛函數。因爲在delete一個抽象類指針時候,必須要通過虛函數找到真正的析構函數。
如:
{
public:
Base(){}
virtual ~Base(){}
};
class Derived: public Base
{
public:
Derived(){};
~Derived(){};
}
void foo()
{
Base *pb;
pb = new Derived;
delete pb;
}
如果析構函數不加virtual,delete pb只會執行Base的析構函數,而不是真正的Derived析構函數。
因爲不是virtual函數,所以調用的函數依賴於指向靜態類型,即Base
二. 純虛析構函數
現在的問題是,我們想把Base做出抽象類,不能直接構造對象,需要在其中定義一個純虛函數。如果其中沒有其他合適的函數,可以把析構函數定義爲純虛的,即將前面的CObject定義改成:
{
public:
Base(){}
virtual ~Base()= 0
};
這裏面有一個誤區,有人認爲,virtual f()=0這種純虛函數語法就是沒有定義體的語義。
其實,這是不對的。這種語法只是表明這個函數是一個純虛函數,因此這個類變成了抽象類,不能產生對象。我們完全可以爲純虛函數指定函數體 。通常的純虛函數不需要函數體,是因爲我們一般不會調用抽象類的這個函數,只會調用派生類的對應函數。這樣,我們就有了一個純虛析構函數的函數體,上面的代碼需要改成:
{
public:
Base()
{
}
virtual ~Base() = 0; //pure virtual
};
Base::~Base()//function body
{
}
這個問題看起來有些學術化,因爲一般我們完全可以在Base中找到一個更加適合的函數,通過將其定義爲沒有實現體的純虛函數,而將整個類定義爲抽象類。但這種技術也有一些應用,如這個例子:
{
public:
virtual ~Base(){};//virtual, not pure
virtual void Hiberarchy() const = 0;//pure virtual
};
void Base::Hiberarchy() const //pure virtual also can have function body
{
std::cout <<"Base::Hiberarchy";
}
class Derived : public Base
{
public:
Derived(){}
virtual void Hiberarchy() const
{
CB::Hiberarchy();
std::cout <<"Derived::Hiberarchy";
}
virtual void foo(){}
};
int main(){
Base* pb=new Derived();
pb->Hiberarchy();
pb->Base::Hiberarchy();
return 0;
}
另外,可以看到,在main中有兩種調用方法,第一種是普通的方式,進行動態鏈接,執行虛函數,得到結果"Derived::Hiberarchy";第二種是指定類的方式,就不再執行虛函數的動態鏈接過程了,結果是"Base::Hiberarchy"。
通過上面的分析可以看出,定義純虛函數的真正目的是爲了定義抽象類,而並不是函數本身。與之對比,在java中,定義抽象類的語法是 abstract class,也就是在類的一級作指定(當然虛函數還是也要加上abstract關鍵字)。是不是這種方式更好一些呢?在Stroustrup的《C++語言的設計與演化》中我找到這樣一段話:
“我選擇的是將個別的函數描述爲純虛的方式,沒有采用將完整的類聲明定義爲抽象的形式,這是因爲純虛函數的概念更加靈活一些。我很看重能夠分階段定義類的能力;也就是說,我發現預先定義一些純虛函數,並把另外一些留給進一步的派生類去定義也是很有用的”。
我還沒有完全理解後一句話,我想從另外一個角度來闡述這個概念。那就是,在一個多層複雜類結構中,中間層次的類應該具體化一些抽象函數,但很可能並不是所有的。中間類沒必要知道是否具體化了所有的虛函數,以及其祖先已經具體化了哪些函數,只要關注自己的職責就可以了。也就是說,中間類沒必要知道自己是否是一個真正的抽象類,設計者也就不用考慮是否需要在這個中間類的類級別上加上類似abstract的說明了。
當然,一個語言的設計有多種因素,好壞都是各個方面的。這只是一個解釋而已。
最後,總結一下關於虛函數的一些常見問題:
1) 虛函數是動態綁定的,也就是說,使用虛函數的指針和引用能夠正確找到實際類的對應函數,而不是執行定義類的函數。這是虛函數的基本功能,就不再解釋了。
2) 構造函數不能是虛函數。而且,在構造函數中調用虛函數,實際執行的是父類的對應函數,因爲自己還沒有構造好, 多態是被disable的。
3) 析構函數可以是虛函數,而且,在一個複雜類結構中,這往往是必須的。
4) 將一個函數定義爲純虛函數,實際上是將這個類定義爲抽象類,不能實例化對象。
5) 純虛函數通常沒有定義體,但也完全可以擁有。
6) 析構函數可以是純虛的,但純虛析構函數必須有定義體,因爲析構函數的調用是在子類中隱含的。
7) 非純的虛函數必須有定義體,不然是一個錯誤。
8) 派生類的override虛函數定義必須和父類完全一致。除了一個特例,如果父類中返回值是一個指針或引用,子類override時可以返回這個指針(或引用)的派生。例如,在上面的例子中,在Base中定義了 virtual Base* clone(); 在Derived中可以定義爲 virtual Derived* clone()。可以看到,這種放鬆對於Clone模式是非常有用的。
其他,有待補充。