C++呼叫虛擬函式的注意事項

2020-07-16 10:04:21
當在類的內部呼叫虛擬函式時,需要注意如下幾點。

在成員函數中呼叫虛擬函式

類的成員函數之間可以互相呼叫。在成員函數(靜態成員函數、建構函式和解構函式除外)中呼叫其他虛成員函數的語句是多型的。例如下面的程式:
#include <iostream>
using namespace std;
class CBase
{
public:
    void func1()
    {
        func2();
    }
    virtual void func2()  {cout << "CBase::func2()" << endl;}
};
class CDerived:public CBase
{
public:
    virtual void func2() { cout << "CDerived:func2()" << endl; }
};
int main()
{
    CDerived d;
    d.func1();
    return 0;
}
程式的輸出結果如下:
CDerived:func2()

第 20 行呼叫 func1 成員函數。進入 func1 成員函數,執行到第 8 行,呼叫 func2 函數。看起來呼叫的應該是 CBase 類的 func2 成員函數,但輸出結果證明實際上呼叫的是 CDerived 類的 func2 成員函數。

這是因為,在 func1 函數中,func2();等價於this -> func2();,而 this 指標顯然是 CBase* 型別的,即是一個基礎類別指標,那麼this -> func2();就是在通過基礎類別指標呼叫虛擬函式,因此這條函數呼叫語句就是多型的。

當本程式執行到第 8 行時,this 指標指向的是一個 CDerived 類的物件,即 d,因此被呼叫的就是 CDerived 類的 func2 成員函數。

在建構函式和解構函式中呼叫虛擬函式

在建構函式和解構函式中呼叫虛擬函式不是多型,因為編譯時即可確定呼叫的是哪個函數。如果本類有該函數,呼叫的就是本類的函數;如果本類沒有,呼叫的就是直接基礎類別的函數;如果直接基礎類別沒有,呼叫的就是間接基礎類別的函數,以此類推。

請看下面的程式:
#include <iostream>
using namespace std;
class A
{
public:
    virtual void hello() { cout << "A::hello" << endl; }
    virtual void bye() { cout << "A::bye" << endl; }
};
class B : public A
{
public:
    virtual void hello() { cout << "B::hello" << endl; }
    B() { hello(); }
    ~B() { bye(); }
};
class C : public B
{
public:
    virtual void hello() { cout << "C::hello" << endl; }
};
int main()
{
    C obj;
    return 0;
}
程式的輸出結果如下:
B::hello
A::bye

類 A 派生出類 B,類 B 派生出類 C。

第 23 行,obj 物件生成時會呼叫類 B 的建構函式,在類 B 的建構函式中呼叫 hello 成員函數。由於在建構函式中呼叫虛擬函式不是多型,所以此時不會呼叫類 C 的 hello 成員函數,而是呼叫類 B 自己的 hello 成員函數。

obj 物件消亡時,會引發類 B 解構函式的呼叫,在類 B 的解構函式中呼叫了 bye 函數。類B沒有自己的 bye 函數,只有從基礎類別 A 繼承的 bye 函數,因此執行的就是類 A 的 bye 函數。

將在建構函式中呼叫虛擬函式實現為多型是不合適的。以上面的程式為例,obj 物件生成時,要先呼叫基礎類別建構函式初始化其中的基礎類別部分。在基礎類別建構函式的執行過程中,派生類部分還未完成初始化。此時,在基礎類別 B 的建構函式中呼叫派生類 C 的 hello 成員函數,很可能是不安全的。

在解構函式中呼叫虛擬函式不能是多型的原因也與此類似,因為執行基礎類別的解構函式時,派生類的解構函式已經執行,派生類物件中的成員變數的值可能已經不正確了。

注意區分多型和非多型的情況

初學者往往弄不清楚一條函數呼叫語句是否是多型的。要注意,通過基礎類別指標或參照呼叫成員函數的語句,只有當該成員函數是虛擬函式時才會是多型。如果該成員函數不是虛擬函式,那麼這條函數呼叫語句就是靜態聯編的,編譯時就能確定呼叫的是哪個類的成員函數。

另外,C++ 規定,只要基礎類別中的某個函數被宣告為虛擬函式,則派生類中的同名、同參數列的成員函數即使前面不寫 virtual 關鍵字,也自動成為虛擬函式。

例如下面的程式:
#include <iostream>
using namespace std;
class A
{
public:
    void func1() { cout<<"A::func1"<<endl; };
    virtual void func2() { cout<<"A::func2"<<endl; };
};
class B:public A
{
public:
    virtual void func1() { cout << "B::func1" << endl;  };
    void func2() { cout << "B::func2" << endl; }  //func2自動成為虛擬函式
};
class C:public B  // C以A為間接基礎類別
{
public:
    void func1() { cout << "C::func1" << endl; }; //func1自動成為虛擬函式
    void func2() { cout << "C::func2" << endl; }; //func2自動成為虛擬函式
};
int main()
{
    C obj;
    A *pa = &obj;
    B *pb = &obj;
    pa->func2();  //多型
    pa->func1();  //不是多型
    pb->func1();  //多型
    return 0;
}
程式的輸出結果如下:
C::func2
A::func1
C::func1

基礎類別 A 中的 func2 是虛擬函式,因此派生類 B、C 中的 func2 宣告時雖然沒有寫 virtual 關鍵字,也都自動成為虛擬函式。所以第 26 行就是一個多型的函數呼叫語句,呼叫的是 C 類的 func2 成員函數。

基礎類別 A 中的 func1 不是虛擬函式,因此第 27 行就不是多型的。編譯時,根據 pa 的型別就可以確定 func1 就是類 A 的成員函數。

func1 在類 B 中成為虛擬函式,因此在類 B 的直接和間接派生類中,func1 都自動成為虛擬函式。因此,第 28 行,pb 是基礎類別指標,func1 是基礎類別 B 和派生類 C 中都有的同名、同參數列的虛擬函式,故這條函數呼叫語句就是多型的。