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

學(xué)無(wú)先后,達(dá)者為師

網(wǎng)站首頁(yè) 編程語(yǔ)言 正文

C++中的多態(tài)問(wèn)題—理解虛函數(shù)表及多態(tài)實(shí)現(xiàn)原理_C 語(yǔ)言

作者:安河橋畔 ? 更新時(shí)間: 2023-04-06 編程語(yǔ)言

注:編譯環(huán)境為VS 2022,指針大小為4字節(jié)

一、多態(tài)的概念

概念

多態(tài),指完成某個(gè)行為,不同的對(duì)象去完成時(shí)會(huì)產(chǎn)生出不同的狀態(tài)。如:定一個(gè)一Animal類(lèi),類(lèi)中包含動(dòng)物的叫聲這種方法,分別定義Dog和Cat類(lèi)繼承自動(dòng)物類(lèi),那么Dog和Cat類(lèi)中也會(huì)包含叫聲這種方法,但是他們具體實(shí)現(xiàn)是不同的,因?yàn)槊糠N動(dòng)物的聲音都不相同,這便是一種多態(tài)。

多態(tài)的分類(lèi)

  • 靜態(tài)多態(tài),也稱(chēng)為靜態(tài)綁定或者早綁定,是指函數(shù)在編譯期間就已經(jīng)確定了函數(shù)的行為。函數(shù)重載、函數(shù)模板等都屬于靜態(tài)多態(tài)。
  • 動(dòng)態(tài)多態(tài),即動(dòng)態(tài)綁定或者晚綁定,指程序在運(yùn)行時(shí)才可以確定函數(shù)的行為。本文主要分析的是動(dòng)態(tài)多態(tài)。

構(gòu)成條件

  • 在繼承體系下,父類(lèi)中包含虛函數(shù)
  • 子類(lèi)中對(duì)父類(lèi)的虛函數(shù)進(jìn)行重寫(xiě)
  • 通過(guò)父類(lèi)的指針或者引用調(diào)用虛函數(shù)

多態(tài)的體現(xiàn):不同的類(lèi)對(duì)象調(diào)用同一函數(shù),會(huì)產(chǎn)生不同的行為。

二、虛函數(shù)的重寫(xiě)

重寫(xiě)的定義

虛函數(shù):virtual關(guān)鍵字修飾的函數(shù)

子類(lèi)中有一個(gè)跟父類(lèi)完全相同的虛函數(shù),即返回值類(lèi)型、函數(shù)名、形參列表都完全相同,則可以說(shuō)子類(lèi)重寫(xiě)了父類(lèi)的虛函數(shù)。

Student類(lèi)中重寫(xiě)了BuyTicket方法:

注意:只要父類(lèi)中函數(shù)用virtual修飾即可,子類(lèi)可以不加,且虛函數(shù)的重寫(xiě)與權(quán)限無(wú)關(guān)。

重寫(xiě)的特殊情況

協(xié)變——返回值類(lèi)型不同

父類(lèi)的虛函數(shù)返回父類(lèi)對(duì)象的指針或者引用,子類(lèi)虛函數(shù)返回子類(lèi)對(duì)象的指針或者引用。

析構(gòu)函數(shù)重寫(xiě)——父類(lèi)與子類(lèi)析構(gòu)函數(shù)名字不同

如果父類(lèi)的析構(gòu)函數(shù)為虛函數(shù),子類(lèi)的析構(gòu)函數(shù)只要定義了,都能與父類(lèi)的析構(gòu)函數(shù)構(gòu)成重寫(xiě)。可以理解為編譯器對(duì)析構(gòu)函數(shù)的名字做了特殊處理,編譯后析構(gòu)函數(shù)的名字統(tǒng)一處理成destructor。

override和final關(guān)鍵字

