網(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類,類中包含動(dòng)物的叫聲這種方法,分別定義Dog和Cat類繼承自動(dòng)物類,那么Dog和Cat類中也會(huì)包含叫聲這種方法,但是他們具體實(shí)現(xiàn)是不同的,因?yàn)槊糠N動(dòng)物的聲音都不相同,這便是一種多態(tài)。
多態(tài)的分類
- 靜態(tài)多態(tài),也稱為靜態(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)成條件
- 在繼承體系下,父類中包含虛函數(shù)
- 子類中對(duì)父類的虛函數(shù)進(jìn)行重寫
- 通過(guò)父類的指針或者引用調(diào)用虛函數(shù)
多態(tài)的體現(xiàn):不同的類對(duì)象調(diào)用同一函數(shù),會(huì)產(chǎn)生不同的行為。
二、虛函數(shù)的重寫
重寫的定義
虛函數(shù):virtual關(guān)鍵字修飾的函數(shù)
子類中有一個(gè)跟父類完全相同的虛函數(shù),即返回值類型、函數(shù)名、形參列表都完全相同,則可以說(shuō)子類重寫了父類的虛函數(shù)。
Student類中重寫了BuyTicket方法:
注意:只要父類中函數(shù)用virtual修飾即可,子類可以不加,且虛函數(shù)的重寫與權(quán)限無(wú)關(guān)。
重寫的特殊情況
協(xié)變——返回值類型不同
父類的虛函數(shù)返回父類對(duì)象的指針或者引用,子類虛函數(shù)返回子類對(duì)象的指針或者引用。
析構(gòu)函數(shù)重寫——父類與子類析構(gòu)函數(shù)名字不同
如果父類的析構(gòu)函數(shù)為虛函數(shù),子類的析構(gòu)函數(shù)只要定義了,都能與父類的析構(gòu)函數(shù)構(gòu)成重寫??梢岳斫鉃榫幾g器對(duì)析構(gòu)函數(shù)的名字做了特殊處理,編譯后析構(gòu)函數(shù)的名字統(tǒng)一處理成destructor。
override和final關(guān)鍵字
這兩個(gè)關(guān)鍵字的主要作用都是幫助用戶檢測(cè)是否構(gòu)成重寫。
-
final
:修飾虛函數(shù),表示虛函數(shù)不可被重寫;另外final也可以修飾類,表示該類不能被繼承
-
override
:修飾虛函數(shù),檢查子類虛函數(shù)是否重寫了父類的虛函數(shù),如果沒有構(gòu)成重寫則會(huì)報(bào)錯(cuò)
區(qū)分重寫、重載、重定義
抽象類的概念
在虛函數(shù)的后面寫上=0,則這個(gè)函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也稱接口類),抽象類不能實(shí)例化對(duì)象。抽象類被集成以后如果沒有對(duì)虛函數(shù)進(jìn)行重寫,則繼承的類也是抽象類。
一般情況下,抽象類必須被繼承,且必須對(duì)虛函數(shù)進(jìn)行重寫,否則定義為抽象類則沒有實(shí)際意義。
Shape類:
class Shape { public: // 純虛函數(shù) virtual double GetArea() = 0; virtual double GetCircumference() = 0; };
三、多態(tài)的實(shí)現(xiàn)原理
父類對(duì)象模型
給出一個(gè)Base類,一個(gè)Derived類繼承Base類
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; };
父類對(duì)象模型:
總結(jié):
- 類中定義了虛函數(shù)以后,定義對(duì)象時(shí),編譯器會(huì)為對(duì)象創(chuàng)建一張?zhí)摫?,并將一個(gè)指向這張?zhí)摫淼闹羔槺4嬖趯?duì)象的前四個(gè)字節(jié),無(wú)論定義幾個(gè)虛函數(shù),對(duì)象都只比多四個(gè)字節(jié)大小。這個(gè)指針稱為函數(shù)虛表指針。
- 虛表地址是在構(gòu)造對(duì)象時(shí)進(jìn)行填充的,構(gòu)造函數(shù)如果顯式實(shí)現(xiàn),編譯器會(huì)對(duì)用戶實(shí)現(xiàn)的函數(shù)進(jìn)行修改,增加給對(duì)象前四個(gè)字節(jié)存放虛表地址的語(yǔ)句。
- 虛表本質(zhì)就是一個(gè)函數(shù)指針數(shù)組,按照聲明順序依次存放虛函數(shù)的地址
補(bǔ)充:生成默認(rèn)構(gòu)造方法的場(chǎng)景
在學(xué)習(xí)類與對(duì)象時(shí)我們知道構(gòu)造函數(shù)是是類的默認(rèn)成員函數(shù),如果用戶沒有顯式定義,編譯器會(huì)默認(rèn)生成,但是實(shí)際上并不是在所有情況下編譯器都會(huì)生成默認(rèn)的構(gòu)造函數(shù),編譯器只會(huì)在需要的時(shí)候生成構(gòu)造函數(shù)。
四種生成默認(rèn)構(gòu)造方法的場(chǎng)景
B類中包含有A類的對(duì)象,B類沒有顯式定義構(gòu)造函數(shù),A類定義了無(wú)參或者全缺省的構(gòu)造方法,則編譯器會(huì)給B類生成默認(rèn)的構(gòu)造方法。
分析:因?yàn)锳類有無(wú)參或者全缺省的構(gòu)造方法,需要在B類中調(diào)用A類的構(gòu)造方法對(duì)A類成員進(jìn)行初始化,所以需要生成B類的構(gòu)造方法,在其初始化列表中調(diào)用A類構(gòu)造方法。
如果A類沒有顯式定義構(gòu)造函數(shù),則不會(huì)生成B類構(gòu)造方法,默認(rèn)賦隨機(jī)值;如果A類定義的構(gòu)造方法不是無(wú)參或者全缺省的,則需要在初始化列表中對(duì)A類對(duì)象初始化:?
繼承中,B繼承A,A中定義了無(wú)參或者全缺省的構(gòu)造方法,B未顯式定義,則編譯器會(huì)給B類生成默認(rèn)的構(gòu)造方法。將B中繼承自A的部分初始化。
虛擬繼承中,B類虛擬繼承子A類,B類未顯式定義構(gòu)造方法,編譯器會(huì)給B類生成默認(rèn)的構(gòu)造方法,目的是:給B類對(duì)象的前4個(gè)字節(jié)填充虛基表地址
類中包含虛函數(shù),未顯示定義構(gòu)造方法,則編譯器會(huì)自動(dòng)生成構(gòu)造方法,為對(duì)象的前4個(gè)字節(jié)填充虛表地址
子類對(duì)象模型
子類虛表構(gòu)建規(guī)則
1.將父類虛表內(nèi)容拷貝一份放到子類虛表中,注意父類和子類用的不是同一張?zhí)摫恚砸陨厦娴腂ase和Derived類為例
可以看出,兩個(gè)虛表指針的地址不同,但虛表中保存的虛函數(shù)的地址都相同。
2.如果子類中將父類的虛函數(shù)進(jìn)行了重寫,則用子類的虛函數(shù)地址替換虛函數(shù)表中相同偏移量的虛函數(shù)的地址。
3.子類中增加的虛函數(shù)按照其在類當(dāng)中的聲明次序放在虛表的最后
子類中增加了兩個(gè)虛函數(shù):
但是由于VS監(jiān)視窗口中無(wú)法顯式新增加的子類,而內(nèi)存窗口只能顯式虛函數(shù)的地址,無(wú)法確認(rèn)是哪個(gè)函數(shù),所以這里通過(guò)打印的方式進(jìn)行驗(yàn)證。
通過(guò)上圖中程序的方式打印出了子類對(duì)象中虛函數(shù)的分布情況,在這里VFP是一個(gè)函數(shù)指針類型,前面加typedef表示為函數(shù)指針類型,如果不加,則是函數(shù)指針變量。
所以是用VFP*接收指向第一個(gè)虛函數(shù)指針的指針,p與*p的類型:
所以最終的結(jié)論是:子類新增的虛函數(shù)按照其在類中的聲明次序放在虛函數(shù)表的最后。
子類對(duì)象的構(gòu)造過(guò)程
構(gòu)造子類對(duì)象時(shí),在初始化列表中先調(diào)用父類的構(gòu)造函數(shù),此時(shí)對(duì)象的前4個(gè)字節(jié)保存的虛表指針指向父類的虛表,之后構(gòu)造子類自己的虛表,虛表再指針指向子類的虛表。
總結(jié)
- 虛表的本質(zhì)是函數(shù)指針數(shù)組,在編譯時(shí)生成
- 虛函數(shù)的重寫也叫覆蓋,指的是虛表中虛函數(shù)的覆蓋,重寫是語(yǔ)法層的叫法,覆蓋是原理層的叫法
- 對(duì)象中保存的是虛表指針,虛表中保存的是虛函數(shù)指針,虛函數(shù)和普通函數(shù)一樣保存在代碼段,在VS中虛表也保存在代碼區(qū)
- 同一個(gè)類的對(duì)象共用同一張?zhí)摫?,父類和子類各自擁有各自的虛表?/li>
多態(tài)的調(diào)用原理
父類對(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ù)的地址。
上面是傳遞父類對(duì)象時(shí)的調(diào)用情況,子類對(duì)象調(diào)用時(shí)的匯編代碼與父類對(duì)象相同,區(qū)別就是子類對(duì)象有自己的虛表,最終調(diào)用的是子類需表中的函數(shù)。
總結(jié)多態(tài)的原理:
創(chuàng)建對(duì)象時(shí),編譯器會(huì)給包含虛函數(shù)的類對(duì)象創(chuàng)建一張?zhí)摫?,并將虛表地址填充在?duì)象的前4個(gè)字節(jié),子類對(duì)象會(huì)拷貝父類對(duì)象的虛表,然后再對(duì)自己重寫的虛函數(shù)進(jìn)行替換,并在虛表中添加子類新增的虛函數(shù);函數(shù)調(diào)用時(shí),編譯器會(huì)先從對(duì)象的前4個(gè)字節(jié)獲取該對(duì)象虛表的地址,然后在虛表中獲取虛函數(shù)地址進(jìn)行函數(shù)調(diào)用;由于每個(gè)類對(duì)象都有屬于該類的一張?zhí)摫恚姨摵瘮?shù)一般都進(jìn)行了重寫,即函數(shù)名與父類相同,但函數(shù)執(zhí)行的內(nèi)容不同,最終產(chǎn)生的結(jié)果就是,不同類的對(duì)象調(diào)用同一函數(shù)產(chǎn)生不同的結(jié)果,由此形成了多態(tài)。
多繼承的虛函數(shù)表
給出兩個(gè)父類Base1和Base2,Derived子類繼承自兩個(gè)父類
通多監(jiān)視窗口查看子類對(duì)象的模型:
多態(tài)中多繼承的子類對(duì)象模型與多繼承的模型原理相同,但是VS的監(jiān)視窗口無(wú)法查看子類新增的虛函數(shù)在需表中的位置,按照之前但繼承中打印虛表中函數(shù)的原理進(jìn)行打印:
最終得到的結(jié)果:
可以看出,子類中增加的虛函數(shù)保存在上面的虛表中。
多繼承子類對(duì)象模型及對(duì)象虛表:
四、繼承與多態(tài)中的常見問(wèn)題
1.析構(gòu)函數(shù)可以設(shè)置為虛函數(shù)嗎?
可以,在繼承體系中,最好將父類的析構(gòu)函數(shù)設(shè)置為虛函數(shù);如果子類中涉及到資源管理,則必須將父類的析構(gòu)函數(shù)設(shè)置為虛函數(shù),這樣父類和子類中的析構(gòu)函數(shù)便會(huì)構(gòu)成重寫(重寫的特殊情況),形成多態(tài),通過(guò)父類指針指向子類對(duì)象時(shí),delete父類對(duì)象的指針也會(huì)調(diào)用子類的析構(gòu)函數(shù)。
子類中涉及資源管理,調(diào)用父類析構(gòu)函數(shù)析構(gòu)子類對(duì)象,則會(huì)有內(nèi)存泄漏,如圖:
2.構(gòu)造函數(shù)可以設(shè)置為虛函數(shù)嗎?
不能,虛函數(shù)是放在虛表中的,虛表指針是在構(gòu)造方法的初始化列表中進(jìn)行填充的,通過(guò)虛表指針才能找到虛函數(shù),但是不調(diào)用構(gòu)造方法就沒有虛表指針,二者矛盾。即如果構(gòu)造方法是虛函數(shù),那么調(diào)用構(gòu)造方法就要通過(guò)虛表指針,但是虛表指針是要通過(guò)調(diào)用構(gòu)造方法才能填充的????截悩?gòu)造與構(gòu)造函數(shù)原理相同。
3.賦值運(yùn)算符重載函數(shù)可以設(shè)置為虛函數(shù)嗎?
可以,但是沒有意義,因?yàn)橘x值運(yùn)算符重載函數(shù)參數(shù)和返回值都是本類類型對(duì)象的引用,設(shè)置程序函數(shù)無(wú)法進(jìn)行重寫,無(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ò) 類名::成員函數(shù) 的方式進(jìn)行調(diào)用,不用通過(guò)象,這樣就無(wú)法找到虛表,也無(wú)法訪問(wèn)虛函數(shù)。
5.內(nèi)聯(lián)函數(shù)可以是虛函數(shù)嗎?
可以設(shè)置,但是沒有意義,因?yàn)樘摵瘮?shù)關(guān)鍵字virtual和inline是矛盾的,inline屬性會(huì)被忽略,函數(shù)不會(huì)展開,而是放到虛函數(shù)表中。
6.友元函數(shù)可以是虛函數(shù)嗎?
不可以,因?yàn)関irtual只能修飾類的成員函數(shù)。
7.為什么多態(tài)必須通過(guò)指針或者引用實(shí)現(xiàn)?
因?yàn)楫?dāng)我們用一個(gè)父類指針或者引用指向子類對(duì)象時(shí),會(huì)發(fā)生內(nè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)存任何“與類型有關(guān)的內(nèi)存委托操作; 會(huì)受到改變的。只有它們所指向內(nèi)存的大小和解釋方式 而已”。 ——《深度探索C++對(duì)象模型》
總結(jié)
原文鏈接:https://blog.csdn.net/qq_44631587/article/details/126687527
相關(guān)推薦
- 2022-09-09 Python如何生成指定區(qū)間中的隨機(jī)數(shù)_python
- 2022-10-22 Redisson?主從一致性問(wèn)題詳解_Redis
- 2023-09-12 Nginx安裝與常見命令
- 2023-05-16 關(guān)于mongodb版本升級(jí)問(wèn)題_MongoDB
- 2022-11-11 python貪吃蛇核心功能實(shí)現(xiàn)下_python
- 2022-06-23 分布式醫(yī)療掛號(hào)系統(tǒng)整合Gateway網(wǎng)關(guān)解決跨域問(wèn)題_其它綜合
- 2022-07-30 注冊(cè)中心eureka的介紹及源碼探索
- 2022-12-07 C++?類this及返回自身對(duì)象的引用方式_C 語(yǔ)言
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支