c++ 虛函數(shù),虛表相關(guān)總結(jié)
面向?qū)ο?,從單一的類開始說起。
class A
{
private:
int m_a;
int m_b;
};
這個(gè)類中有兩個(gè)成員變量,都是int類型,所以這個(gè)類在內(nèi)存中占用多大的內(nèi)存空間呢?
sizeof(A), 8個(gè)字節(jié),一個(gè)int占用四個(gè)字節(jié)。下圖驗(yàn)證:

這兩個(gè)數(shù)據(jù)在內(nèi)存中是怎樣排列的呢?

原來是這樣,我們根據(jù)debug出來的地址畫出a對(duì)象在內(nèi)存的結(jié)構(gòu)圖

如果 class A 中包含成員函數(shù)呢? A 的大小又是多少?
class A
{
public:
void func1() {}
private:
int m_a;
int m_b;
};
直接告訴你答案,類的成員函數(shù)多大? 沒人能回答你,并且不是本文的重點(diǎn),類的成員函數(shù)是放在代碼區(qū)的,不算在類的大小內(nèi)。
類的對(duì)象共享這一段代碼,試想,如果每一個(gè)對(duì)象都有一段代碼,光是存儲(chǔ)這些代碼得占用多少空間?所以同一個(gè)類的對(duì)象共用一段代碼。
共用同一段代碼怎么區(qū)分不同的對(duì)象呢?
實(shí)際上,你在調(diào)用成員函數(shù)時(shí),a.func1() 會(huì)被編譯器翻譯為 A::func1(&a),也就是A* const this, this 就是 a 對(duì)象的地址。
所以根據(jù)this指針就能找到對(duì)應(yīng)的數(shù)據(jù),通過這同一段代碼來處理不同的數(shù)據(jù)。

接下來我們討論一下繼承,子類繼承父類,將會(huì)繼承父類的數(shù)據(jù),以及父類函數(shù)的調(diào)用權(quán)。
以下的測試可以驗(yàn)證這個(gè)情況。
class A
{
public:
void func1() { cout << "A func1" << endl; }
private:
int m_a;
int m_b;
};
class B : public A
{
public:
void func2() { cout << "B func2" << endl; }
private:
int m_c;
};
int main(int argc, char const* argv[])
{
B b;
b.func1();
b.func2();
return 0;
}
輸出:
// A func1 // B func2
那么對(duì)象b在內(nèi)存中的結(jié)構(gòu)是什么樣的呢?
繼承關(guān)系,先把a(bǔ)中的數(shù)據(jù)繼承過來,再有一份自己的數(shù)據(jù)。

每個(gè)包含虛函數(shù)的類都有一個(gè)虛表,虛表是屬于類的,而不是屬于某個(gè)具體的對(duì)象,一個(gè)類只需要一個(gè)虛表即可。同一個(gè)類的所有對(duì)象都使用同一個(gè)虛表。
為了指定對(duì)象的虛表,對(duì)象內(nèi)部包含指向一個(gè)虛表的指針,來指向自己所使用的虛表。為了讓每個(gè)包含虛表的類的對(duì)象都擁有一個(gè)虛表指針,編譯器在類中添加了一個(gè)指針,*__vptr,用來指向虛表。這樣,當(dāng)類的對(duì)象在創(chuàng)建時(shí)便擁有了這個(gè)指針,且這個(gè)指針的值會(huì)自動(dòng)被設(shè)置為指向類的虛表。
class A
{
public:
void func1() { cout << "A func1" << endl; }
virtual void vfunc1() { cout << "A vfunc1" << endl; }
private:
int m_a;
int m_b;
};
cout << sizeof(A);, 輸出12,A中包括兩個(gè)int型的成員變量,一個(gè)虛指針,指針占4個(gè)字節(jié)。
a的內(nèi)存結(jié)構(gòu)如下:

虛表是一個(gè)函數(shù)指針數(shù)組,數(shù)組里存放的都是函數(shù)指針,指向虛函數(shù)所在的位置。
對(duì)象調(diào)用虛函數(shù)時(shí),會(huì)根據(jù)虛指針找到虛表的位置,再根據(jù)虛函數(shù)聲明的順序找到虛函數(shù)在數(shù)組的哪個(gè)位置,找到虛函數(shù)的地址,從而調(diào)用虛函數(shù)。
調(diào)用普通函數(shù)則不像這樣,普通函數(shù)在編譯階段就指定好了函數(shù)位置,直接調(diào)用即可。
class A
{
public:
void func1() { cout << "A func1" << endl; }
virtual void vfunc1() { cout << "A vfunc1" << endl; }
private:
int m_a;
int m_b;
};
class B : public A
{
public:
void func1() { cout << "B func1" << endl; }
virtual void vfunc2() { cout << "B vfunc2" << endl; }
private:
int m_a;
};
像這樣,B類繼承自A類,B中又定義了一個(gè)虛函數(shù)vfunc2, 它的虛表又是怎么樣的呢?
給出結(jié)論,虛表如下圖所示:

我們來驗(yàn)證一下:

