【C++初階學習】之 繼承與多態(多態)

一、什麼是多態

    多態通俗的來講,就是多種狀態,就是一個事物在不同條件下表現出的不同狀態。比如出門這件事,受天氣,氣溫不同條件的影響,會表現出不同的狀態,下雨天出門就要打傘,夏天出門就要穿得涼快一點,冬天出門就要穿厚一點。同樣是出門,因外界條件不同,會有多種具體的實現方式。

  • 多態的定義:多態是在不同繼承關係的類對象,去調用同一函數,產生了不同的行爲。

二、多態的定義及實現

在繼承中要構成多態還有兩個條件:

1. 調用函數的對象必須是指針或者引用

2. 被調用的函數必須是虛函數,且完成了虛函數的重寫

PS:指針的兩種類型:靜態類型-->聲明變量的類型(編譯期間已經確定)

                                     動態類型-->實際指向空間的類型(代碼執行期間,若構成重寫則會變成動態類型)

  • 虛函數就是加上virtual關鍵字的函數,重寫虛函數就是在派生類中,有跟基類中名字,參數,類型,返回值完全相同的虛函數。
  • 有一個特殊情況也可以構成重寫:協變。
//重寫的虛函數的返回值可以不同,但是必須分別是基類指針和派生類指針或者基類
//引用和派生類引用。
class A{};
class B : public A {};
class Person {
public:
 virtual A* f() {return new A;}
};
class Student : public Person {
public:
 virtual B* f() {return new B;}
};

#析構函數的重寫問題

只要基類中的析構函數爲虛函數,就構成了析構函數的重寫,無需與派生類同名。

因爲在編譯過程中,析構函數名稱統一被替換爲destructor,所以基類中的析構函數最好給成虛函數。

三、多態的原理

①、虛表的介紹及構建過程

論證方法:1.構造繼承方式的場景
                  2.驗證對象模型
                  3.驗證虛表的內容(通過所掌握的內容推斷)
 

從上圖中看出,顯然虛函數比普通成員函數多了一個叫_vfptr的東西,從名字上看,應該是存放了一個什麼指針,下面來看看這是什麼指針。

class Base
{//測試代碼
public:
 virtual void Func1(){cout << "Base::Func1()" << endl;}//基類虛函數F1
 virtual void Func2(){cout << "Base::Func2()" << endl;}//基類虛函數F2
 void Func3(){cout << "Base::Func3()" << endl;}//基類普通函數F3
private:
 int _b = 1;
};
class Derive : public Base
{
public:
 virtual void Func1(){cout << "Derive::Func1()" << endl;}//派生類重寫F1
private:
 int _d = 2;
};
int main()
{
 Base b;
 Derive d;
 return 0;
}

這個存放虛函數的表格,被叫做虛表。

總結一下:

1. 派生類對象d中也有一個虛表指針,d對象由兩部分構成,一部分是父類繼承下來的成員,虛表指針也就 是存在部分的另一部分是自己的成員。

2. 基類b對象和派生類d對象虛表是不一樣的,這裏我們發現Func1完成了重寫,所以d的虛表中存的是重寫的Derive::Func1,所以虛函數的重寫也叫作覆蓋,覆蓋就是指虛表中虛函數的覆蓋。重寫是語法的叫法,覆蓋是原理層的叫法。

3. 另外Func2繼承下來後是虛函數,所以放進了虛表,Func3也繼承下來了,但是不是虛函數,所以不會放進虛表。

4. 虛函數表本質是一個存虛函數指針的指針數組,這個數組最後面放了一個nullptr。

 

②、多態函數的調用過程

要構成多態的調用形式:首先多態的兩個條件都要滿足
引用的哪個類的對象,將來調用具體類的虛函數
1.取虛表的地址&vfptr(取對象地址,從對象前四個字節中取虛表的地址)
2.傳遞this指針
3.從虛表中取將要調用虛函數的地址(&vfptr+虛函數在虛表中的偏移量)
4.調用該虛函數

③、單繼承、多繼承中的虛表構建規律

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};


typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	// 依次取虛表中的虛函數指針打印並調用。調用就可以看出存的是哪個函數
	cout << " 虛表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d個虛函數地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;
	// 思路:取出b、d對象的頭4bytes,就是虛表的指針,前面我們說了虛函數表本質是一個存虛函數指針的
	//指針數組,這個數組最後面放了一個nullptr
	// 1.先取b的地址,強轉成一個int*的指針
	// 2.再解引用取值,就取到了b對象頭4bytes的值,這個值就是指向虛表的指針
	// 3.再強轉成VFPTR*,因爲虛表就是一個存VFPTR類型(虛函數指針類型)的數組。
	// 4.虛表指針傳遞給PrintVTable進行打印虛表
	// 5.需要說明的是這個打印虛表的代碼經常會崩潰,因爲編譯器有時對虛表的處理不乾淨,虛表最後面
    //沒有放nullptr,導致越界,這是編譯器的問題。清理解決方案,再編譯就好了。
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);
	return 0;
}

單繼承中派生類的虛表構建:a.先將基類中的虛表內容拷貝一份到派生類虛表中。

                                               b.如果派生類重寫了基 類中某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數。

                                               c.派生類自己新增加的虛函數按其在 派生類中的聲明次序增加到派生類虛表的最後。

class Base1 {
public:
 virtual void func1() {cout << "Base1::func1" << endl;}
 virtual void func2() {cout << "Base1::func2" << endl;}
private:
 int b1;
};
class Base2 {
public:
 virtual void func1() {cout << "Base2::func1" << endl;}
 virtual void func2() {cout << "Base2::func2" << endl;}
private:
 int b2;
};
class Derive : public Base1, public Base2 {
public:
 virtual void func1() {cout << "Derive::func1" << endl;}
 virtual void func3() {cout << "Derive::func3" << endl;}
private:
 int d1;
};

多繼承中派生類的虛表構建:

與單繼承的不同點:多繼承派生類的未重寫的虛函數放在第一個繼承基類部分的虛函數表中。


以上,繼承與多態的知識點已經大體介紹完畢。還有一些細小的知識點如override,final關鍵字的用法,抽象類生成對象的函數,綜合兩者的菱形繼承和菱形虛擬繼承等,改日重開一貼再議。

如有錯誤、不準確之處,歡迎提議,感激不盡。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章