天天看點

虛析構函數(√)、純虛析構函數(√)、虛構造函數(X)

一. 虛析構函數

我們知道,為了能夠正确的調用對象的析構函數,一般要求具有層次結構的頂級類定義其析構函數為虛函數。因為在delete一個抽象類指針的時候,必須要通過虛函數找到真正的析構函數。例如:

#include <iostream>
using namespace std;
class Base
{
public:
Base() {}
virtual ~Base() {cout<<"Base Destruct"<<endl;}
};
class Derived : public Base
{
public:
    Derived() {}
    ~Derived() {cout<<"Derived Destruct"<<endl;}
};
int main()
{
    Base * pb = new Derived();
    delete pb;

    return 0;
}
           

這是正确的用法,會發生動态綁定,它會先調用Derived的析構函數,然後調用Base的析構函數。

如果Base析構函數不加virtual,說明Base的析構函數不是虛析構函數,調用的函數依賴于指向靜态類型(即Base),delete pb隻會執行Base的析構函數,而不是真正的Derived析構函數。

二. 純虛析構函數

現在的問題是,我們想把Base做成抽象類,不能直接構造對象,需要在其中定義一個純虛函數。如果其中沒有其他合适的函數,可以把析構函數定義為純虛的,即将前面的Base定義改成:

class Base
{
public:
    Base() {}
    virtual ~Base() = 0;
};
           

可是,這段代碼不能通過編譯,通常是link錯誤,不能找到~Base()的引用。這是因為,析構函數、構造函數和其他内部函數不一樣,在調用時,編譯器需要産生一個調用鍊。也就是說,Derived的析構函數裡面隐含調用了Base的析構函數。而剛才的代碼中,缺少~Base()的函數體,當然會出現錯誤。

這裡面有一個誤區,有人認為,virtual f()=0這種純虛函數是沒有定義體的。其實,這是不對的。這種文法隻是表明這個函數是一個純虛函數,是以這個類變成了抽象類,不能産生對象。我們完全可以為純虛函數指定函數體。通常的純虛函數不需要函數體,是因為我們一般不會調用抽象類的這個函數,隻會調用派生類的對應函數。這樣,我們就有了一個純虛析構函數的函數體,上面的代碼需要改成:

class Base
{
public:
    Base(){}
    virtual ~Base() = 0; //pure virtual
};
Base::~Base()//function body
{
}
           

從文法角度來說,不可以将上面的析構函數直接寫入類聲明中(内聯函數的寫法)。

這個問題看起來有些學術化,因為一般我們完全可以在Base中找到一個更加适合的函數,通過将其定義為沒有實作體的純虛函數,而将整個類定義為抽象類。但這種技術也有一些應用,如這個例子:

class Base //abstract class
{
public:
    virtual ~Base(){};//virtual, but not pure
    virtual void Hiberarchy() const = 0;//pure virtual
};

void Base::Hiberarchy() const //pure virtual also can have function body
{
    cout <<"Base::Hiberarchy"<<endl;
}

class Derived : public Base
{
public:
    Derived(){}
    virtual void Hiberarchy() const
    {
        Base::Hiberarchy();
        cout <<"Derived::Hiberarchy"<<endl;
    }
};

int main()
{
    Base* pb=new Derived();
    pb->Hiberarchy();
    pb->Base::Hiberarchy();
    return 0;
}
           

在這個例子中,我們試圖列印出類的繼承關系。在基類Base中定義了虛函數Hiberarchy,然後在派生類中重載此函數。我們再一次看到,由于想把Base做成個抽象類,而這個類中沒有其他合适的成員方法可以定義為純虛,我們隻好将Hiberarchy定義為純虛的。(當然,完全可以定義~Base函數,這就和上面的讨論一樣了。)

另外,可以看到,在main中有兩種調用方法,第一種是普通的方式,進行動态連結,執行虛函數,得到結果"Derived::Hiberarchy";第二種是指定類的方式,就不再執行虛函數的動态連結過程了,結果是"Base::Hiberarchy"。

通過上面的分析可以看出,定義純虛函數的真正目的是為了定義抽象類,而并不是函數本身。

最後,總結一下關于虛函數的一些常見問題:

1) 虛函數是動态綁定的,也就是說,使用虛函數的指針和引用能夠正确找到實際類的對應函數,而不是執行定義類的函數。這是虛函數的基本功能。

2) 構造函數不能是虛函數。因為虛函數的意思是對象産生後開啟動态綁定,程式會根據對象的動态類型來選擇要調用的方法。然而在構造函數運作時,這個對象根本不存在,是以不能産生動态綁定。而且,在構造函數中調用虛函數,實際執行的是父類的對應函數,因為自己還沒有構造好, 多态是被disable的。

3) 析構函數可以是虛函數,而且,在一個複雜類結構中,這往往是必須的。

4) 将一個函數定義為純虛函數,實際上是将這個類定義為抽象類,不能執行個體化該類的對象。

5) 純虛函數通常沒有定義體,但也完全可以擁有。

6) 析構函數可以是純虛的,但純虛析構函數必須有定義體,因為析構函數的調用是在子類中隐含的。

7) 非純的虛函數必須有定義體,不然是一個錯誤。

8) 派生類的override虛函數定義必須和父類完全一緻。除了一個特例,如果父類中傳回值是一個指針或引用,子類override時可以傳回這個指針(或引用)的派生。例如,在上面的例子中,在Base中定義了 virtual Base* clone(); 在Derived中可以定義為 virtual Derived* clone()。可以看到,這種放松對于Clone模式是非常有用的。

繼續閱讀