A a; B b; void(*avfunc1)() = (void(*)()) *(int*) (*(int*)&a); void (*bvfunc1)() = (void(*)()) *(int*) *((int*)&b); void (*bvfunc2)() = (void(*)()) * (int*)(*((int*)&b) + 4); avfunc1(); bvfunc1(); bvfunc2();
來解釋一下代碼: void(*avfunc1)() 聲明一個(gè)返回值為void, 無參數(shù)的函數(shù)指針 avfunc1, 變量名代表我們想要取A類的vfunc1這個(gè)虛函數(shù)。
右半部分的第一部分,(void(*)()) 代表我們最后要轉(zhuǎn)換成對(duì)應(yīng)上述類型的指針,右邊需要給一個(gè)地址。
我們看 (*int(*)&a), 把a(bǔ)的地址強(qiáng)轉(zhuǎn)成int*, 再解引用得到 虛指針的地址。
*(int*) (*(int*)&a) 再強(qiáng)轉(zhuǎn)解引用得到虛表的地址,最后強(qiáng)轉(zhuǎn)成函數(shù)指針。
同理得到 bvfunc1, bvfunc2, +4是因?yàn)橐粋€(gè)指針占4個(gè)字節(jié),+4得到虛表的第二項(xiàng)。
覆蓋
class A
{
public:
void func1() { cout << "A func1" << endl; }
virtual void vfunc1() { cout << "A vfunc1" << endl; }
private:
int m_a;
int m_b;
};
class B : public A
{
public:
void func1() { cout << "B func1" << endl; }
virtual void vfunc1() { cout << "B vfunc1" << endl; }
private:
int m_a;
};
子類重寫父類的虛函數(shù),需要函數(shù)簽名保持一致,該種情況在內(nèi)存中的結(jié)構(gòu)為:

多態(tài)
父類指針指向子類對(duì)象的情況下,如果指針調(diào)用的是虛函數(shù),則編譯器會(huì)將會(huì)從虛指針?biāo)傅奶摵瘮?shù)表中找到對(duì)應(yīng)的地址執(zhí)行相應(yīng)的函數(shù)。
子類很多的話,每個(gè)子類都覆蓋了對(duì)應(yīng)的虛函數(shù),則通過虛表找到的虛函數(shù)執(zhí)行后不就執(zhí)行了不同的代碼嘛,表現(xiàn)出多態(tài)了嘛。
我們把經(jīng)過虛表調(diào)用虛函數(shù)的過程稱為動(dòng)態(tài)綁定,其表現(xiàn)出來的現(xiàn)象稱為運(yùn)行時(shí)多態(tài)。動(dòng)態(tài)綁定區(qū)別于傳統(tǒng)的函數(shù)調(diào)用,傳統(tǒng)的函數(shù)調(diào)用我們稱之為靜態(tài)綁定,即函數(shù)的調(diào)用在編譯階段就可以確定下來了。
那么,什么時(shí)候會(huì)執(zhí)行函數(shù)的動(dòng)態(tài)綁定?這需要符合以下三個(gè)條件。
- 通過指針來調(diào)用函數(shù)
- 指針 upcast 向上轉(zhuǎn)型(繼承類向基類的轉(zhuǎn)換稱為 upcast)
- 調(diào)用的是虛函數(shù)
為什么父類指針可以指向子類?
子類繼承自父類,子類也屬于A的類型。
最后通過一個(gè)例子來體會(huì)一下吧:
class Shape
{
public:
virtual void draw() = 0;
};
class Rectangle : public Shape
{
void draw() { cout << "rectangle" << endl; }
};
class Circle : public Shape
{
void draw() { cout << "circle" << endl; }
};
class Triangle : public Shape
{
void draw() { cout << "triangle" << endl; }
};
int main(int argc, char const *argv[])
{
vector<Shape*> v;
v.push_back(new Rectangle());
v.push_back(new Circle());
v.push_back(new Triangle());
for (Shape* p : v) {
p->draw();
}
return 0;
}
有些話是大白話,哈哈,如果這篇文章寫的不錯(cuò),解決了你的疑惑的話,點(diǎn)個(gè)贊再走吧!
不對(duì)的地方也請(qǐng)指出來,大家一起學(xué)習(xí)進(jìn)步。
以上就是c++ 虛函數(shù),虛表相關(guān)總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于c++ 虛函數(shù),虛表的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Qt利用QScroller實(shí)現(xiàn)home界面滑動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了Qt如何利用QScroller實(shí)現(xiàn)home界面滑動(dòng)效果,文中的實(shí)現(xiàn)過程講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-11-11
C語言設(shè)置和取得socket狀態(tài)的相關(guān)函數(shù)用法
這篇文章主要介紹了C語言設(shè)置和取得socket狀態(tài)的相關(guān)函數(shù)用法,分別是setsockopt()函數(shù)和getsockopt()函數(shù)的使用介紹,需要的朋友可以參考下2015-09-09
Linux?C/C++?timeout命令實(shí)現(xiàn)運(yùn)行具有時(shí)間限制功能
inux?timeout命令的一個(gè)屬性是時(shí)間限制。可以為任何命令設(shè)置時(shí)間限制。如果時(shí)間到期,命令將停止執(zhí)行,這篇文章主要介紹了Linux?C/C++?timeout命令實(shí)現(xiàn)(運(yùn)行具有時(shí)間限制),需要的朋友可以參考下2023-02-02