這兩個(gè)關(guān)鍵字的主要作用都是幫助用戶(hù)檢測(cè)是否構(gòu)成重寫(xiě)。

  • final:修飾虛函數(shù),表示虛函數(shù)不可被重寫(xiě);另外final也可以修飾類(lèi),表示該類(lèi)不能被繼承

  • override:修飾虛函數(shù),檢查子類(lèi)虛函數(shù)是否重寫(xiě)了父類(lèi)的虛函數(shù),如果沒(méi)有構(gòu)成重寫(xiě)則會(huì)報(bào)錯(cuò)

區(qū)分重寫(xiě)、重載、重定義

抽象類(lèi)的概念

在虛函數(shù)的后面寫(xiě)上=0,則這個(gè)函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類(lèi)叫做抽象類(lèi)(也稱(chēng)接口類(lèi)),抽象類(lèi)不能實(shí)例化對(duì)象。抽象類(lèi)被集成以后如果沒(méi)有對(duì)虛函數(shù)進(jìn)行重寫(xiě),則繼承的類(lèi)也是抽象類(lèi)。

一般情況下,抽象類(lèi)必須被繼承,且必須對(duì)虛函數(shù)進(jìn)行重寫(xiě),否則定義為抽象類(lèi)則沒(méi)有實(shí)際意義。

Shape類(lèi):

class Shape
{
public:
	// 純虛函數(shù)
	virtual double GetArea() = 0;
	virtual double GetCircumference() = 0;
};

三、多態(tài)的實(shí)現(xiàn)原理

父類(lèi)對(duì)象模型

給出一個(gè)Base類(lèi),一個(gè)Derived類(lèi)繼承Base類(lèi)

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Fun1c()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	virtual void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
public:
	int  _a;
};
class Derived:public Base
{
public:
	int _b;
};

父類(lèi)對(duì)象模型:

總結(jié):

  • 類(lèi)中定義了虛函數(shù)以后,定義對(duì)象時(shí),編譯器會(huì)為對(duì)象創(chuàng)建一張?zhí)摫?,并將一個(gè)指向這張?zhí)摫淼闹羔槺4嬖趯?duì)象的前四個(gè)字節(jié),無(wú)論定義幾個(gè)虛函數(shù),對(duì)象都只比多四個(gè)字節(jié)大小。這個(gè)指針?lè)Q為函數(shù)虛表指針。
  • 虛表地址是在構(gòu)造對(duì)象時(shí)進(jìn)行填充的,構(gòu)造函數(shù)如果顯式實(shí)現(xiàn),編譯器會(huì)對(duì)用戶(hù)實(shí)現(xiàn)的函數(shù)進(jìn)行修改,增加給對(duì)象前四個(gè)字節(jié)存放虛表地址的語(yǔ)句。
  • 虛表本質(zhì)就是一個(gè)函數(shù)指針數(shù)組,按照聲明順序依次存放虛函數(shù)的地址

補(bǔ)充:生成默認(rèn)構(gòu)造方法的場(chǎng)景

在學(xué)習(xí)類(lèi)與對(duì)象時(shí)我們知道構(gòu)造函數(shù)是是類(lèi)的默認(rèn)成員函數(shù),如果用戶(hù)沒(méi)有顯式定義,編譯器會(huì)默認(rèn)生成,但是實(shí)際上并不是在所有情況下編譯器都會(huì)生成默認(rèn)的構(gòu)造函數(shù),編譯器只會(huì)在需要的時(shí)候生成構(gòu)造函數(shù)。

四種生成默認(rèn)構(gòu)造方法的場(chǎng)景

B類(lèi)中包含有A類(lèi)的對(duì)象,B類(lèi)沒(méi)有顯式定義構(gòu)造函數(shù),A類(lèi)定義了無(wú)參或者全缺省的構(gòu)造方法,則編譯器會(huì)給B類(lèi)生成默認(rèn)的構(gòu)造方法。

分析:因?yàn)锳類(lèi)有無(wú)參或者全缺省的構(gòu)造方法,需要在B類(lèi)中調(diào)用A類(lèi)的構(gòu)造方法對(duì)A類(lèi)成員進(jìn)行初始化,所以需要生成B類(lèi)的構(gòu)造方法,在其初始化列表中調(diào)用A類(lèi)構(gòu)造方法。

