日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

C++?超詳細分析多態的原理與實現_C 語言

作者:ymz123_ ? 更新時間: 2022-05-27 編程語言

多態的定義及實現

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

比如買票這個行為,當普通人買票時,是全價買票;學生買票時,是半價買票;軍人買票時是優先買票。

多態的構成條件

多態是在不同繼承關系的類對象,去調用同一函數,產生了不同的行為。比如Student繼承了Person。Person對象買票全價,Student對象買票半價。

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

  • 必須通過基類的指針或者引用調用虛函數
  • 被調用的函數必須是虛函數,且派生類必須對基類的虛函數進行重寫。

虛函數重寫

虛函數:即被virtual修飾的類成員函數。

class Person{
public:
	virtual void BuyTicket() {
		cout << "買票-全價" << endl;
	}
};

虛函數的重寫(覆蓋):派生類中有一個跟基類完全相同的虛函數(即派生類虛函數與基類虛函數的返回值類型、函數名字、參數列表完全相同),稱子類的虛函數重寫了基類的虛函數。

注:在重寫基類虛函數時,派生類的虛函數在不加virtual關鍵字時,雖然也可以構成重寫(因為繼承后基類的虛函數被繼承下來了,在派生類依舊保持虛函數屬性),但該種寫法不是很規范。

class Person{
public:
	virtual void BuyTicket(){ cout << "買票-全價" << endl; }
};

class Student : public Person{
public:
	virtual void BuyTicket(){ cout << "買票-半價" << endl; }
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	
	Func(ps);
	Func(st);

	return 0;
} 

虛函數重寫的兩個例外:

1.協變(基類與派生類虛函數返回值類型不同) 派生類重寫基類虛函數時,與基類虛函數返回值類型不同。即基類虛函數返回基類對象的指針或者引用,派生類虛函數返回派生類對象的指針或引用時,稱為協變。

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; }
}; 

析構函數的重寫 (基類與派生類析構函數的名字不同) 如果基類的析構函數為虛函數,此時派生類析構函數只要定義,無論是否加virtual關鍵字,都與基類的析構函數構成重寫,雖然基類與派生類析構函數名字不同。這里可以理解為編譯器對析構函數的名稱做了特殊處理,編譯后析構函數的名稱同一處理成destructor

class Person{
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};

class Student : public Person{
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};

int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;

	return 0;
}

只有派生類Student的析構函數重寫了Person的析構函數,這里的delete對象調用析構函數,才能構成多態,才能保證p1和p2指向的對象正確調用析構函數。

C++11的override和final

從上面可以看出,C++對函數重寫的要求比較嚴格,但有些情況下由于疏忽,可能會導致函數名字母次序寫反而無法構成重寫,而這種錯誤在編譯期間是不會報錯的,但程序運行時不會得到預期結果。因此C++11提供了override和final兩個關鍵字,可以幫助用戶檢測是否重寫。

final:修飾虛函數,表示該虛函數不能再被重寫。即final一個類,這個類不能被繼承。

class Car
{
public:
	virtual void Drive() final {}
};

class Benz : public Car
{
public:
	virtual void Drive() { cout << "Benz-舒適" << endl; }
};

override:檢查派生類虛函數是否重寫了基類某個虛函數,如果沒有重寫,編譯器將會報錯。

class Car
{
public:
	virtual void Drive() {}
};

class Benz : public Car
{
public:
	virtual void Drive() override { cout << "Benz-舒適" << endl;}
};

重載、覆蓋(重寫)、因此(重定義)的對比

抽象類

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

class Car 
{
public:
	virtual void Drive() = 0;
};

class Benz : public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒適" << endl;
	}
};

class BMW : public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};

void Test()
{
	Car* pBenz = new Benz;
	pBenz->Drive();

	Car* pBMW = new BMW;
	pBMW->Drive();
}

接口繼承和實現繼承

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

多態的原理

虛函數表

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

public:
	int _b = 1;
};

sizeof(Base)是多少?

處理_b成員,還多一個_vfptr放在對象的前面(有些平臺可能會放到對象的最后面,這與平臺有關),對象中的這個指針叫做虛函數表指針(v代表virtual,f代表function)。一個含有虛函數的類中都至少有一個虛函數表指針,因為虛函數的地址要被放到虛函數表中,虛函數表也稱虛表。

針對上面的代碼做出以下改造:

1.增加一個派生類Derive去繼承Base

2.Derive中重寫Func1

