1. 程式人生 > >C++虛擬函式表(含測試程式碼)

C++虛擬函式表(含測試程式碼)

自己搞不懂C++虛擬函式之間的呼叫關係,特地花費一個下午加一個晚上查資料學習,現在把學到的發上來,供大家學習批評;

在此之前感謝這些大佬的部落格等,為我解惑甚多:

1、虛表與虛表指標

C++中的虛擬函式的實現一般是通過虛擬函式表(V-Table)來實現的。虛擬函式表實際上是一塊連續的記憶體,每個記憶體單元中記錄一個虛擬函式地址。每個類的虛擬函式表被該類的所有物件所共享。32位系統下,如果一個類存在N個虛擬函式,該類的虛表佔N*4個位元組。只有虛類才有虛擬函式表。

虛類的物件,除自身的成員變數外還存在一個虛表指標(vfptr),該指標指向該類的虛表的起始地址。因此,虛類物件的記憶體大小 = 成員變數大小(考慮對齊) + vfptr大小。

虛標指標在記憶體空間中位於成員變數之前(有的說位於之後,至少我用vs測試是在之前)。

見測試程式碼main1.cpp

2、單繼承下的虛表

以上考慮的是基類的虛表,現在來看看派生子類的虛表構造過程:

見測試程式碼main2.cpp

3、多繼承下的虛表

多繼承時,派生類的虛表構造與單繼承基本無異,只是此時派生子類自己的虛擬函式要加在第一個繼承的基類的虛表末尾。

見測試程式碼main3.cpp

4、型別相容性原則

型別相容規則是指在需要基類物件的任何地方,都可以使用公有派生類的物件來替代。型別相容規則是多型性的重要基礎之一。

型別相容規則中所指的替代包括以下情況:

    子類物件可以當作父類物件使用

    子類物件可以直接賦值給父類物件

    子類物件可以直接初始化父類物件

    父類指標可以直接指向子類物件

              父類引用可以直接引用子類物件

在替代之後,派生類物件就可以作為基類的物件使用,但是隻能使用從基類繼承的成員。

當父類指標(引用)指向子類物件時,子類物件退化成父類物件,只能訪問父類中定義的成員。

但是函式又有virtual修飾的話,就會展現多型行為,會根據實際指標指向的物件判斷函式的呼叫。

5、虛擬函式表解釋多型

型別轉換原則很難記憶,但使用虛擬函式表來解釋便很容易理解:

public:
	virtual void func1() { cout << "Base1::func1\n"; }
	virtual void func2() { cout << "Base1::func2\n"; }
	int b1;
}base1;
class Base2
{
public:
	virtual void func1() { cout << "Base2::func1\n"; }
	virtual void func2() { cout << "Base2::func2\n"; }
	int b2;
}base2;
class Derive : public Base1, public Base2
{
public:
	virtual void func1() { cout << "Derive::func1\n"; }
	virtual void func3() { cout << "Derive::func3\n"; }
	int d;
}derive;

1》派生類物件就可以作為基類的物件使用,但是隻能使用從基類繼承的成員。

無論是Base1 b(derive)還是Base1 b = new Base1(derive),物件b是作為Base1基類的物件,因此物件b只能呼叫Base1中已經宣告的成員;

2》派生類物件賦值基類的物件時,具體呼叫的函式是基類的函式;而基類指標或引用派生類物件時具體呼叫的函式要看派生類有沒有覆蓋這個函式。

若派生類物件賦值基類的物件,即Base1 b(derive),此時只不過是使用derive的成員給b的成員賦值,而derive的虛表並沒用賦值給b,物件b依然沿用基類的虛表,實際呼叫的函式當然是基類Base1的函式;

若基類指標或引用派生類物件,即Base1 *pb1 = new Derive()獲Base2& pb2 = derive,此時b並沒有建立新的物件,而是沿用derive的記憶體空間。看下圖:

此時,若pb2呼叫Base2中宣告的函式時,會通過虛表指標vfptr2查詢對應函式,因為vfptr2是原派生類物件derive的虛表指標,所以呼叫的是原本型別(即派生類)的函式成員。

見測試程式碼main4.cpp