如果A類(lèi)沒(méi)有顯式定義構(gòu)造函數(shù),則不會(huì)生成B類(lèi)構(gòu)造方法,默認(rèn)賦隨機(jī)值;如果A類(lèi)定義的構(gòu)造方法不是無(wú)參或者全缺省的,則需要在初始化列表中對(duì)A類(lèi)對(duì)象初始化:?

繼承中,B繼承A,A中定義了無(wú)參或者全缺省的構(gòu)造方法,B未顯式定義,則編譯器會(huì)給B類(lèi)生成默認(rèn)的構(gòu)造方法。將B中繼承自A的部分初始化。

虛擬繼承中,B類(lèi)虛擬繼承子A類(lèi),B類(lèi)未顯式定義構(gòu)造方法,編譯器會(huì)給B類(lèi)生成默認(rèn)的構(gòu)造方法,目的是:給B類(lèi)對(duì)象的前4個(gè)字節(jié)填充虛基表地址

類(lèi)中包含虛函數(shù),未顯示定義構(gòu)造方法,則編譯器會(huì)自動(dòng)生成構(gòu)造方法,為對(duì)象的前4個(gè)字節(jié)填充虛表地址

子類(lèi)對(duì)象模型

子類(lèi)虛表構(gòu)建規(guī)則

1.將父類(lèi)虛表內(nèi)容拷貝一份放到子類(lèi)虛表中,注意父類(lèi)和子類(lèi)用的不是同一張?zhí)摫?,仍以上面的Base和Derived類(lèi)為例

可以看出,兩個(gè)虛表指針的地址不同,但虛表中保存的虛函數(shù)的地址都相同。

2.如果子類(lèi)中將父類(lèi)的虛函數(shù)進(jìn)行了重寫(xiě),則用子類(lèi)的虛函數(shù)地址替換虛函數(shù)表中相同偏移量的虛函數(shù)的地址。

3.子類(lèi)中增加的虛函數(shù)按照其在類(lèi)當(dāng)中的聲明次序放在虛表的最后

子類(lèi)中增加了兩個(gè)虛函數(shù):

但是由于VS監(jiān)視窗口中無(wú)法顯式新增加的子類(lèi),而內(nèi)存窗口只能顯式虛函數(shù)的地址,無(wú)法確認(rèn)是哪個(gè)函數(shù),所以這里通過(guò)打印的方式進(jìn)行驗(yàn)證。

通過(guò)上圖中程序的方式打印出了子類(lèi)對(duì)象中虛函數(shù)的分布情況,在這里VFP是一個(gè)函數(shù)指針類(lèi)型,前面加typedef表示為函數(shù)指針類(lèi)型,如果不加,則是函數(shù)指針變量。

所以是用VFP*接收指向第一個(gè)虛函數(shù)指針的指針,p與*p的類(lèi)型:

所以最終的結(jié)論是:子類(lèi)新增的虛函數(shù)按照其在類(lèi)中的聲明次序放在虛函數(shù)表的最后。

子類(lèi)對(duì)象的構(gòu)造過(guò)程

構(gòu)造子類(lèi)對(duì)象時(shí),在初始化列表中先調(diào)用父類(lèi)的構(gòu)造函數(shù),此時(shí)對(duì)象的前4個(gè)字節(jié)保存的虛表指針指向父類(lèi)的虛表,之后構(gòu)造子類(lèi)自己的虛表,虛表再指針指向子類(lèi)的虛表。

總結(jié)

  • 虛表的本質(zhì)是函數(shù)指針數(shù)組,在編譯時(shí)生成
  • 虛函數(shù)的重寫(xiě)也叫覆蓋,指的是虛表中虛函數(shù)的覆蓋,重寫(xiě)是語(yǔ)法層的叫法,覆蓋是原理層的叫法
  • 對(duì)象中保存的是虛表指針,虛表中保存的是虛函數(shù)指針,虛函數(shù)和普通函數(shù)一樣保存在代碼段,在VS中虛表也保存在代碼區(qū)
  • 同一個(gè)類(lèi)的對(duì)象共用同一張?zhí)摫?,父?lèi)和子類(lèi)各自擁有各自的虛表。

