c++ 虛函數(shù),虛表相關總結
面向對象,從單一的類開始說起。
class A { private: int m_a; int m_b; };
這個類中有兩個成員變量,都是int類型,所以這個類在內存中占用多大的內存空間呢?
sizeof(A), 8個字節(jié),一個int占用四個字節(jié)。下圖驗證:
這兩個數(shù)據(jù)在內存中是怎樣排列的呢?
原來是這樣,我們根據(jù)debug出來的地址畫出a對象在內存的結構圖
如果 class A 中包含成員函數(shù)呢? A 的大小又是多少?
class A { public: void func1() {} private: int m_a; int m_b; };
直接告訴你答案,類的成員函數(shù)多大? 沒人能回答你,并且不是本文的重點,類的成員函數(shù)是放在代碼區(qū)的,不算在類的大小內。
類的對象共享這一段代碼,試想,如果每一個對象都有一段代碼,光是存儲這些代碼得占用多少空間?所以同一個類的對象共用一段代碼。
共用同一段代碼怎么區(qū)分不同的對象呢?
實際上,你在調用成員函數(shù)時,a.func1() 會被編譯器翻譯為 A::func1(&a),也就是A* const this, this 就是 a 對象的地址。
所以根據(jù)this指針就能找到對應的數(shù)據(jù),通過這同一段代碼來處理不同的數(shù)據(jù)。
接下來我們討論一下繼承,子類繼承父類,將會繼承父類的數(shù)據(jù),以及父類函數(shù)的調用權。
以下的測試可以驗證這個情況。
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
那么對象b在內存中的結構是什么樣的呢?
繼承關系,先把a中的數(shù)據(jù)繼承過來,再有一份自己的數(shù)據(jù)。
每個包含虛函數(shù)的類都有一個虛表,虛表是屬于類的,而不是屬于某個具體的對象,一個類只需要一個虛表即可。同一個類的所有對象都使用同一個虛表。
為了指定對象的虛表,對象內部包含指向一個虛表的指針,來指向自己所使用的虛表。為了讓每個包含虛表的類的對象都擁有一個虛表指針,編譯器在類中添加了一個指針,*__vptr,用來指向虛表。這樣,當類的對象在創(chuàng)建時便擁有了這個指針,且這個指針的值會自動被設置為指向類的虛表。
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中包括兩個int型的成員變量,一個虛指針,指針占4個字節(jié)。
a的內存結構如下:
虛表是一個函數(shù)指針數(shù)組,數(shù)組里存放的都是函數(shù)指針,指向虛函數(shù)所在的位置。
對象調用虛函數(shù)時,會根據(jù)虛指針找到虛表的位置,再根據(jù)虛函數(shù)聲明的順序找到虛函數(shù)在數(shù)組的哪個位置,找到虛函數(shù)的地址,從而調用虛函數(shù)。
調用普通函數(shù)則不像這樣,普通函數(shù)在編譯階段就指定好了函數(shù)位置,直接調用即可。
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中又定義了一個虛函數(shù)vfunc2, 它的虛表又是怎么樣的呢?
給出結論,虛表如下圖所示:
我們來驗證一下:
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)() 聲明一個返回值為void, 無參數(shù)的函數(shù)指針 avfunc1, 變量名代表我們想要取A類的vfunc1這個虛函數(shù)。
右半部分的第一部分,(void(*)()) 代表我們最后要轉換成對應上述類型的指針,右邊需要給一個地址。
我們看 (*int(*)&a), 把a的地址強轉成int*, 再解引用得到 虛指針的地址。
*(int*) (*(int*)&a) 再強轉解引用得到虛表的地址,最后強轉成函數(shù)指針。
同理得到 bvfunc1, bvfunc2, +4是因為一個指針占4個字節(jié),+4得到虛表的第二項。
覆蓋
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ù)簽名保持一致,該種情況在內存中的結構為:
多態(tài)
父類指針指向子類對象的情況下,如果指針調用的是虛函數(shù),則編譯器會將會從虛指針所指的虛函數(shù)表中找到對應的地址執(zhí)行相應的函數(shù)。
子類很多的話,每個子類都覆蓋了對應的虛函數(shù),則通過虛表找到的虛函數(shù)執(zhí)行后不就執(zhí)行了不同的代碼嘛,表現(xiàn)出多態(tài)了嘛。
我們把經過虛表調用虛函數(shù)的過程稱為動態(tài)綁定,其表現(xiàn)出來的現(xiàn)象稱為運行時多態(tài)。動態(tài)綁定區(qū)別于傳統(tǒng)的函數(shù)調用,傳統(tǒng)的函數(shù)調用我們稱之為靜態(tài)綁定,即函數(shù)的調用在編譯階段就可以確定下來了。
那么,什么時候會執(zhí)行函數(shù)的動態(tài)綁定?這需要符合以下三個條件。
- 通過指針來調用函數(shù)
- 指針 upcast 向上轉型(繼承類向基類的轉換稱為 upcast)
- 調用的是虛函數(shù)
為什么父類指針可以指向子類?
子類繼承自父類,子類也屬于A的類型。
最后通過一個例子來體會一下吧:
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; }
有些話是大白話,哈哈,如果這篇文章寫的不錯,解決了你的疑惑的話,點個贊再走吧!
不對的地方也請指出來,大家一起學習進步。
以上就是c++ 虛函數(shù),虛表相關總結的詳細內容,更多關于c++ 虛函數(shù),虛表的資料請關注腳本之家其它相關文章!
相關文章
C++二叉樹的前序中序后序非遞歸實現(xiàn)方法詳細講解
前序遍歷的順序是根、左、右。任何一顆樹都可以認為分為左路節(jié)點,左路節(jié)點的右子樹。先訪問左路節(jié)點,再來訪問左路節(jié)點的右子樹。把訪問左路節(jié)點的右子樹看成一個子問題,就可以完整遞歸訪問了2023-03-03Qt圖形圖像開發(fā)之高性能曲線圖模塊QCustomplot庫詳細使用方法與實例(支持動、靜曲線圖)
這篇文章主要介紹了Qt圖形圖像開發(fā)之高性能曲線圖模塊QCustomplot庫詳細使用方法與實例(支持動、靜曲線圖),需要的朋友可以參考下2020-03-03