新聞中心
問(wèn)題:定義一個(gè)空的類(lèi)型,里面沒(méi)有任何的成員變量或者成員函數(shù),對(duì)這個(gè)類(lèi)型進(jìn)行 sizeof 運(yùn)算,結(jié)果是?

結(jié)果是1,因?yàn)榭疹?lèi)型的實(shí)例不包含任何信息,按道理 sizeof 計(jì)算之后結(jié)果是0,但是在聲明任何類(lèi)型的實(shí)例的時(shí)候,必須在內(nèi)存占有一定的空間,否則無(wú)法使用這些實(shí)例,至于占據(jù)多少內(nèi)存大小,由編譯器決定。
繼續(xù)問(wèn):如果在這個(gè)類(lèi)型里添加一個(gè)構(gòu)造函數(shù)和析構(gòu)函數(shù),那么結(jié)果又是多少?
還是1,因?yàn)槲覀冋{(diào)用構(gòu)造函數(shù)和析構(gòu)函數(shù),只需要知道函數(shù)的地址即可,而這些函數(shù)的地址只和類(lèi)型相關(guān),和類(lèi)型的實(shí)例無(wú)關(guān),編譯器不會(huì)為這兩個(gè)函數(shù)在實(shí)例內(nèi)添加任何額外的信息。
繼續(xù)問(wèn):如果把析構(gòu)函數(shù)變?yōu)樘摵瘮?shù)呢?結(jié)果是多少?
c++編譯器發(fā)現(xiàn)了類(lèi)型里有虛函數(shù),,就會(huì)為這個(gè)類(lèi)型生成一個(gè)虛函數(shù)表,并在該類(lèi)型的每一個(gè)實(shí)例中添加一個(gè)指向虛函數(shù)表的指針,在32位機(jī)器,指針類(lèi)型大小是4字節(jié),結(jié)果是4,64位機(jī)器中,指針大小是8字節(jié),結(jié)果是8。
面向?qū)ο蟮亩鄳B(tài)的實(shí)現(xiàn)效果
多態(tài):同樣的調(diào)用語(yǔ)句有多種不同的表現(xiàn)形態(tài)
看下面的例子:
- class animal
- {
- public:
- void sleep()
- {
- cout<<"animal sleep"<
- }
- void breathe()
- {
- cout<<"animal breathe"<
- }
- };
- class fish:public animal
- {
- public:
- void breathe()
- {
- cout<<"fish bubble"<
- }
- };
- int main(void)
- {
- fish fh;
- animal *pAn=&fh;
- pAn->breathe();
- return 0;
- }
父類(lèi)指針指向了子類(lèi)對(duì)象,調(diào)用了 breathe 方法,那么結(jié)果是animal breathe,也就是說(shuō)調(diào)用的是父類(lèi)的breathe方法。 這沒(méi)有實(shí)現(xiàn)多態(tài)性。因?yàn)镃++編譯器在編譯的時(shí)候,要確定每個(gè)對(duì)象調(diào)用的函數(shù)的地址,這稱(chēng)為早期綁定(early binding),當(dāng)fish類(lèi)的對(duì)象fh的地址賦給父類(lèi)的pAn指針時(shí),C++編譯器進(jìn)行了類(lèi)型轉(zhuǎn)換,它認(rèn)為父類(lèi)的指針變量pAn保存的就是animal對(duì)象的地址。當(dāng)在main函數(shù)中執(zhí)行pAn->breathe時(shí),調(diào)用的就是animal對(duì)象的breathe函數(shù)。
#p#
進(jìn)一步說(shuō):
在我們構(gòu)造fish類(lèi)的對(duì)象時(shí),首先要調(diào)用父類(lèi):animal類(lèi)的構(gòu)造函數(shù)去構(gòu)造animal類(lèi)的對(duì)象,然后才調(diào)用fish類(lèi)的構(gòu)造函數(shù)完成自身部分的構(gòu)造,從而拼接出一個(gè)完整的fish對(duì)象。當(dāng)將fish類(lèi)的對(duì)象轉(zhuǎn)換為animal類(lèi)型時(shí),該對(duì)象就被認(rèn)為是原對(duì)象整個(gè)內(nèi)存模型的上半部分,也就是圖中的“animal的對(duì)象所占內(nèi)存”。
那么當(dāng)利用類(lèi)型轉(zhuǎn)換后的對(duì)象指針去調(diào)用它的方法時(shí),當(dāng)然也就是調(diào)用它所在的內(nèi)存中的方法。因此,輸出animal breathe。這不是多態(tài)的表現(xiàn)形式。
多態(tài)實(shí)現(xiàn)的三個(gè)條件
必要的前提是必須有繼承關(guān)系、然后我們需要父類(lèi)指針(引用)去調(diào)用子類(lèi)的對(duì)象,且關(guān)鍵是:子類(lèi)有對(duì)父類(lèi)的虛函數(shù)的重寫(xiě)。virtual關(guān)鍵字,告訴編譯器這個(gè)函數(shù)要支持多態(tài),我們不要根據(jù)指針類(lèi)型判斷如何調(diào)用方法,而是要根據(jù)指針?biāo)赶虻膶?shí)際對(duì)象類(lèi)型來(lái)判斷如何調(diào)用。
多態(tài)的理論基礎(chǔ)
前面的例子,輸出的結(jié)果是因?yàn)榫幾g器在編譯的時(shí)候,就已經(jīng)確定了對(duì)象調(diào)用的函數(shù)的地址,要解決這個(gè)問(wèn)題就要使用遲綁定(late binding)技術(shù)。當(dāng)編譯器使用遲綁定時(shí),就會(huì)在運(yùn)行時(shí)再去確定對(duì)象的類(lèi)型以及正確的調(diào)用函數(shù)。而要讓編譯器采用遲綁定,就要在基類(lèi)中聲明函數(shù)時(shí)使用virtual關(guān)鍵字,這樣的函數(shù)我們稱(chēng)為虛函數(shù)。一旦某個(gè)函數(shù)在基類(lèi)中聲明為virtual,那么在所有的派生類(lèi)中該函數(shù)都是virtual,而不需要再顯式地聲明為virtual。
所謂的動(dòng)態(tài)聯(lián)編:根據(jù)實(shí)際的對(duì)象類(lèi)型來(lái)判斷重寫(xiě)函數(shù)的調(diào)用。
C++中多態(tài)的實(shí)現(xiàn)原理
當(dāng)類(lèi)中聲明虛函數(shù)時(shí),編譯器會(huì)在類(lèi)中生成一個(gè)虛函數(shù)表,虛函數(shù)表是一個(gè)存儲(chǔ)類(lèi)成員函數(shù)指針的數(shù)據(jù)結(jié)構(gòu),虛函數(shù)表是由編譯器自動(dòng)生成與維護(hù)的,virtual成員函數(shù)會(huì)被編譯器放入虛函數(shù)表中,存在虛函數(shù)時(shí),每個(gè)對(duì)象中都有一個(gè)指向虛函數(shù)表的指針(vptr指針)
如圖,編譯器為每個(gè)類(lèi)的對(duì)象提供一個(gè)虛表指針vptr,這個(gè)指針指向?qū)ο笏鶎兕?lèi)的虛函數(shù)表。在程序運(yùn)行時(shí),根據(jù)對(duì)象的類(lèi)型去初始化vptr,從而讓vptr正確的指向所屬類(lèi)的虛表,從而在調(diào)用虛函數(shù)時(shí),就能夠找到正確的函數(shù)。
fish fh; animal*pAn=&fh; pAn->breathe;
由于父類(lèi)的指針pAn實(shí)際指向的對(duì)象類(lèi)型是子類(lèi)的對(duì)象,因此vptr指向的子類(lèi)fish 類(lèi)的vtable,當(dāng)調(diào)用pAn->breathe時(shí),根據(jù)虛表中的函數(shù)地址找到的就是fish類(lèi)的breathe函數(shù)。正是由于每個(gè)對(duì)象調(diào)用的虛函數(shù)都是通過(guò)虛表指針來(lái)索引的,也就決定了虛表指針的正確初始化是非常重要的。換句話(huà)說(shuō),在虛表指針沒(méi)有正確初始化之前,我們不能夠去調(diào)用虛函數(shù)。
#p#
那么虛表指針在什么時(shí)候,或者說(shuō)在什么地方初始化呢?
c++是在構(gòu)造函數(shù)中進(jìn)行虛表的創(chuàng)建和虛表指針的初始化。
構(gòu)造函數(shù)的調(diào)用順序:在構(gòu)造子類(lèi)對(duì)象時(shí),要先調(diào)用父類(lèi)的構(gòu)造函數(shù),此時(shí)編譯器只“看到了”父類(lèi),并不知道后面是否后還有繼承者,它初始化父類(lèi)對(duì)象的虛表指針vptr,該虛表指針指向父類(lèi)的虛表。當(dāng)執(zhí)行子類(lèi)的構(gòu)造函數(shù)時(shí),子類(lèi)對(duì)象的虛表指針vptr被初始化, 此時(shí) vptr指向自身的虛表。當(dāng)fish類(lèi)的fh對(duì)象構(gòu)造完畢后,其內(nèi)部的虛表指針也就被初始化為指向fish類(lèi)的虛表。
在類(lèi)型轉(zhuǎn)換后,調(diào)用pAn->breathe,由于pAn實(shí)際指向的是fish類(lèi)的對(duì)象,該對(duì)象內(nèi)部的虛表指針指向的是fish類(lèi)的虛表,因此最終調(diào)用的是fish類(lèi)的breathe函數(shù)。
說(shuō)明:
通過(guò)虛函數(shù)表指針VPTR調(diào)用重寫(xiě)函數(shù)是在程序運(yùn)行時(shí)進(jìn)行的,因此需要通過(guò)尋址操作才能確定真正應(yīng)該調(diào)用的函數(shù)。而普通成員函數(shù)是在編譯時(shí)就確定了調(diào)用的函數(shù)。在效率上,虛函數(shù)的效率要低很多。出于效率考慮,沒(méi)有必要將所有成員函數(shù)都聲明為虛函數(shù)
對(duì)象在創(chuàng)建的時(shí),由編譯器對(duì)VPTR指針進(jìn)行初始化,只有當(dāng)對(duì)象的構(gòu)造完全結(jié)束后VPTR的指向才最終確定,到底是父類(lèi)對(duì)象的VPTR指向父類(lèi)虛函數(shù)表還是子類(lèi)對(duì)象的VPTR指向子類(lèi)虛函數(shù)表。
回到開(kāi)始的問(wèn)題:
- class A
- {
- void g(){.....}
- };
- 則sizeof(A)=1;如果改為如下:
- class A
- {
- public:
- virtual void f()
- {
- ......
- }
- void g(){.....}
- }
則 sizeof(A)=4,這是因?yàn)樵陬?lèi)A中存在virtual function,為了實(shí)現(xiàn)多態(tài),每個(gè)含有virtual function的類(lèi)中都隱式包含著一個(gè)靜態(tài)虛指針vptr指向該類(lèi)的靜態(tài)虛表vtable, vtable中的表項(xiàng)指向類(lèi)中的每個(gè)virtual function的入口地址。
多態(tài)是在程序進(jìn)行動(dòng)態(tài)綁定得以實(shí)現(xiàn)的,而不是編譯時(shí)就確定對(duì)象的調(diào)用方法的靜態(tài)綁定。
程序運(yùn)行到動(dòng)態(tài)綁定時(shí),通過(guò)基類(lèi)的指針?biāo)赶虻膶?duì)象類(lèi)型,通過(guò)vptr找到其所指向的vtable,然后調(diào)用其相應(yīng)的方法,即可實(shí)現(xiàn)多態(tài)。這就是動(dòng)態(tài)綁定(dynamic binding)或者叫做遲后聯(lián)編(lazy compile)。
- class base;
- base *pbase;
- class base
- {
- public:
- base()
- {
- pbase=this;
- }
- virtual void fn()
- {
- cout<<"base"<
- }
- };
- class derived:public base
- {
- void fn()
- {
- cout<<"derived"<
- }
- };
- derived aa;
- int main(void)
- {
- pbase->fn();
- return 0;
- }
在base類(lèi)的構(gòu)造函數(shù)中將this指針保存到pbase全局變量中。在定義全局對(duì)象aa,即調(diào)用derived aa;時(shí),要調(diào)用基類(lèi)的構(gòu)造函數(shù),先構(gòu)造基類(lèi)的部分,然后是子類(lèi)的部分,由這兩部分拼接出完整的對(duì)象aa。
這個(gè)this指針指向的當(dāng)然也就是aa對(duì)象,那么我們?cè)趍ain函數(shù)中利用pbase調(diào)用fn,因?yàn)閜base實(shí)際指向的是aa對(duì)象,而aa對(duì)象內(nèi)部的虛表指針指向的是自身的虛表,最終調(diào)用的當(dāng)然是derived類(lèi)中的fn函數(shù)。
在derived類(lèi)中聲明fn函數(shù)時(shí),忘了加public關(guān)鍵字,導(dǎo)致聲明為了private(默認(rèn)為private),但通過(guò)前面我們所講述的虛函數(shù)調(diào)用機(jī)制,也就明白了這個(gè)地方并不影響它輸出正確的結(jié)果。不知道這算不算C++的一個(gè)Bug,因?yàn)樘摵瘮?shù)的調(diào)用是在運(yùn)行時(shí)確定調(diào)用哪一個(gè)函數(shù),所以編譯器在編譯時(shí),并不知道pbase指向的是aa對(duì)象,所以導(dǎo)致這個(gè)奇怪現(xiàn)象的發(fā)生。如果直接用aa對(duì)象去調(diào)用,由于對(duì)象類(lèi)型是確定的(注意aa是對(duì)象變量,不是指針變量),編譯器往往會(huì)采用早期綁定,在編譯時(shí)確定調(diào)用的函數(shù),于是就會(huì)發(fā)現(xiàn)fn是私有的,不能直接調(diào)用。
#p#
如果直接在基類(lèi)的構(gòu)造函數(shù)中調(diào)用虛函數(shù),會(huì)怎樣?
在調(diào)用基類(lèi)的構(gòu)造函數(shù)時(shí),編譯器只“看到了”父類(lèi),并不知道后面是否后還有繼承者,它只是初始化父類(lèi)對(duì)象的虛表指針,讓該虛表指針指向父類(lèi)的虛表,所以看到結(jié)果當(dāng)然不正確。只有在子類(lèi)的構(gòu)造函數(shù)調(diào)用完畢后,整個(gè)虛表才構(gòu)建完畢,此時(shí)才能真正應(yīng)用C++的多態(tài)性。換句話(huà)說(shuō),不要在構(gòu)造函數(shù)中去調(diào)用虛函數(shù)實(shí)現(xiàn)多態(tài),當(dāng)然如果只是想調(diào)用本類(lèi)的函數(shù),也無(wú)所謂。
得到一個(gè)結(jié)論:
虛函數(shù)和純虛函數(shù)比較
虛函數(shù)
引入原因:為了方便使用多態(tài)特性,我們常常需要在基類(lèi)中定義虛函數(shù)。
純虛函數(shù)
引入原因:為了實(shí)現(xiàn)多態(tài)性,純虛函數(shù)有點(diǎn)像java中的接口,自己不去實(shí)現(xiàn)過(guò)程,讓繼承他的子類(lèi)去實(shí)現(xiàn)。在很多情況下,基類(lèi)本身生成對(duì)象是不合情理的。例如,動(dòng)物作為一個(gè)基類(lèi)可以派生出老虎、孔雀等子類(lèi),但動(dòng)物本身生成對(duì)象明顯不合常理。 這時(shí)我們就將動(dòng)物類(lèi)定義成抽象類(lèi),也就是包含純虛函數(shù)的類(lèi),純虛函數(shù)就是基類(lèi)只定義了函數(shù)體,沒(méi)有實(shí)現(xiàn)過(guò)程:
- virtual void Eat() = 0; 直接=0 不要 在cpp中定義就可以了
虛函數(shù)和純虛函數(shù)的區(qū)別
虛函數(shù)中的函數(shù)是實(shí)現(xiàn)的哪怕是空實(shí)現(xiàn),它的作用是這個(gè)函數(shù)在子類(lèi)里面可以被重載,運(yùn)行時(shí)動(dòng)態(tài)綁定實(shí)現(xiàn)動(dòng)態(tài),而純虛函數(shù)是個(gè)接口,是個(gè)函數(shù)聲明,在基類(lèi)中不實(shí)現(xiàn),要等到子類(lèi)中去實(shí)現(xiàn)
虛函數(shù)在子類(lèi)里可以不重載,但是虛函數(shù)必須在子類(lèi)里去實(shí)現(xiàn)。
總結(jié):
對(duì)于虛函數(shù)調(diào)用來(lái),每一個(gè)對(duì)象內(nèi)部都有一個(gè)虛表指針,該虛表指針被初始化為本類(lèi)的虛表。所以在程序中,不管你的對(duì)象類(lèi)型如何轉(zhuǎn)換,但該對(duì)象內(nèi)部的虛表指針是固定的,所以才能實(shí)現(xiàn)動(dòng)態(tài)的對(duì)象函數(shù)調(diào)用,這就是C++多態(tài)性實(shí)現(xiàn)的原理。
如果基類(lèi)有虛函數(shù):
1、每一個(gè)類(lèi)都有虛表。
2、虛表可以繼承,如果子類(lèi)沒(méi)有重寫(xiě)虛函數(shù),那么子類(lèi)虛表中仍然會(huì)有該函數(shù)的地址,只不過(guò)這個(gè)地址指向的是基類(lèi)的虛函數(shù)實(shí)現(xiàn)。如果基類(lèi)3個(gè)虛函數(shù),那么基類(lèi)的虛表中就有三項(xiàng)(虛函數(shù)地址),派生類(lèi)也會(huì)有虛表,至少有三項(xiàng),如果重寫(xiě)了相應(yīng)的虛函數(shù),那么虛表中的地址就會(huì)改變,指向自身的虛函數(shù)實(shí)現(xiàn)。如果派生類(lèi)有自己的虛函數(shù),那么虛表中就會(huì)添加該項(xiàng)。
3、派生類(lèi)的虛表中虛函數(shù)地址的排列順序和基類(lèi)的虛表中虛函數(shù)地址排列順序相同。
本文題目:c++編譯器對(duì)多態(tài)的實(shí)現(xiàn)原理總結(jié)
分享路徑:http://m.fisionsoft.com.cn/article/djpsopo.html


咨詢(xún)
建站咨詢(xún)
