C++ 解析 - 虛擬函數 Virtual Function

C++ 解析 主目錄
建構元與解構元
虛擬函數
函數


虛擬函數

當我們談到虛擬函數 (virtual function 或 virtual method) 時, 總是會和 "繼承" 與 "多型" 牽扯在一起. 虛擬函數就是指一個函數的行為可以在其所屬類別之衍生類別中被另一個和該函數具有相同簽章(signature) 之同名函數所重新設計和替換掉. 換句話說, 虛擬函數存在的目的就是讓衍生類別可以自行設計修改原有之函數行為.



VTABLE / VPOINTER

虛擬函數的運作直接和 VPTR 和 VTABLE 有關。當類別的宣告中含有一個以上的虛擬函數時,編譯器就會為這個類別產生這兩樣東西:VTABLE 裡面存的是這個類別中所有的虛擬函數之位址,VPTR則是一個指向VTABLE的指標。編譯器處理/控制虛擬函數的方法是在每一個類別附加一個隱藏的data member (VPTR) 在此一data member中存放一個指向記載這些虛擬函數記憶體位址的一個特殊矩陣(VTABLE); 一個類別只有一個VTABLE,該 table 基本上就是編譯器在編譯該程式時產生的一個簡單的靜態 (static) 矩陣。基底類別和衍生類別都各自擁有自己的VTABLE.

類別的建構函數會令VPTR 指向VTABLE,當系統執行到程式在呼叫一個虛擬函數時, 系統會讓程式尋找該一物件object中所存放的vpointer並導引到相對應的 VTABLE 去取得該虛擬函數程式碼所在的位址並執行此一程式碼,這就是系統執行遇到虛擬函數時的處理運作過程。

如果衍生類別重新定義了某一虛擬函數, 則會在其VTABLE中更新該新虛擬函數之位址, 否則仍然存放該虛擬函數在基底類別的VTABLE中相同的位址. 如果衍生類別定義了新的虛擬函數, 則會在其VTABLE中加入該新虛擬函數之位址.

程式執行到一個衍生物件並開始建構時, 它的基底物件會先被建構起來並產生其VTABLE. 如果衍生類別有重新定義某個虛擬函數, 則衍生物件的建構函數會在該衍生物件的 VTABLE中更新被重新定義虛擬函數之位址. 這就是你不應該在建構函數中呼叫虛擬函數的原因: 衍生物件中被重定義的虛擬函數之位址可能還沒有被放入/更改到衍生物件之VTABLE中. 你可能執行到舊的虛擬函數.

C++的原則是 VPTR 會指向設定它的建構函數所屬類別的 VTABLE 。所以當衍生類別在呼叫基底類別的建構函數時,基底類別所設定的 VPTR 當然是指向基底類別的 VTABLE ,等到開始執行衍生類別的建構函數,這時衍生類別的 VPTR 就會指向衍生類別的 VTABLE 了。

沒有虛擬的建構元
建構元要根據物件的型別來建立 VTABLE 和設定 VPTR ,所以在執行建構元之前是沒有 VTABLE 和 VPTR 的,既然沒有 VTABLE 和VPTR 編譯程式要根據什麼去找到一個虛擬的建構元呢?

為什麼需要虛擬解構元?因為當我們透過指標或參考型別來使用物件時,到最後都必需用 delete 運算子將物件所佔有的記憶體歸還給系統, 如果解構元不是虛擬函數,那編譯程式將只呼叫原來指標被宣告所指向的類別,而不會呼叫指標真正所屬的類別的解構元,如此一來,將很容易產生記憶體遺失的狀況。

函數同名遮蓋 Name Hiding

我們先來看看以下的程式範例:

#include <iostream>

using namespace std;

class Base
{
public:
virtual void funcA() {
cout << "Base::void funcA ()" << endl;
}
virtual void funcA (int a) {
cout << "Base::void funcA (int a)" << endl;
}
};

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

int main()
{
Derived d;
d.funcA ();
d.funcA (3);
return 0;
}


以上程式可以在編譯是產生以下錯誤:

'Derived: funcA' : function does not take 1 arguments

程式中涉及了以下幾件事:

Derived d;
d.funcA();  // OK
d.funcA(3); // Not OK

在衍生類別中重新定義了函數 funcA() 之後, 和我們想像中只是會多產生一個不同版本的多載函數不同, 實際上該行為還會遮蓋掉基底類別中另一個同名但有一個引數的 funcA(int a). 換句話說, 在衍生類別重新定義基底類別中某一多載函數會把所有同名之多載函數全部遮蓋掉, 只剩該一被重新定義之函數. 如果衍生類別還想使用原來在基底類別中其他的同名函數則必需在衍生類別中一一重新定義. 以下就是修改後之程式碼:

#include <iostream>

using namespace std;

class Base
{
public:
virtual void funcA() {
cout << "Base::void funcA ()" << endl;
}
virtual void funcA (int a) {
cout << "Base::void funcA (int a)" << endl;
}
};

class Derived : public Base
{
public:
virtual void funcA () {
cout << "Derived::void funcA ()" << endl;
}
virtual void funcA (int a) {
cout << "Derived::void funcA (int a)" << endl;
}
};

int main()
{
Derived d;
d.funcA ();
d.funcA (3);
return 0;
}


注意: 以上範例中之函數若是不宣告為虛擬也是一樣的結果, 這和虛擬與否無關.
C++ 解析 主目錄
建構元與解構元
虛擬函數
函數

沒有留言:

張貼留言