一.概念
二.多態(tài)的定義和實(shí)現(xiàn)
1.簡單使用
虛函數(shù)
虛函數(shù)重寫
注意,虛函數(shù)重寫的是實(shí)現(xiàn)部分,如果父類在參數(shù)上給了缺省值,那么重寫時(shí)依然使用父類的缺省值,與子類的缺省值無關(guān)。
2.虛函數(shù)重寫的兩個(gè)例外
1.協(xié)變
派生類重寫基類虛函數(shù)時(shí),與基類虛函數(shù)返回值類型不同。即基類虛函數(shù)返回基類對象的指針或者引用,派生類虛函數(shù)返回派生類對象的指針或者引用時(shí),稱為協(xié)變。
2.析構(gòu)函數(shù)的重寫
如果基類的析構(gòu)函數(shù)為虛函數(shù),此時(shí)派生類析構(gòu)函數(shù)只要定義,無論是否加virtual關(guān)鍵字,都與基類的析構(gòu)函數(shù)構(gòu)成重寫,雖然基類與派生類析構(gòu)函數(shù)名字不同。雖然函數(shù)名不相同,看起來違背了重寫的規(guī)則,其實(shí)不然,這里可以理解為編譯器對析構(gòu)函數(shù)的名稱做了特殊處理,編譯后析構(gòu)函數(shù)的名稱統(tǒng)一處理成destructor。
如果這里的析構(gòu)函數(shù)不處理成虛構(gòu)函數(shù),那么p是person類型,它就會調(diào)用person的析構(gòu)函數(shù),就會導(dǎo)致student無法析構(gòu)完全從而致使內(nèi)存泄漏。所以對于需要被繼承的類,最好在析構(gòu)函數(shù)上都加上virtal。
3. C++11 override 和 final
final:修飾虛函數(shù),表示該虛函數(shù)不能再被重寫
override: 檢查派生類虛函數(shù)是否重寫了基類某個(gè)虛函數(shù),如果沒有重寫編譯報(bào)錯(cuò)
4.重載,重定義,重寫對比
三.多態(tài)的原理
1.虛函數(shù)表
過觀察測試我們發(fā)現(xiàn)b對象是8bytes,除了_b成員,還多一個(gè)__vfptr放在對象的前面(注意有些平臺可能會放到對象的最后面,這個(gè)跟平臺有關(guān),另外會有內(nèi)存對齊),對象中的這個(gè)指針我們叫做虛函數(shù)表指針(v代表virtual,f代表function)。一個(gè)含有虛函數(shù)的類中都至少都有一個(gè)虛函數(shù)表指針,因?yàn)樘摵瘮?shù)的地址要被放到虛函數(shù)表中,虛函數(shù)表也簡稱虛表。那么派生類中這個(gè)表放了些什么呢?我們接著往下分析
通過觀察和測試,我們發(fā)現(xiàn)了以下幾點(diǎn)問題:
1. 派生類對象d中也有一個(gè)虛表指針,d對象由兩部分構(gòu)成,一部分是父類繼承下來的成員,虛表指針也就是存在部分的另一部分是自己的成員。
2. 基類b對象和派生類d對象虛表是不一樣的,這里我們發(fā)現(xiàn)Func1完成了重寫,所以d的虛表 中存的是重寫的Derive::Func1,所以虛函數(shù)的重寫也叫作覆蓋,覆蓋就是指虛表中虛函數(shù)的覆蓋。重寫是語法的叫法,覆蓋是原理層的叫法。
3. 另外Func2繼承下來后是虛函數(shù),所以放進(jìn)了虛表,F(xiàn)unc3也繼承下來了,但是不是虛函數(shù),所以不會放進(jìn)虛表。
4. 虛函數(shù)表本質(zhì)是一個(gè)存虛函數(shù)指針的指針數(shù)組,一般情況這個(gè)數(shù)組最后面放了一個(gè)nullptr。
5. 總結(jié)一下派生類的虛表生成:a.先將基類中的虛表內(nèi)容拷貝一份到派生類虛表中 b.如果派生類重寫了基類中某個(gè)虛函數(shù),用派生類自己的虛函數(shù)覆蓋虛表中基類的虛函數(shù)
c.派生類自己新增加的虛函數(shù)按其在派生類中的聲明次序增加到派生類虛表的最后。
6. 這里還有一個(gè)童鞋們很容易混淆的問題:虛函數(shù)存在哪的?虛表存在哪的? 答:虛函數(shù)存在虛表,虛表存在對象中。注意上面的回答的錯(cuò)的。但是很多童鞋都是這樣深以為然的。注意虛表存的是虛函數(shù)指針,不是虛函數(shù),虛函數(shù)和普通函數(shù)一樣的,都是存在代碼段的,只是他的指針又存到了虛表中。另外對象中存的不是虛表,存的是虛表指針。那么虛表存在哪的呢?實(shí)際我們?nèi)ヲ?yàn)證一下會發(fā)現(xiàn)vs下是存在代碼段的,Linux g++下大家自己去驗(yàn)證?
2.總結(jié)
一.虛函數(shù)表本質(zhì)是一個(gè)函數(shù)指針數(shù)組,里面存的是函數(shù)指針。
二.重寫實(shí)際上是先把父類的虛表拷貝到子類,再將子類重寫的虛函數(shù)的地址覆蓋到虛表上,所以重寫也被叫做覆蓋。
三.對于編譯器來說,你傳父類,看到的是父類,你傳子類經(jīng)過切片看到的還是父類。在經(jīng)過重寫后會生成一個(gè)_vfptr的指針,該指針指向虛函數(shù)表。普通調(diào)用是在編譯時(shí)生成地址,多態(tài)調(diào)用則是在運(yùn)行時(shí)在指向?qū)ο蟮奶摵瘮?shù)表里找調(diào)用函數(shù)的地址。這樣就完成了互不影響。
四.再回過頭來看多態(tài)的兩個(gè)條件。1.為什么必須是父類的指針或引用,不能是子類指針或者父類對象呢?因?yàn)槿绻亲宇惖闹羔樆蛞媚敲淳椭荒苤赶蜃宇惒粫a(chǎn)生多態(tài)。如果是父類對象,那么子類對象在賦值時(shí)會進(jìn)行切片拷貝到父類對象,但虛表并不會一起拷貝,所以即使給父類進(jìn)行拷貝后,它的虛表依然指向父類的函數(shù)(而指針進(jìn)行切片后并不會進(jìn)行拷貝,而是指向子類里父類的那一部分,也指向子類的虛表)。
3.靜態(tài)綁定和動態(tài)綁定
四.單繼承和多繼承
1.單繼承
觀察下圖中的監(jiān)視窗口中我們發(fā)現(xiàn)看不見func3和func4。這里是編譯器的監(jiān)視窗口故意隱藏了這兩個(gè)函數(shù),也可以認(rèn)為是他的一個(gè)小bug。那么我們?nèi)绾尾榭磀的虛表呢?下面我們使用代碼打印出虛表中的函數(shù)(上文說到虛函數(shù)表是一個(gè)函數(shù)指針數(shù)組里存的是函數(shù)指針,我們只需要通過遍歷虛表,再對里面的成員進(jìn)行解引用就可以看到虛表里的每一個(gè)函數(shù)了)。
從這里可以很清楚的看到它在虛表里確實(shí)存了func3和func4的函數(shù)指針,但在監(jiān)視窗口內(nèi)卻無法看到。
2.多繼承
1.多繼承的虛表
單繼承會繼承父類的虛表然后進(jìn)行重寫(覆蓋),那么多繼承會產(chǎn)生多張?zhí)摫韱幔?/strong>
答案是的,它會繼承多張?zhí)摫怼?/strong>
根據(jù)上文的單繼承,我們可以知道子類自己的成員函數(shù)指針也是放在虛表里,那么在多繼承里,子類有多張?zhí)摫?,自己的成員函數(shù)指針放在那一張?zhí)摫砝锬兀窟€是與之前方法一樣,我們將多張?zhí)摫碇赶虻暮瘮?shù)都打印出來。
我們可以看到func3是存在第一張?zhí)摫砝锏?,所以可以得出結(jié)論,多繼承子類自己的成員函數(shù)指針存在第一張?zhí)摫砝铩?/strong>
2.多繼承的重寫
ps:下面代碼有微小變動,所以地址與上面不同
這里有一個(gè)奇怪的現(xiàn)象,我們可以看到兩個(gè)fun1的地址不同,但卻都調(diào)用了func1這個(gè)函數(shù),為什么這個(gè)地方覆蓋的地址會不一樣呢?
接下來轉(zhuǎn)到匯編
一步一步來看,首先到call指令,這時(shí)就到了虛表的地址準(zhǔn)備開始跳轉(zhuǎn)。
到達(dá)虛表里開始正常調(diào)用函數(shù),jmp后面括號里的地址是什么呢?。
可以看出這是func1的地址我們跳轉(zhuǎn)到了Derve里Base1的func1里了,接著打斷點(diǎn),到達(dá)ptr2的位置。
這里的eax的地址與之前的不一樣了,因?yàn)樗沁M(jìn)入了Derive里base2的虛表了。
這里不僅jump的地址與之前不一樣,jmp后面的地址也與之前不一樣了。接著往下走。
按下F10,它并沒有跳到func1里,而是對ecx減了一個(gè)8,再進(jìn)行跳轉(zhuǎn)。接下來進(jìn)行跳轉(zhuǎn)。
在jmp后又跳轉(zhuǎn)到了另一個(gè)jmp指令里,這里的00B31230就是之前在Base1調(diào)用func1進(jìn)行jmp時(shí)的地址。后面的0B327E0h跟上面的base1的func1地址也相同??梢钥闯鰞蓚€(gè)指針都調(diào)用的是同一個(gè)fun1,只是ptr2多走了一部分。
ptr2進(jìn)行了一個(gè)封裝,其中關(guān)鍵就是倒數(shù)第二步,ecx-8。ecx里存的是this指針的值,這里為什么要讓this指針減8呢?回歸最開始的一張圖。
func1是derive的成員函數(shù),那么就this指針就必須指向derive對象,而之前的ptr1恰好指向了derive對象,所以不需要額外操作,但ptr2需要通過減法來讓它指向derive對象。這里可以回過頭再來看一下ptr1準(zhǔn)備call的時(shí)候。
ptr1直接將自己的值給了ecx,而ptr2是將值給了ecx后又對ecx進(jìn)行了減8。
結(jié)論
調(diào)用函數(shù)實(shí)際分為兩個(gè)部分:1.傳rhis指針(ecx)2.call地址,然后進(jìn)行跳轉(zhuǎn)。ptr2繞了一大圈就是為了修正this指針,讓this指針指向Derive對象。Derive的func1實(shí)際只有一個(gè),所以可以認(rèn)為base1里的地址是一個(gè)真地址,base2里的地址是一個(gè)"假"地址。這個(gè)“假”是指它進(jìn)行了封裝,先修正,再跳轉(zhuǎn)。
五.抽象類
在虛函數(shù)的后面寫上 =0 ,則這個(gè)函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也叫接口類),抽象類不能實(shí)例化出對象。派生類繼承后也不能實(shí)例化出對象,只有重寫純虛函數(shù),派生類才能實(shí)例化出對象。純虛函數(shù)規(guī)范了派生類必須重寫,另外純虛函數(shù)更體現(xiàn)出了接口繼承。
抽象類強(qiáng)制了派生類重寫了虛函數(shù)。
接口繼承和實(shí)現(xiàn)繼承
普通函數(shù)的繼承是一種實(shí)現(xiàn)繼承,派生類繼承了基類函數(shù),可以使用函數(shù),繼承的是函數(shù)的實(shí)
現(xiàn)。虛函數(shù)的繼承是一種接口繼承,派生類繼承的是基類虛函數(shù)的接口,目的是為了重寫,達(dá)成
多態(tài),繼承的是接口。所以如果不實(shí)現(xiàn)多態(tài),不要把函數(shù)定義成虛函數(shù)。
六.問答題
什么是多態(tài)
是面向?qū)ο蟪绦蛟O(shè)計(jì)語言中的一種機(jī)制。這種機(jī)制實(shí)現(xiàn)了方法的定義與具體的對象無關(guān),而對方法的調(diào)用則可以關(guān)聯(lián)于具體的對象。多態(tài)分為靜態(tài)多態(tài)(函數(shù)重載等等)和動態(tài)多態(tài)(繼承中虛函數(shù)重寫+父類指針或引用調(diào)用)。更靈活方便的多種形態(tài)調(diào)用。
多態(tài)的實(shí)現(xiàn)原理
1.靜態(tài)通過函數(shù)名修飾規(guī)則;2.動態(tài)通過虛函數(shù)表。
inline函數(shù)可以是虛函數(shù)嗎
可以編譯通過。但是在編譯時(shí)編譯器自動忽略掉內(nèi)聯(lián)屬性,這個(gè)函數(shù)就不再是inline,因?yàn)樘摵瘮?shù)要放到虛表中去。所以語法可以通過,但實(shí)際已經(jīng)不是內(nèi)聯(lián)了。
靜態(tài)成員可以是虛函數(shù)嗎
不能。因?yàn)殪o態(tài)成員沒有this指針,使用類型::成員函數(shù)的調(diào)用方式無法訪問虛函數(shù)表,所以靜態(tài)成員函數(shù)無法放進(jìn)虛函數(shù)表。
構(gòu)造函數(shù)可以是虛函數(shù)嗎
不能。虛表是在編譯時(shí)生成的,虛表指針是在構(gòu)造函數(shù)初始化列表階段才初始化的。
對象訪問普通函數(shù)快還是虛函數(shù)更快?
首先如果是普通對象,是一樣快的。如果是指針對象或者是引用對象,則調(diào)用的普通函數(shù)快,因?yàn)闃?gòu)成多態(tài),運(yùn)行時(shí)調(diào)用虛函數(shù)需要到虛函數(shù)表中去查找。
虛函數(shù)表是在什么階段生成的,存在哪的?
虛函數(shù)表是在編譯階段就生成的,一般情況下存在代碼段(常量區(qū))的。
什么是抽象類?抽象類的作用?文章來源:http://www.zghlxwxcb.cn/news/detail-610013.html
抽象類強(qiáng)制重寫了虛函數(shù),另外抽象類體現(xiàn)出了接口繼承關(guān)系。文章來源地址http://www.zghlxwxcb.cn/news/detail-610013.html
到了這里,關(guān)于【C++進(jìn)階】:多態(tài)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!