3.Base再增加一個虛函數Func2和一個普通函數Func3

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

	virtual void Func2()
	{
		 cout << "Base::Func2()" << endl;
	}
		
	void Fun3()
	{
		cout << "Base::Func3()" << endl;
	}

private:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::unc1()" << endl;
	}
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。

5.總結一下派生類的虛表生成:a.先將基類中的虛表內容拷貝一份到派生類虛表中。b.如果派生類重寫了基類中某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數。c.派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后。

6.虛表存的是虛函數指針,不是虛函數,虛函數和普通函數一樣,都存在代碼段,只是它的指針又存到了虛表中。另外對象中存的不是虛表,存的是虛表指針。

動態綁定與靜態綁定

1.靜態綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也成為靜態多態。比如:函數重載。

2.動態綁定又稱為后期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型確定程序的具體行為,調用具體的函數,也成為動態多態。

單繼承和多繼承關系的虛函數表

單繼承中的虛函數表

使用代碼打印出虛表中的函數:

取出b、d對象的頭4bytes,就是虛表的指針,虛函數表本質是一個存虛函數指針的指針數組,這個數組最后面放了一個nullptr

1.先取b的地址,強轉成一個int的指針

2.再解引用取值,就取到了b對象頭4bytes的值,這個值就是指向虛表的指針。

3.再強轉成VFPTR,因為虛表就是一個存VFPTR類型(虛函數指針類型)的數組。

4.虛表指針傳遞給PrintVFT進行打印虛表。

5.注意:這個打印虛表的代碼經常會崩潰,因為編譯器有時對虛表的處理不干凈,虛表最后沒有放nullptr,導致越界,這時編譯器的問題。需要點目錄欄的-生成-清理解決方案,再編譯。

typedef void(*VFPTR)();

void PrintVFT(VFPTR vft[])
{
	printf("%p\n", vft);
	for (size_t i = 0; vft[i] != nullptr; ++i)
	{
		printf("vft[%d]:%p->", i, vft[i]);
		vft[i]();
	}
	printf("\n");
}

int main()
{
	Base b;
	Derive d;

	PrintVFT((VFPTR*)(*(int*)&b));
	PrintVFT((VFPTR*)(*(int*)&d));

	return 0;
}

int main()
{
	Base bb;

	int a = 0;
	int* p1 = new int;
	const char* p2 = "hello world";
	auto pf = PrintVFT;
	static int b = 1;

	printf("棧幀變量:%p\n", &a);
	printf("堆變量:%p\n", p1);
	printf("常量區變量:%p\n", p2);
	printf("函數地址變量:%p\n", pf);
	printf("靜態區變量:%p\n", &b);
	printf("虛函數表地址:%p\n", *(int*)&bb);

	return 0;
}

從以上代碼也可以看出,虛函數表地址與常量區變量地址非常相近,它也存在常量區。

多繼承中的虛函數表

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 << "Base2::func1" << endl; }
	virtual void func3() { cout << "Base2::func3" << endl; }
private:
	int d1;
};

int main()
{
	Derive d;
	PrintVFT((VFPTR*)(*(int*)&d));
	PrintVFT((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));

	Base1* p1 = &d;
	p1->func1();

	Base2* p2 = &d;
	p2->func1();

	return 0;
}

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

常見問題

1.inline函數可以是虛函數嗎

inline函數沒有地址,虛函數需要放到虛表中,這樣是矛盾的。但VS編譯器此時就會忽略inline屬性,這個函數就不再是inline。

2.靜態成員可以是虛函數嗎? 不能

靜態成員函數沒有this指針,使用類型::成員函數的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。

3.構造函數可以是虛函數嗎? 不能

(1)因為虛函數表指針是在構造函數初始化列表階段初始化的。如果構造函數是虛函數,那么調用構造函數時對象中虛表指針都沒有初始化。

(2)沒有意義,因為子類中要調用父類構造函數初始化。寫成虛函數目的是多態,構造函數不需要多態的方式。

4.析構函數可以是虛函數嗎?

是,最好把基類的析構函數定義成虛函數。

5.對象訪問普通函數快還是虛函數更快?

如果是普通對象,是一樣快的。如果是指針對象或者是引用對象,則調用的普通函數快,因為構成多態,運行時調用虛函數需要到虛函數表中去查找。

6.虛函數表是在什么階段生成的?存在哪里?

虛函數表是在編譯階段就生成的,一般情況下存在代碼段(常量區)。

7.抽象類的作用?

抽象類強制重寫了虛函數,另外抽象類體現出了接口繼承關系。

原文鏈接:https://blog.csdn.net/mmz123_/article/details/122670185

欄目分類
最近更新