多態(tài)的調(diào)用原理

父類(lèi)對(duì)象,函數(shù)調(diào)用時(shí)的匯編代碼:

普通函數(shù)調(diào)用時(shí)直接傳遞函數(shù)的地址,這個(gè)地址在編譯期間就確定了,虛函數(shù)則要經(jīng)過(guò)虛表指針尋址等步驟。從上面的匯編代碼也可以看出動(dòng)態(tài)多態(tài)的晚綁定的特點(diǎn),在編譯期間普通函數(shù)的調(diào)用已經(jīng)確定了要調(diào)用的具體函數(shù),虛函數(shù)則無(wú)法確定,只有等程序運(yùn)行起來(lái),形參b是具體哪個(gè)對(duì)象確定了以后,才能確定要調(diào)用的函數(shù)的地址。

上面是傳遞父類(lèi)對(duì)象時(shí)的調(diào)用情況,子類(lèi)對(duì)象調(diào)用時(shí)的匯編代碼與父類(lèi)對(duì)象相同,區(qū)別就是子類(lèi)對(duì)象有自己的虛表,最終調(diào)用的是子類(lèi)需表中的函數(shù)。

總結(jié)多態(tài)的原理:

創(chuàng)建對(duì)象時(shí),編譯器會(huì)給包含虛函數(shù)的類(lèi)對(duì)象創(chuàng)建一張?zhí)摫恚⑻摫淼刂诽畛湓趯?duì)象的前4個(gè)字節(jié),子類(lèi)對(duì)象會(huì)拷貝父類(lèi)對(duì)象的虛表,然后再對(duì)自己重寫(xiě)的虛函數(shù)進(jìn)行替換,并在虛表中添加子類(lèi)新增的虛函數(shù);函數(shù)調(diào)用時(shí),編譯器會(huì)先從對(duì)象的前4個(gè)字節(jié)獲取該對(duì)象虛表的地址,然后在虛表中獲取虛函數(shù)地址進(jìn)行函數(shù)調(diào)用;由于每個(gè)類(lèi)對(duì)象都有屬于該類(lèi)的一張?zhí)摫?,且虛函?shù)一般都進(jìn)行了重寫(xiě),即函數(shù)名與父類(lèi)相同,但函數(shù)執(zhí)行的內(nèi)容不同,最終產(chǎn)生的結(jié)果就是,不同類(lèi)的對(duì)象調(diào)用同一函數(shù)產(chǎn)生不同的結(jié)果,由此形成了多態(tài)。

多繼承的虛函數(shù)表

給出兩個(gè)父類(lèi)Base1和Base2,Derived子類(lèi)繼承自?xún)蓚€(gè)父類(lèi)

通多監(jiān)視窗口查看子類(lèi)對(duì)象的模型:

多態(tài)中多繼承的子類(lèi)對(duì)象模型與多繼承的模型原理相同,但是VS的監(jiān)視窗口無(wú)法查看子類(lèi)新增的虛函數(shù)在需表中的位置,按照之前但繼承中打印虛表中函數(shù)的原理進(jìn)行打?。?/p>

最終得到的結(jié)果:

可以看出,子類(lèi)中增加的虛函數(shù)保存在上面的虛表中。

多繼承子類(lèi)對(duì)象模型及對(duì)象虛表:

四、繼承與多態(tài)中的常見(jiàn)問(wèn)題

1.析構(gòu)函數(shù)可以設(shè)置為虛函數(shù)嗎?

