1. 程式人生 > >C++中的多型、單繼承、多繼承、菱形繼承、菱形虛擬繼承

C++中的多型、單繼承、多繼承、菱形繼承、菱形虛擬繼承

C++中的繼承體系,有單繼承、多繼承、菱形繼承、菱形虛擬繼承,以及各型別的物件模型,我們今天做一個簡單的剖析

(1)什麼多型?

所謂多型,就是“多種形態”。在面向物件的方法中一般是這樣描述多型的:向不同的物件傳送同一個訊息,不同的物件在接收時會產生不同的行為(即方法)。
多型=動態多型+靜態多型
a.關於靜態多型:函式過載
b.關於動態多型:
構成動態多型的兩個必要條件:
(1)子類對父類的虛擬函式重寫
(2)函式通過父類得到指標或引用進行傳參 

實現多型的主要技術是虛繼承,當滿足以上條件時,父類物件呼叫被重寫過得虛擬函式,那就去父類中尋找;當子類物件呼叫時,就去子類中尋找重寫過後的虛擬函式,依次來實現多型,即不同的形態。如下圖:


2:多型的物件模型--單繼承&多繼承? 
(1)探索單繼承物件模型
當子類與父類構成多型時,子類繼承父類的虛表之後,子類虛表裡虛擬函式在記憶體裡的儲存情況;具體以下面例子作為分析參考:



由上圖可得,一個類裡面只要有虛擬函式,類的物件就會有虛表,例中第一個虛表中分別是
Base類中虛擬函式fun1,fun2的地址,第二個虛表是Drive類繼承父類的虛表,明顯這個虛表的內容
發生了變化,分別是Drive類中重寫父類的虛擬函式fun1,繼承父類的虛擬函式fun2,以及子類本身的虛擬函式
fun3,fun4,需要注意的是,每個虛表都以0結束
(2)探索多繼承物件模型
當一個子類繼承多個父類時構成多繼承,此時子類會繼承這些父類的虛表,子類虛表裡虛擬函式在記憶體裡的儲存情況
具體以下面例子(2個父類)作為分析參考:

由以上例子可得,子類Drive分別繼承了父類Base1,Base2,從而子類得到了兩個虛表,繼承Base1的
虛表中,存放了自己重寫之後的虛擬函式fun1,繼承Base1中的fun2,以及自己的fun3;
繼承Base2的虛表中,存放了自己重寫之後的虛擬函式fun1,以及繼承Base2中的fun2
注意的是,這裡子類自己的fun3只放在了第一個虛表中。
3:多型的物件模型--菱形繼承和菱形虛擬繼承?
(1)菱形繼承
   顧名思義,菱形繼承就是,幾個類的繼承關係呈菱形狀。
為此,我們舉例解釋:
例如,有A類,B類,C類,D類;其中,B類,C類繼承了A類,D類繼承了B類,C類。繼承關係如下:


程式碼如下:
#include <iostream>
using namespace std;
class A
{
public:
	virtual void f1()
	{
		cout<<"A::f1()"<<endl;
	}
	virtual void f2()
	{
		cout<<"A::f2()"<<endl;
	}
public:
	int _a;
};

class B:public A
{
public:
	virtual void f1()
	{
		cout<<"B::f1()"<<endl;
	}
	virtual void f3()
	{
		cout<<"B::f3()"<<endl;
	}
public:
	int _b;

};

class C:public A
{
public:
	virtual void f1()
	{
		cout<<"C::f1()"<<endl;
	}
	virtual void f4()
	{
		cout<<"C::f4()"<<endl;
	}
public:
	int _c;
};

class D:public B,public C
{
public:
	virtual void f1()
	{
		cout<<"D::f1()"<<endl;
	}
	virtual void f5()
	{
		cout<<"D::f5()"<<endl;
	}
public:
	int _d;
};

//列印虛擬函式表
typedef void(*V_FUNC)();

void PrintVtable(int* vtable)
{
	printf("vtable:%p\n",vtable);
	int** pvtable=(int**)vtable;
	for(size_t i=0; pvtable[i]!=0;i++)
	{
		printf("vtable[%u]:0x%p->",i,pvtable[i]);
		V_FUNC f=(V_FUNC)pvtable[i];
		f();
	}
	cout<<"------------------------------------\n";
}

void test()
{
	D d;
	d.B::_a=5;
	d.C::_a=6;
	d._b=1;
	d._c=2;
	d._d=3;
	PrintVtable(*(int**)&d);
	PrintVtable(*(int**)((char*)&d+sizeof(B)));
}

int main()
{
	test();
	return 0;
}
建立一個D類的物件d,下圖為檢視d物件中儲存的成員變數情況,以及虛表指向:

總結:在普通的菱形繼承中,處於最先的D型別的物件d,它繼承了B,C,並且B,C中分別儲存了
一份來自繼承A的變數;除此之外,B,C還存了虛表指標,通過它可以找到虛表中存的虛擬函式地址,
最後,d物件還存放了自己定義的變數和繼承B,C自己定義的變數。
(2)菱形虛擬繼承
    菱形虛擬繼承就是在普通菱形繼承的前提下加了虛繼承(本例是,B,C虛繼承A)
建立一個D類的物件d,下圖為檢視d物件中儲存的成員變數情況,以及虛表指向:

總結:菱形虛擬繼承與菱形繼承的區別在於,B,C繼承A的公共成員a,既不儲存在
B裡,也不儲存在C裡,而是儲存在一塊公共的部分,而會將B,C相對於這個變數的
偏移地址(這裡的偏移量地址叫做虛基表)存在B,C裡。所以說,物件d裡的B,C裡存放了虛表指標,虛基表指標,
自己的變數。