網站首頁 編程語言 正文
多態
面向對象編程有三大特性:繼承、封裝和多態。
其中,多態又分為編譯時多態和運行時多態。編譯多態是通過重載函數體現的,運行多態是通過虛函數體現的。
多態是如何實現的呢?下面舉個例子:
#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 << "編譯時多態" << endl;
Base* base1 = new Base;
base1->fun1(1);
base1->fun2(1,1);
cout << "運行時多態" << endl;
Base* base = new Son1;
base->fun();
base = new Son2;
base->fun();
delete base;
base = NULL;
return 0;
}
結果:
在例子中
- 由于Base類中 fun1 和 fun2 函數簽名不同(其中,函數后面是否有const 也是簽名的一部分),從結果分析實現重載,體現了多態性。
- Base為基類,其中的函數為虛函數。子類1繼承并重寫了基類的函數,子類2繼承基類但沒有重寫基類的函數,從結果分析子類體現了多態性。
那么為什么會出現多態性,其底層的原理是什么?這里需要引出一些相關的概念來進行解釋。
虛表和虛表指針
- 虛表:虛函數表的縮寫,類中含有virtual關鍵字修飾的方法時,編譯器會自動生成虛表
- 虛表指針:在含有虛函數的類實例化對象時,對象地址的前四個字節存儲的指向虛表的指針
父類對象模型:
子類對象模型:
上圖中展示了虛表和虛表指針在基類對象和派生類對象中的模型,下面闡述實現多態的過程:
(1)編譯器在發現基類中有虛函數時,會自動為每個含有虛函數的類生成一份虛表,該表是一個一維數組,虛表里保存了虛函數的入口地址
(2)編譯器會在每個對象的前四個字節中保存一個虛表指針,即vptr,指向對象所屬類的虛表。在構造時,根據對象的類型去初始化虛指針vptr,從而讓vptr指向正確的虛表,從而在調用虛函數時,能找到正確的函數
(3)所謂的合適時機,在派生類定義對象時,程序運行會自動調用構造函數,在構造函數中創建虛表并對虛表初始化。在構造子類對象時,會先調用父類的構造函數,此時,編譯器只“看到了”父類,并為父類對象初始化虛表指針,令它指向父類的虛表;當調用子類的構造函數時,為子類對象初始化虛表指針,令它指向子類的虛表
(4)當派生類對基類的虛函數沒有重寫時,派生類的虛表指針指向的是基類的虛表;當派生類對基類的虛函數重寫時,派生類的虛表指針指向的是自身的虛表;當派生類中有自己的虛函數時,在自己的虛表中將此虛函數地址添加在后面這樣指向派生類的基類指針在運行時,就可以根據派生類對虛函數重寫情況動態的進行調用,從而實現多態性。
下面在VS2019環境下,通過程序展現:
代碼部分:
#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 強轉成int *類型,為了后面取b對象的前四個字節,前四個字節是虛表指針
// 3.*(int *)&b 取前四個字節,即vptr虛表地址
//
// *****printf("第一個虛函數地址:%p\n", *(int *)*(int *)&b);*****:
// 根據上面的解析我們知道*(int *)&b是vptr,即虛表指針.并且虛表是存放虛函數指針的
// 所以虛表中每個元素(虛函數指針)在32位編譯器下是4個字節,因此(int *)*(int *)&b
// 這樣強轉后為了后面的取四個字節.所以*(int *)*(int *)&b就是虛表的第一個元素.
// 即f()的地址.
// 那么接下來的取第二個虛函數地址也就依次類推. 始終記著vptr指向的是一塊內存,
// 這塊內存存放著虛函數地址,這塊內存就是我們所說的虛表.
cout << "class A 成員函數、成員變量的地址::" << 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 成員函數、成員變量的地址::" << 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 成員函數、成員變量的地址::" << 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;
}
運行結果:
整個程序圖示:
通過圖示我們可以看出,函數在構造后,通過vptr尋找到vtbl,進而得到所對應的成員函數。而它是怎么做到尋找到所需要的是父類還是子類的成員函數呢?
這里就要提到另一個隱藏的指針,this指針。
this指針是隱藏在類里面的一個指針,它指向當前對象,通過它可以訪問當前對象的所有成員。
如程序中如果出現:
?? ?C c;
?? ?c.vfunc1();
其實編譯器會對其進行處理,從直觀上可以將 vfunc1() 看作是下面形式(不知編譯器是否這樣轉換):
?? ?c.A::vfunc1(&c);
其中,&c就是隱藏的this指針,通過this指針,進而得到c對象需要的成員函數。
同時,這里面還包括另一個C++語法:動態綁定和靜態綁定
- 靜態綁定:綁定的是靜態類型,所對應的函數或屬性依賴于對象的靜態類型,發生在編譯期;
- 動態綁定:綁定的是動態類型,所對應的函數或屬性依賴于對象的動態類型,發生在運行期;
從上面的定義也可以看出,非虛函數一般都是靜態綁定,而虛函數都是動態綁定(如此才可實現多態性)。
所以,我們在上面代碼中加入一些代碼如下:
?? ?B bb;
?? ?A aa = (A)bb;
?? ?aa.vfunc1();
同時,加入斷點,進行調試,通過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內容是B對象強制轉換而來,aa.vfunc1()調用的是靜態綁定的A::vfunc1()。同時,在匯編中我們得到,在調用時,直接call xxxx,call后面是一個固定的地址,從這里依舊可以看出是靜態綁定。
同時,我們繼續運行下面代碼:
?? ?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對象,有一個向上轉型操作,可以確定,這應該是動態綁定。同時,在匯編代碼中,call后面并不是一個固定的地址,從這里我們也可以看出pa調用了B::vfunc1()。
原文鏈接:https://blog.csdn.net/qq_43142509/article/details/125433115
相關推薦
- 2022-09-08 python?中Mixin混入類的使用方法詳解_python
- 2023-02-26 詳解pandas中Series()和DataFrame()的區別與聯系_python
- 2022-10-10 python?pandas數據處理之刪除特定行與列_python
- 2021-12-01 CentOS7?防火墻(firewall)的操作命令大全_Linux
- 2022-05-03 C#面向對象設計原則之接口隔離原則_C#教程
- 2022-06-30 Oracle中游標Cursor的用法詳解_oracle
- 2022-06-01 AutoMapper實體映射基本用法_實用技巧
- 2023-11-13 matplotlib圖例(legend)如何自由設置其位置、大小以及樣式
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支