版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
第C++虛函數(shù)表與類的內(nèi)存分布深入分析理解目錄不可定義為虛函數(shù)的函數(shù)將析構(gòu)函數(shù)定義為虛函數(shù)的作用虛函數(shù)表原理繼承關(guān)系中虛函數(shù)表結(jié)構(gòu)多重繼承的虛函數(shù)表多態(tài)調(diào)用原理對(duì)齊和補(bǔ)齊規(guī)則為什么要有對(duì)齊和補(bǔ)齊資源鏈接
不可定義為虛函數(shù)的函數(shù)
類的靜態(tài)函數(shù)和構(gòu)造函數(shù)不可以定義為虛函數(shù):
靜態(tài)函數(shù)的目的是通過類名+函數(shù)名訪問類的static變量,或者通過對(duì)象調(diào)用staic函數(shù)實(shí)現(xiàn)對(duì)static成員變量的讀寫,要求內(nèi)存中只有一份數(shù)據(jù)。而虛函數(shù)在子類中重寫,并且通過多態(tài)機(jī)制實(shí)現(xiàn)動(dòng)態(tài)調(diào)用,在內(nèi)存中需要保存不同的重寫版本。
構(gòu)造函數(shù)的作用是構(gòu)造對(duì)象,而虛函數(shù)的調(diào)用是在對(duì)象已經(jīng)構(gòu)造完成,并且通過調(diào)用時(shí)動(dòng)態(tài)綁定。動(dòng)態(tài)綁定是因?yàn)槊總€(gè)類對(duì)象內(nèi)部都有一個(gè)指針,指向虛函數(shù)表的首地址。而且虛函數(shù),類的成員函數(shù),static成員函數(shù)都不是存儲(chǔ)在類對(duì)象中,而是在內(nèi)存中只保留一份。
將析構(gòu)函數(shù)定義為虛函數(shù)的作用
類的構(gòu)造函數(shù)不能定義為虛函數(shù),析構(gòu)函數(shù)可以定義為虛函數(shù),這樣當(dāng)我們delete一個(gè)指向子類對(duì)象的基類指針時(shí)可以達(dá)到調(diào)用子類析構(gòu)函數(shù)的作用,從而動(dòng)態(tài)釋放內(nèi)存。
如下我們先定義一個(gè)基類和子類
classVirtualTableA
public:
virtual~VirtualTableA()
cout"DesturctVirtualTableA"endl;
virtualvoidprint()
cout"printvirtualtableA"endl;
classVirtualTableB:publicVirtualTableA
public:
virtual~VirtualTableB()
cout"DesturctVirtualTableB"endl;
virtualvoidprint();
voidVirtualTableB::print()
cout"thisisvirtualtableB"endl;
}
我們寫一個(gè)函數(shù)做測(cè)試
voiddestructVirtualTable()
VirtualTableA*pa=newVirtualTableB();
useTable(pa);
deletepa;
voiduseTable(VirtualTableA*pa)
//實(shí)現(xiàn)動(dòng)態(tài)調(diào)用
pa-print();
}
程序輸出
thisisvirtualtableB
DesturctVirtualTableB
DesturctVirtualTableA
在上面的例子中我們先在destructVirtualTable函數(shù)中new了一個(gè)VirtualTableB類型對(duì)象,并用基類VirtualTableA的指針指向了這個(gè)對(duì)象。
然后將基類指針對(duì)象pa傳遞給useTable函數(shù),這樣會(huì)根據(jù)多態(tài)原理調(diào)用VirtualTableB的print函數(shù),然后再執(zhí)行deletepa操作。
此時(shí)如果pa的析構(gòu)函數(shù)不寫成虛函數(shù),那么就只會(huì)調(diào)用VirtualTableA的析構(gòu)函數(shù),不會(huì)調(diào)用子類VirtualTableB的析構(gòu)函數(shù),導(dǎo)致內(nèi)存泄露。
而我們將析構(gòu)函數(shù)寫成虛析構(gòu)之后,可以看到先調(diào)用了子類VirtualTableB的析構(gòu)函數(shù),再調(diào)用了基類VirtualTableA的析構(gòu)函數(shù),達(dá)到了釋放子類空間的目的。
有人會(huì)問?將析構(gòu)函數(shù)不寫為虛函數(shù),直接delete子類對(duì)象VirtualTableB,調(diào)用子類的析構(gòu)函數(shù)不可以嗎?比如,如下的調(diào)用
VirtualTableB*pb=newVirtualTableB();
deletepa;
上述調(diào)用沒有問題,無(wú)論析構(gòu)函數(shù)是否為虛析構(gòu)都可以成功釋放子類空間。但是項(xiàng)目編程中常常會(huì)編寫一些通用接口,比如上面的useTable函數(shù),
它只接受VirtualTableA類型的指針,所以我們常常會(huì)用基類指針接受子類對(duì)象來通過多態(tài)的方式調(diào)用子類函數(shù),為了方便delete基類指針也要釋放子類空間,
就要將析構(gòu)函數(shù)設(shè)置為虛函數(shù)。
虛函數(shù)表原理
為了介紹虛函數(shù)表原理,我們先實(shí)現(xiàn)一個(gè)基類和子類
classBaseclass
public:
Baseclass():a(1024){}
virtualvoidf(){cout"Base::f"endl;}
virtualvoidg(){cout"Base::g"endl;}
virtualvoidh(){cout"Base::h"endl;}
inta;
//01234567(虛函數(shù)表空間)89101112131415(存儲(chǔ)的是a)
classDeriveClass:publicBaseclass
public:
virtualvoidf(){cout"Derive::f"endl;}
virtualvoidg2(){cout"Derive::g2"endl;}
virtualvoidh3(){cout"Derive::h3"endl;}
};
一個(gè)類對(duì)象其內(nèi)存分布的基本結(jié)構(gòu)為虛函數(shù)表地址+非靜態(tài)成員變量,類的成員函數(shù)不占用類對(duì)象的空間,他們分布在一片屬于類的共有區(qū)域。
類的靜態(tài)成員函數(shù)喝成員變量不占用類對(duì)象的空間,他們分配在靜態(tài)區(qū)。
虛函數(shù)表的地址存儲(chǔ)在類對(duì)象的起始位置。所以我們利用這個(gè)原理,通過尋址的方式訪問虛函數(shù)表里的函數(shù)
voiduseVitualTable()
Baseclassb;
b.a=1024;
cout"sizeofbis"sizeof(b)endl;
int*p=(int*)(
cout"pointeraddressofvituraltable"pendl;
cout"addressofbis"bendl;
cout"addressofais"p+2endl;
cout"addressofp+1is"p+1endl;
cout"valueofais"*(p+2)endl;
cout"addressofvituraltable"(int*)(*p)endl;
cout"sizeofintis"sizeof(int)endl;
cout"sizeofpis"sizeof(p)"sizeof(int*)is"sizeof(int*)endl;
FuncpFun=(Func)(*(int*)(*p));
pFun();
pFun=(Func)*((int*)(*p)+2);
pFun();
pFun=(Func)(*((int*)(*p)+4));
pFun();
}
上面的程序輸出
sizeofbis16
pointeraddressofvituraltable0xb6fdd0
addressofbis0xb6fdd0
addressofais0xb6fdd8
addressofp+1is0xb6fdd4
valueofais1024
addressofvituraltable0x46d890
sizeofintis4
sizeofpis8sizeof(int*)is8
Base::f
Base::g
Base::h
可以看到b的大小為16字節(jié),因?yàn)槲业臋C(jī)器是64位的,所以指針類型都占用8字節(jié),int占用4字節(jié),但是要遵循補(bǔ)齊原則,結(jié)構(gòu)體的大小要為最大成員大小的整數(shù)倍,所以要補(bǔ)齊4字節(jié),那么8+4+4=16字節(jié),關(guān)于類對(duì)象對(duì)齊和補(bǔ)齊原則稍后再詳述。
b的內(nèi)存分布如下圖
這個(gè)根據(jù)不同的機(jī)器所占的字節(jié)數(shù)不一樣,在32位機(jī)器上int為4字節(jié),虛函數(shù)表地址為4字節(jié),4+4=8字節(jié),這個(gè)再之后再說明對(duì)齊和補(bǔ)齊的原則。
b表示取b的地址,因?yàn)樘摵瘮?shù)表地址存儲(chǔ)在b的起始地址,所以b也是虛函數(shù)表的地址的地址,我們通過int*強(qiáng)轉(zhuǎn)是方便存儲(chǔ)b的地址,因?yàn)?4位機(jī)器指針都是8字節(jié),32位機(jī)器指針是4字節(jié)。
p為虛函數(shù)表的地址的地址,p+1具體移動(dòng)了4個(gè)字節(jié),因?yàn)閜+1移動(dòng)多少個(gè)字節(jié)取決于p所指向的數(shù)據(jù)類型int,int為4字節(jié),所以p+1在p的地址移動(dòng)四個(gè)字節(jié),p+2在p的地址移動(dòng)8個(gè)字節(jié)。
p只想虛函數(shù)表的地址,換句話說p存儲(chǔ)的是虛函數(shù)表的地址,虛函數(shù)表地址占用8字節(jié),p+2就是從p向后移動(dòng)8字節(jié),這樣剛好找到a的地址。
那么*(p+2)就是取a的數(shù)值。
int*(*p)就是取虛函數(shù)表的地址,轉(zhuǎn)為int*是方便讀寫。
我們將b的內(nèi)存分布以及虛函數(shù)表結(jié)構(gòu)畫出來
上圖中可以看到虛函數(shù)表中存儲(chǔ)的是虛函數(shù)的地址,所以通過不斷位移虛函數(shù)表的指針就可以達(dá)到指向不同虛函數(shù)的目的。
FuncpFun=(Func)(*(int*)(*p));
pFun();
*(int*)(*p)就是取出虛函數(shù)表首地址指向的虛函數(shù),再通過Func轉(zhuǎn)化為函數(shù)類型,然后調(diào)用pFun即可調(diào)用虛函數(shù)f。
所以想調(diào)用第二個(gè)虛函數(shù)g,將(int*)(*p)加2位移8個(gè)字節(jié)即可
pFun=(Func)*((int*)(*p)+2);
pFun();
同樣的道理調(diào)用h就不贅述了。
繼承關(guān)系中虛函數(shù)表結(jié)構(gòu)
DeriveClass繼承了BaseTest類,子類如果重寫了虛函數(shù),則子類的虛函數(shù)表中存儲(chǔ)的虛函數(shù)為子類重寫的,否則為基類的。
我們畫一下DeriveClass的虛函數(shù)表結(jié)構(gòu)
因?yàn)楹瘮?shù)f被DeriveClass重寫,所以DeriveClass的虛函數(shù)表存儲(chǔ)的是自己重寫的f。
而虛函數(shù)g和h沒有被DeriveClass重寫,所以DeriveClass虛函數(shù)表存儲(chǔ)的是基類的g和h。
另外DeriveClass虛函數(shù)表里也存儲(chǔ)了自己特有的虛函數(shù)g2和h3.
下面我們還是利用尋址的方式調(diào)用虛函數(shù)
voidderiveTable()
DeriveClassd;
int*p=(int*)(
int*virtual_tableb=(int*)(*p);
FuncpFun=(Func)(*(virtual_tableb));
pFun();
pFun=(Func)(*(virtual_tableb+2));
pFun();
pFun=(Func)(*(virtual_tableb+4));
pFun();
pFun=(Func)(*(virtual_tableb+6));
pFun();
pFun=(Func)(*(virtual_tableb+8));
pFun();
程序輸出
Derive::f
Base::g
Base::h
Derive::g2
Derive::h3
可見DeriveClass虛函數(shù)表里存儲(chǔ)的f是DeriveClass的f。
(int*)(*p)表述取出p所指向的內(nèi)存空間的內(nèi)容,p指向的正好是虛函數(shù)表的地址,所以*p就是虛函數(shù)表的地址。
因?yàn)槲覀儾恢捞摵瘮?shù)表的具體類型,所以轉(zhuǎn)為int*類型,因?yàn)橹羔樤?4位機(jī)器上都是8字節(jié),可以保證空間大小正確。
接下來就是尋址和函數(shù)調(diào)用的過程,這里不再贅述。
多重繼承的虛函數(shù)表
上面的例子我們知道,如果類有虛函數(shù),那么編譯器會(huì)為該類的實(shí)例分配8字節(jié)存儲(chǔ)虛函數(shù)表的地址。
所有繼承該類的子類也會(huì)擁有8字節(jié)的空間存儲(chǔ)自己的虛函數(shù)表地址。
多重繼承的情況就是類對(duì)象空間里存儲(chǔ)多張?zhí)摵瘮?shù)表地址。子類繼承于兩個(gè)基類,并且基類都有虛函數(shù),那么子類就有兩張?zhí)摵瘮?shù)表。
多態(tài)調(diào)用原理
當(dāng)我們通過基類指針存儲(chǔ)子類對(duì)象時(shí),調(diào)用虛函數(shù),會(huì)調(diào)用子類的實(shí)現(xiàn)版本,這叫做多態(tài)。
通過前面的實(shí)驗(yàn)和圖示,我們已經(jīng)知道如果子類重寫了基類的虛函數(shù),那么他自己的虛函數(shù)表里存儲(chǔ)的就是自己實(shí)現(xiàn)的版本。
通過基類指針存儲(chǔ)子類對(duì)象時(shí),基類指針實(shí)際指向的是子類的空間,尋址也是找到子類的虛函數(shù)表,從虛函數(shù)表中找到子類實(shí)現(xiàn)的虛函數(shù),
然后調(diào)用子類版本,從而達(dá)到多態(tài)效果。
對(duì)齊和補(bǔ)齊規(guī)則
在考察一個(gè)類對(duì)象所占空間時(shí),虛函數(shù)、成員函數(shù)(包括靜態(tài)與非靜態(tài))和靜態(tài)數(shù)據(jù)成員都是不占用類對(duì)象的存儲(chǔ)空間的。對(duì)象大小=vptr(虛函數(shù)表指針,可能不止一個(gè))+所有非靜態(tài)數(shù)據(jù)成員大小+Aligin字節(jié)大?。ㄒ蕾囉诓煌木幾g器對(duì)齊和補(bǔ)齊)
對(duì)齊:類(結(jié)構(gòu)體)對(duì)象每個(gè)成員分配內(nèi)存的起始地址為其所占空間的整數(shù)倍。
補(bǔ)齊:類(結(jié)構(gòu)體)對(duì)象所占用的總大小為其內(nèi)部最大成員所占空間的整數(shù)倍。
下面我們先定義幾個(gè)類
namespaceAligneTest
classA
classB
charch;
voidfunc()
classC
charch1;//占用1字節(jié)
charch2;//占用1字節(jié)
virtualvoidfunc()
classD
intin;
virtualvoidfunc()
classE
charm;
intin;
}
然后通過代碼測(cè)試他們的大小
externvoidaligneTest()
AligneTest::Aa;
AligneTest::Bb;
AligneTest::Cc;
AligneTest::Dd;
AligneTest::Ee;
cout"sizeof(a):"sizeof(a)endl;
cout"sizeof(b):"sizeof(b)endl;
cout"sizeof(c):"sizeof(c)endl;
cout"sizeof(d):"sizeof(d)endl;
cout"sizeof(e):"sizeof(e)endl;
}
程序輸出
sizeof(a):1
sizeof(b):1
sizeof(c):16
sizeof(d):16
sizeof(e):8
我們分別對(duì)每個(gè)類的大小做解釋
a是A的對(duì)象,A是一個(gè)空類,編譯器為了區(qū)分不同的空類,所以為每個(gè)空類對(duì)象分配1字節(jié)的空間保存其信息,用來區(qū)別不同類對(duì)象。
b是B的對(duì)象,因?yàn)锽中定義了一個(gè)char成員變量和func函數(shù),func函數(shù)不占用空間,所以b的大小為char的大小,也就是1字節(jié)。
c是C的對(duì)象,因?yàn)镃中包含虛函數(shù),所以C的對(duì)象c中會(huì)分配8字節(jié)用來存儲(chǔ)虛函數(shù)表,虛函數(shù)表放在c內(nèi)存的首地址,然后是ch1,
以及ch2。假設(shè)c的起始地址為0,那么0~7字節(jié)存儲(chǔ)虛函數(shù)表地址,第8個(gè)字節(jié)是1的整數(shù)倍,所以不同對(duì)齊,第8個(gè)字節(jié)存儲(chǔ)ch1。
第9個(gè)字節(jié)是1的整數(shù)倍,所以第9個(gè)字節(jié)存儲(chǔ)ch2。那么c的大小為8+2=10,因?yàn)檠a(bǔ)齊規(guī)則要求c的大小為最大成員大小的整數(shù)
倍,最大成員為虛函數(shù)表地址8字節(jié),所以要補(bǔ)齊6個(gè)字節(jié),10+6=16,所以c的大小為16字節(jié)。
其內(nèi)存分配如下圖
d是D的對(duì)象,因?yàn)镈中包含虛函數(shù),所以D的對(duì)象d中會(huì)分配8字節(jié)空間存儲(chǔ)虛函數(shù)表地址,比如0~7字節(jié)存儲(chǔ)虛函數(shù)表地址,接下來第8個(gè)字節(jié),
因?yàn)閕nt為4字節(jié),8是4的整數(shù)倍,所以不需要對(duì)齊,第8~11字節(jié)存儲(chǔ)in,這樣d的大小變?yōu)?+4=12,因?yàn)楦鶕?jù)補(bǔ)齊規(guī)則需要補(bǔ)齊4字節(jié),總共
溫馨提示
- 1. 本站所有資源如無(wú)特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 中學(xué)教師職稱晉升制度
- 企業(yè)員工培訓(xùn)與素質(zhì)拓展訓(xùn)練制度
- 交通宣傳教育材料制作與發(fā)放制度
- 2026年工程監(jiān)理員工程質(zhì)量控制與安全管理試題
- 2026年全科醫(yī)師規(guī)范化培訓(xùn)結(jié)業(yè)考試醫(yī)學(xué)診斷技能題
- 鑄造培訓(xùn)課件范文
- 昆蟲標(biāo)本鑒定服務(wù)合同
- 古對(duì)今課件練習(xí)題
- 2026適應(yīng)氣候變化從業(yè)人員指南:自然環(huán)境風(fēng)險(xiǎn)與解決方案-
- 2024年靈璧縣幼兒園教師招教考試備考題庫(kù)帶答案解析(奪冠)
- 2026年上半年眉山天府新區(qū)公開選調(diào)事業(yè)單位工作人員的參考題庫(kù)附答案
- 用電安全隱患檢測(cè)的新技術(shù)及應(yīng)用
- 新疆克州阿合奇縣2024-2025學(xué)年七年級(jí)上學(xué)期期末質(zhì)量檢測(cè)英語(yǔ)試卷(含答案及聽力原文無(wú)音頻)
- 《水庫(kù)泥沙淤積及影響評(píng)估技術(shù)規(guī)范》
- 2023-2024學(xué)年浙江省杭州市西湖區(qū)教科版五年級(jí)上冊(cè)期末考試科學(xué)試卷
- GB/T 7948-2024滑動(dòng)軸承塑料軸套極限PV試驗(yàn)方法
- DL∕T 1057-2023 自動(dòng)跟蹤補(bǔ)償消弧線圈成套裝置技術(shù)條件
- AQ 2003-2018 軋鋼安全規(guī)程(正式版)
- 村委會(huì)指定監(jiān)護(hù)人證明書模板
- 送給業(yè)主禮物方案
- JJG 393-2018便攜式X、γ輻射周圍劑量當(dāng)量(率)儀和監(jiān)測(cè)儀
評(píng)論
0/150
提交評(píng)論