可以,在繼承體系中,最好將父類(lèi)的析構(gòu)函數(shù)設(shè)置為虛函數(shù);如果子類(lèi)中涉及到資源管理,則必須將父類(lèi)的析構(gòu)函數(shù)設(shè)置為虛函數(shù),這樣父類(lèi)和子類(lèi)中的析構(gòu)函數(shù)便會(huì)構(gòu)成重寫(xiě)(重寫(xiě)的特殊情況),形成多態(tài),通過(guò)父類(lèi)指針指向子類(lèi)對(duì)象時(shí),delete父類(lèi)對(duì)象的指針也會(huì)調(diào)用子類(lèi)的析構(gòu)函數(shù)。

子類(lèi)中涉及資源管理,調(diào)用父類(lèi)析構(gòu)函數(shù)析構(gòu)子類(lèi)對(duì)象,則會(huì)有內(nèi)存泄漏,如圖:

2.構(gòu)造函數(shù)可以設(shè)置為虛函數(shù)嗎?

不能,虛函數(shù)是放在虛表中的,虛表指針是在構(gòu)造方法的初始化列表中進(jìn)行填充的,通過(guò)虛表指針才能找到虛函數(shù),但是不調(diào)用構(gòu)造方法就沒(méi)有虛表指針,二者矛盾。即如果構(gòu)造方法是虛函數(shù),那么調(diào)用構(gòu)造方法就要通過(guò)虛表指針,但是虛表指針是要通過(guò)調(diào)用構(gòu)造方法才能填充的???截悩?gòu)造與構(gòu)造函數(shù)原理相同。

3.賦值運(yùn)算符重載函數(shù)可以設(shè)置為虛函數(shù)嗎?

可以,但是沒(méi)有意義,因?yàn)橘x值運(yùn)算符重載函數(shù)參數(shù)和返回值都是本類(lèi)類(lèi)型對(duì)象的引用,設(shè)置程序函數(shù)無(wú)法進(jìn)行重寫(xiě),無(wú)法構(gòu)成多態(tài)。

4.靜態(tài)函數(shù)可以設(shè)置為虛函數(shù)嗎 ?

虛函數(shù)必須在創(chuàng)建對(duì)象后,通過(guò)對(duì)象的前4個(gè)字節(jié)的虛表指針調(diào)用。而靜態(tài)成員函數(shù)可以通過(guò) 類(lèi)名::成員函數(shù) 的方式進(jìn)行調(diào)用,不用通過(guò)象,這樣就無(wú)法找到虛表,也無(wú)法訪(fǎng)問(wèn)虛函數(shù)。

5.內(nèi)聯(lián)函數(shù)可以是虛函數(shù)嗎?

可以設(shè)置,但是沒(méi)有意義,因?yàn)樘摵瘮?shù)關(guān)鍵字virtual和inline是矛盾的,inline屬性會(huì)被忽略,函數(shù)不會(huì)展開(kāi),而是放到虛函數(shù)表中。

6.友元函數(shù)可以是虛函數(shù)嗎?

不可以,因?yàn)関irtual只能修飾類(lèi)的成員函數(shù)。

7.為什么多態(tài)必須通過(guò)指針或者引用實(shí)現(xiàn)?

因?yàn)楫?dāng)我們用一個(gè)父類(lèi)指針或者引用指向子類(lèi)對(duì)象時(shí),會(huì)發(fā)生內(nèi)存切割,用子類(lèi)中屬于父類(lèi)的部分給父類(lèi)賦值:

	Animal& animal1 = dog;
 	Animal* animal2 = new Dog();

而下面的語(yǔ)句則不會(huì)產(chǎn)生內(nèi)存切割:

Ainmal animal3 = dog;

為什么會(huì)這樣呢?

“一個(gè)pointer或一個(gè)reference之所以支持多態(tài),是因?yàn)樗鼈儾⒉灰l(fā)內(nèi)存任何“與類(lèi)型有關(guān)的內(nèi)存委托操作; 會(huì)受到改變的。只有它們所指向內(nèi)存的大小和解釋方式 而已”。 ——《深度探索C++對(duì)象模型》

總結(jié)

原文鏈接:https://blog.csdn.net/qq_44631587/article/details/126687527

欄目分類(lèi)
最近更新