C++ — 多態

多態的概念

通俗來說,就是多種形態,具體點就是去完成某個行爲,當不同的對象去完成時會產生出不同的狀態。

多態定義的構成條件

  1. 基類中必須存在虛函數,派生類必須對基類中的虛函數進行重寫
  2. 必須通過基類的指針或者引用來調用虛函數
    虛函數:被virtual修飾的類成員函數稱爲虛函數。
    虛函數的重寫(覆蓋):派生類中有一個跟基類完全相同的虛函數(即派生類虛函數與基類虛函數的返回值類型、函數名字、參數列表完全相同),稱子類的虛函數重寫了基類的虛函數。
  3. 派生類虛函數virtual可加可不加
  4. 派生類虛函數的訪問權限可以與基類虛函數的訪問權限不同,基類中的虛函數的訪問權限必須是public
class A
{
public:
	virtual void Test()	//虛函數
	{
		cout << "test" << endl;
	}
	int _a;
};
class B : public A
{
public:
	virtual void Test()
	{
		cout << "test233" << endl;
	}
	int _b;
};
void test(A& a)
{
	a.Test();	//傳遞不同的對象,調用不同對象中的函數
}

虛函數重寫的兩個例外

  1. 協變(基類與派生類虛函數返回值類型不同)
    派生類重寫基類虛函數時,與基類虛函數返回值類型不同。即基類虛函數返回基類對象的指針或者引用,派生類虛函數返回派生類對象的指針或者引用時,稱爲協變。
  2. 析構函數的重寫(基類與派生類析構函數的名字不同)
    如果基類的析構函數爲虛函數,此時派生類析構函數只要定義,無論是否加virtual關鍵字,都與基類的析構函數構成重寫,雖然基類與派生類析構函數名字不同。雖然函數名不相同,看起來違背了重寫的規則,其實不然,這裏可以理解爲編譯器對析構函數的名稱做了特殊處理,編譯後析構函數的名稱統一處理成destructor。所以,基類的析構函數建議都寫成虛函數。

抽象類

在虛函數的後面寫上 =0 ,則這個函數爲純虛函數。包含純虛函數的類叫做抽象類(也叫接口類),抽象類不能實例化出對象。派生類繼承後也不能實例化出對象,**只有重寫純虛函數,派生類才能實例化出對象。**純虛函數規範了派生類必須重寫,另外純虛函數更體現出了接口繼承。

class B
{
public:
	virtual void Test() = 0	//純虛函數
	{
		cout << "test" << endl;
	}
	int _b;
};
class C : public B
{
public:
	virtual void Test()
	{
		cout << "test233" << endl;
	}
	int _c;
};

B* b = new C;
b->Test();

接口繼承和實現繼承

普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用基類函數,繼承的是函數的實現。
虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是爲了重寫,達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數。

多態的原理

虛函數表

一個含有虛函數的類中都至少都有一個虛函數表指針,因爲虛函數的地址要被放到 虛函數表中,虛函數表也簡稱虛表。

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};sizeof(Base),我們會發現除了_b成員,還多一個__vfptr放在對象的前面(注意有些平臺可能會
放到對象的最後面,這個跟平臺有關),對象中的這個指針我們叫做虛函數表指針。

虛基表的構建方式

基類虛表的構建方式
將虛函數的地址按照其在類中聲明的先後次序放置在虛表中
派生類虛表的構建方式

  1. 派生類先將基類虛表中內容拷貝一份放在派生類的虛表中
  2. 如果派生類重寫了基類的某個虛函數,用派生類自己的虛函數地址替換虛表的相同偏移量位置的基類虛函數
  3. 對於派生類新增加虛函數,按照聲明的次序放在虛表的末尾

派生類與基類使用的不是同一張虛表,即使派生類沒有對基類的任何虛函數進行重寫
在這裏插入圖片描述
在這裏插入圖片描述
因此,在虛表中已經提前將地址寫好,指向父類時,調用父類虛函數表中的父類虛函數,指向子類時,調用子類虛函數表中的子類虛函數。
注:

  1. 同類型對象共用一份虛表
  2. 虛表中指針的順序爲聲明的順序
  3. 子類具有新的虛函數,放在繼承的虛表後面

打印虛表

class B
{
public:
	virtual void Test() = 0	//純虛函數
	{
		cout << "test" << endl;
	}
	int _b;
};
class C1 : public B
{
public:
	virtual void Test()
	{
		cout << "test233" << endl;
	}
	int _c1;
};
class C2 : public B
{
public:
	virtual void Test()
	{
		cout << "test466" << endl;
	}
	int _c2;
};

typedef void(*VFPTR) ();
void PrintfTable(VFPTR Table[])
{
	// 依次取虛表中的虛函數指針打印並調用。調用就可以看出存的是哪個函數
	cout << " 虛表地址>" << Table << endl;
	for (int i = 0; Table[i] != nullptr; ++i)
	{
		printf(" 第%d個虛函數地址 :0X%x,->", i, Table[i]);
		VFPTR f = Table[i];
		f();
	}
	cout << endl;
}
int main()
{
	//創建兩個派生類
	C1 c1;
	C2 c2;
	// 思路:取出c1、c2、對象的頭4bytes,就是虛表的指針,虛函數表本質是一個存虛函數指針的指針數組,這個數組最後面放了一個nullptr
	// 1.先取c的地址,強轉成一個int*的指針
	// 2.再解引用取值,就取到了b對象頭4bytes的值,這個值就是指向虛表的指針
	// 3.再強轉成VFPTR*,因爲虛表就是一個存VFPTR類型(虛函數指針類型)的數組。
	// 4.虛表指針傳遞給PrintfTable進行打印虛表
	// 5.需要說明的是這個打印虛表的代碼經常會崩潰,因爲編譯器有時對虛表的處理不乾淨,虛表最後面沒有放nullptr,導致越界,這是編譯器的問題。我們只需要點目錄欄的-生成-清理解決方案,再編譯就好了。
	VFPTR* vTableb = (VFPTR*)(*(int*)&c1);
	PrintfTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&c2);
	PrintfTable(vTabled);
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章