C/C++多態(tài)深入探究原理
多態(tài)
面向?qū)ο缶幊逃腥筇匦裕豪^承、封裝和多態(tài)。
其中,多態(tài)又分為編譯時多態(tài)和運(yùn)行時多態(tài)。編譯多態(tài)是通過重載函數(shù)體現(xiàn)的,運(yùn)行多態(tài)是通過虛函數(shù)體現(xiàn)的。
多態(tài)是如何實(shí)現(xiàn)的呢?下面舉個例子:
#include <iostream> using namespace std; class Base { public: virtual void fun() { cout << " Base::func()" << endl; } void fun1(int a) { cout << "Base::func1()" << endl; } void fun2(int a, int b) { cout << "Base::func2()" << endl; } }; class Son1 : public Base { public: virtual void fun() override { cout << " Son1::func()" << endl; } }; class Son2 : public Base { }; int main() { cout << "編譯時多態(tài)" << endl; Base* base1 = new Base; base1->fun1(1); base1->fun2(1,1); cout << "運(yùn)行時多態(tài)" << endl; Base* base = new Son1; base->fun(); base = new Son2; base->fun(); delete base; base = NULL; return 0; }
結(jié)果:
在例子中
- 由于Base類中 fun1 和 fun2 函數(shù)簽名不同(其中,函數(shù)后面是否有const 也是簽名的一部分),從結(jié)果分析實(shí)現(xiàn)重載,體現(xiàn)了多態(tài)性。
- Base為基類,其中的函數(shù)為虛函數(shù)。子類1繼承并重寫了基類的函數(shù),子類2繼承基類但沒有重寫基類的函數(shù),從結(jié)果分析子類體現(xiàn)了多態(tài)性。
那么為什么會出現(xiàn)多態(tài)性,其底層的原理是什么?這里需要引出一些相關(guān)的概念來進(jìn)行解釋。
虛表和虛表指針
- 虛表:虛函數(shù)表的縮寫,類中含有virtual關(guān)鍵字修飾的方法時,編譯器會自動生成虛表
- 虛表指針:在含有虛函數(shù)的類實(shí)例化對象時,對象地址的前四個字節(jié)存儲的指向虛表的指針
父類對象模型:
子類對象模型:
上圖中展示了虛表和虛表指針在基類對象和派生類對象中的模型,下面闡述實(shí)現(xiàn)多態(tài)的過程:
(1)編譯器在發(fā)現(xiàn)基類中有虛函數(shù)時,會自動為每個含有虛函數(shù)的類生成一份虛表,該表是一個一維數(shù)組,虛表里保存了虛函數(shù)的入口地址
(2)編譯器會在每個對象的前四個字節(jié)中保存一個虛表指針,即vptr,指向?qū)ο笏鶎兕惖奶摫?。在?gòu)造時,根據(jù)對象的類型去初始化虛指針vptr,從而讓vptr指向正確的虛表,從而在調(diào)用虛函數(shù)時,能找到正確的函數(shù)
(3)所謂的合適時機(jī),在派生類定義對象時,程序運(yùn)行會自動調(diào)用構(gòu)造函數(shù),在構(gòu)造函數(shù)中創(chuàng)建虛表并對虛表初始化。在構(gòu)造子類對象時,會先調(diào)用父類的構(gòu)造函數(shù),此時,編譯器只“看到了”父類,并為父類對象初始化虛表指針,令它指向父類的虛表;當(dāng)調(diào)用子類的構(gòu)造函數(shù)時,為子類對象初始化虛表指針,令它指向子類的虛表
(4)當(dāng)派生類對基類的虛函數(shù)沒有重寫時,派生類的虛表指針指向的是基類的虛表;當(dāng)派生類對基類的虛函數(shù)重寫時,派生類的虛表指針指向的是自身的虛表;當(dāng)派生類中有自己的虛函數(shù)時,在自己的虛表中將此虛函數(shù)地址添加在后面這樣指向派生類的基類指針在運(yùn)行時,就可以根據(jù)派生類對虛函數(shù)重寫情況動態(tài)的進(jìn)行調(diào)用,從而實(shí)現(xiàn)多態(tài)性。
下面在VS2019環(huán)境下,通過程序展現(xiàn):
代碼部分:
#include <iostream> using namespace std; class A { public: virtual void vfunc1() { cout << "A::vfunc1() -> "; } virtual void vfunc2() { cout << "A::vfunc2() -> " ; } void func1() { cout << "A::func1() -> " ; } void func2() { cout << "A::func2() -> " ; } int m_data1, m_data2; }; class B : public A { public: virtual void vfunc1() { cout << "B::vfunc1() -> " ; } void func2() { cout << "B::func2() -> " ; } int m_data3; }; class C : public B { public: virtual void vfunc1() { cout << "C::vfunc1() -> " ; } void func2() { cout << "C::func2() -> " ; } int m_data1, m_data4; }; int main() { // 這里指針操作比較混亂,在此稍微解析下: // *****printf("虛表地址:%p\n", *(int *)&b); 解析*****: // 1.&b代表對象b的起始地址 // 2.(int *)&b 強(qiáng)轉(zhuǎn)成int *類型,為了后面取b對象的前四個字節(jié),前四個字節(jié)是虛表指針 // 3.*(int *)&b 取前四個字節(jié),即vptr虛表地址 // // *****printf("第一個虛函數(shù)地址:%p\n", *(int *)*(int *)&b);*****: // 根據(jù)上面的解析我們知道*(int *)&b是vptr,即虛表指針.并且虛表是存放虛函數(shù)指針的 // 所以虛表中每個元素(虛函數(shù)指針)在32位編譯器下是4個字節(jié),因此(int *)*(int *)&b // 這樣強(qiáng)轉(zhuǎn)后為了后面的取四個字節(jié).所以*(int *)*(int *)&b就是虛表的第一個元素. // 即f()的地址. // 那么接下來的取第二個虛函數(shù)地址也就依次類推. 始終記著vptr指向的是一塊內(nèi)存, // 這塊內(nèi)存存放著虛函數(shù)地址,這塊內(nèi)存就是我們所說的虛表. cout << "class A 成員函數(shù)、成員變量的地址::" << endl; A a; cout << "A::vptr 地址 :" << *(int*)&a << endl; cout << "A::vtbl 地址 :" << *(int*)*(int*)&a << endl; cout << "A::vtbl 地址 :" << *((int*)*(int*)(&a) + 1) << endl; union { void* pv; void(A::* pfn)(); } u; u.pfn = &A::vfunc1; (a.*u.pfn)(); cout << u.pv << endl; u.pfn = &A::vfunc2; (a.*u.pfn)(); cout << u.pv << endl; u.pfn = &A::func1; (a.*u.pfn)(); cout << u.pv << endl; u.pfn = &A::func2; (a.*u.pfn)(); cout << u.pv << endl; cout << "class B 成員函數(shù)、成員變量的地址::" << endl; B b; cout << "B::vptr 地址 :" << *(int*)&b << endl; cout << "B::vtbl 地址 :" << *(int*)*(int*)&b << endl; cout << "B::vtbl 地址 :" << *((int*)*(int*)(&b) + 1) << endl; union { void* pv; void(B::* pfn)(); } m; m.pfn = &B::vfunc1; (b.*m.pfn)(); cout << m.pv << endl; m.pfn = &B::vfunc2; (b.*m.pfn)(); cout << m.pv << endl; m.pfn = &B::func1; (b.*m.pfn)(); cout << m.pv << endl; m.pfn = &B::func2; (b.*m.pfn)(); cout << m.pv << endl; cout << "class C 成員函數(shù)、成員變量的地址::" << endl; C c; cout << "C::vptr 地址 :" << *(int*)&c << endl; cout << "C::vtbl 地址 :" << *(int*)*(int*)&c << endl; cout << "C::vtbl 地址 :" << *((int*)*(int*)(&c) + 1) << endl; union { void* pv; void(C::* pfn)(); } n; n.pfn = &C::vfunc1; (c.*n.pfn)(); cout << n.pv << endl; n.pfn = &C::vfunc2; (c.*n.pfn)(); cout << n.pv << endl; n.pfn = &C::func1; (c.*n.pfn)(); cout << n.pv << endl; n.pfn = &C::func2; (c.*n.pfn)(); cout << n.pv << endl; }
運(yùn)行結(jié)果:
整個程序圖示:
通過圖示我們可以看出,函數(shù)在構(gòu)造后,通過vptr尋找到vtbl,進(jìn)而得到所對應(yīng)的成員函數(shù)。而它是怎么做到尋找到所需要的是父類還是子類的成員函數(shù)呢?
這里就要提到另一個隱藏的指針,this指針。
this指針是隱藏在類里面的一個指針,它指向當(dāng)前對象,通過它可以訪問當(dāng)前對象的所有成員。
如程序中如果出現(xiàn):
C c;
c.vfunc1();
其實(shí)編譯器會對其進(jìn)行處理,從直觀上可以將 vfunc1() 看作是下面形式(不知編譯器是否這樣轉(zhuǎn)換):
c.A::vfunc1(&c);
其中,&c就是隱藏的this指針,通過this指針,進(jìn)而得到c對象需要的成員函數(shù)。
同時,這里面還包括另一個C++語法:動態(tài)綁定和靜態(tài)綁定
- 靜態(tài)綁定:綁定的是靜態(tài)類型,所對應(yīng)的函數(shù)或?qū)傩砸蕾囉趯ο蟮撵o態(tài)類型,發(fā)生在編譯期;
- 動態(tài)綁定:綁定的是動態(tài)類型,所對應(yīng)的函數(shù)或?qū)傩砸蕾囉趯ο蟮膭討B(tài)類型,發(fā)生在運(yùn)行期;
從上面的定義也可以看出,非虛函數(shù)一般都是靜態(tài)綁定,而虛函數(shù)都是動態(tài)綁定(如此才可實(shí)現(xiàn)多態(tài)性)。
所以,我們在上面代碼中加入一些代碼如下:
B bb;
A aa = (A)bb;
aa.vfunc1();
同時,加入斷點(diǎn),進(jìn)行調(diào)試,通過vs2019窗口查看反匯編代碼,我們得到如下代碼:
B bb;
00B63237 lea ecx,[bb]
00B6323D call B::B (0B6129Eh)
A aa = (A)bb;
00B63242 lea eax,[bb]
00B63248 push eax
00B63249 lea ecx,[aa]
00B6324F call A::A (0B6128Ah)
aa.vfunc1();
00B63254 lea ecx,[aa]
00B6325A call A::vfunc1 (0B6111Dh)
由于,aa是一個A的對象而非指針,即使a內(nèi)容是B對象強(qiáng)制轉(zhuǎn)換而來,aa.vfunc1()調(diào)用的是靜態(tài)綁定的A::vfunc1()。同時,在匯編中我們得到,在調(diào)用時,直接call xxxx,call后面是一個固定的地址,從這里依舊可以看出是靜態(tài)綁定。
同時,我們繼續(xù)運(yùn)行下面代碼:
A* pa = new B;
pa->vfunc1();pa = &b;
pa->vfunc1();
得到如下反匯編:
A* pa = new B;
00B6325F push 10h
00B63261 call operator new (0B6114Fh)
00B63266 add esp,4
00B63269 mov dword ptr [ebp-174h],eax
00B6326F cmp dword ptr [ebp-174h],0
00B63276 je __$EncStackInitStart+68Fh (0B6328Bh)
00B63278 mov ecx,dword ptr [ebp-174h]
00B6327E call B::B (0B6129Eh)
00B63283 mov dword ptr [ebp-17Ch],eax
00B63289 jmp __$EncStackInitStart+699h (0B63295h)
00B6328B mov dword ptr [ebp-17Ch],0
00B63295 mov eax,dword ptr [ebp-17Ch]
00B6329B mov dword ptr [pa],eax
pa->vfunc1();
00B632A1 mov eax,dword ptr [pa]
00B632A7 mov edx,dword ptr [eax]
00B632A9 mov esi,esp
00B632AB mov ecx,dword ptr [pa]
00B632B1 mov eax,dword ptr [edx]
00B632B3 call eax
00B632B5 cmp esi,esp
00B632B7 call __RTC_CheckEsp (0B61316h) //并非固定地址pa = &b;
00B632BC lea eax,[b]
00B632BF mov dword ptr [pa],eax
pa->vfunc1();
00B632C5 mov eax,dword ptr [pa]
00B632CB mov edx,dword ptr [eax]
00B632CD mov esi,esp
00B632CF mov ecx,dword ptr [pa]
00B632D5 mov eax,dword ptr [edx]
00B632D7 call eax
00B632D9 cmp esi,esp
00B632DB call __RTC_CheckEsp (0B61316h)
在下面這段程序中,我們可以看到,指針pa指向一個B對象,有一個向上轉(zhuǎn)型操作,可以確定,這應(yīng)該是動態(tài)綁定。同時,在匯編代碼中,call后面并不是一個固定的地址,從這里我們也可以看出pa調(diào)用了B::vfunc1()。
到此這篇關(guān)于C/C++多態(tài)深入探究原理的文章就介紹到這了,更多相關(guān)C語言多態(tài)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語言中判斷兩個IPv4地址是否屬于同一個子網(wǎng)的代碼
這篇文章主要介紹了C語言中判斷兩個IPv4地址是否屬于同一個子網(wǎng)的代碼,需要的朋友可以參考下2017-09-09C++ 流插入和流提取運(yùn)算符的重載的實(shí)現(xiàn)
這篇文章主要介紹了C++ 流插入和流提取運(yùn)算符的重載的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12Pipes實(shí)現(xiàn)LeetCode(195.第十行)
這篇文章主要介紹了Pipes實(shí)現(xiàn)LeetCode(195.第十行),本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08C語言實(shí)現(xiàn)宿舍管理系統(tǒng)課程設(shè)計
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)宿舍管理系統(tǒng)課程設(shè)計,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03