日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

C++多态的原理(虚函数指针和虚函数表)

發布時間:2023/12/15 c/c++ 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C++多态的原理(虚函数指针和虚函数表) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

C++多態的原理 (虛函數指針和虛函數表)

  • 1.虛函數指針和虛函數表
  • 2.繼承中的虛函數表
    • 2.1單繼承中的虛函數表
    • 2.2多繼承中的虛函數表
  • 3.多態的原理
  • 4.總結

1.虛函數指針和虛函數表

以下代碼:
問類實例化出的對象占幾個字節?

#include<iostream> using namespace std; class A {int m_a; public:void func() {cout << "調用類A的func()函數" << endl;} }; int main() {A a;cout <<"sizeof(a):"<<sizeof(a) << endl;system("pause");return 0; }

結果顯而易見 sizeof(a)=4,因為成員函數存放在公共的代碼段, 所以只計算成員變量m_a(int型)所占字節的大小。


當我們將成員函數定義為虛函數時,結果卻出現了不同的情況:

#include<iostream> using namespace std; class A {int m_a; public:virtual void func() {cout << "調用類A的func()函數" << endl;} }; int main() {A a;cout <<"sizeof(a):" <<sizeof(a) << endl;system("pause");return 0; }



我們注意到當成員函數定義為虛函數時,同一個類的實例化對象大小變為了8個字節。多出來的4個字節是怎么回事呢?
另外在對象a中還多出了一個void**類型名為_vfptr的變量。它是一個二級指針, 指針在32位平臺中占4字節, 所以這里的結果是8(m_a的4字節+_vfptr的4字節), 那么_vfptr到底是個什么東西? 類中有了虛函數之后才有了_vfptr, 它們之間到底有著什么聯系?

當一個類中有虛函數時,編譯期間就會為這個類分配一片連續的內存 (虛表vftable),來存放虛函數的地址。類中只保存著指向虛表的指針 (虛函數表指針_vfptr) ,當這個類實例出對象時,每個對象都會有一個虛函數表指針_vfptr 。虛函數其實和普通函數一樣,存放在代碼段。


一個含有虛函數的類中都至少都有一個虛函數表,因為虛函數的地址要被放到虛函數表中,那么派生類中這個表放了些什么呢?我們接著往下分析。

針對上面的代碼我們做出以下改造:

1.我們增加一個派生類去繼承基類
2.基類中重寫Func1
3.派生類再增加一個虛函數Func2和一個普通函數Func3

#include<iostream> using namespace std; class Base { public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;} private:int _b = 1; }; class Derive : public Base { public:virtual void Func1(){cout << "Derive::Func1()" << endl;} private:int _d = 2; }; int main() {Base b;Derive d;return 0; }


通過觀察和測試,我們發現了以下幾點問題:

  • 派生類對象d中也有一個虛表指針,d對象由兩部分構成,一部分是父類繼承下來的成員,虛表指針也就是存在的部分,的另一部分是自己的成員。
  • 基類b對象和派生類d對象虛表是不一樣的,這里我們發現Func1()完成了重寫,所以d的虛表中存的是重寫的Derive::Func1,所以虛函數的重寫也叫作覆蓋, 覆蓋就是指虛表中虛函數的覆蓋。重寫是語法的叫法,覆蓋是原理層的叫法。
  • Func2()繼承下來后是虛函數,所以放進了虛表,Func3()也繼承下來了,但是不是虛函數,所以不會放進虛表。
  • 虛函數表本質是一個存虛函數指針的指針數組,這個數組最后面放了一個nullptr。
  • 總結一下派生類的虛表生成:a.先將基類中的虛表內容拷貝一份到派生類虛表中 b.如果派生類重寫了基類中某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數 c.派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后。
  • 虛函數存在哪的?虛表存在哪的?
    答:虛函數存在虛表,虛表存在對象中。上面的回答是錯的。注意虛表存的是虛函數指針,不是虛函數,虛函數和普通函數一樣的,都是存在代碼段的,只是他的指針又存到了虛表中。另外對象中存的不是虛表,存的是虛表指針。那么虛表存在哪的呢?實際我們去驗證一下會發現在vs下是存在代碼段的。
  • 總結:

    當一個類中有虛函數時, 在編譯期間,就會為這個類分配一片連續的內存 (虛表vftable), 來存放虛函數的地址, 類中只保存著指向虛表的指針 (虛函數指針_vfptr) , 虛函數其實和普通函數一樣, 存放在代碼段。當這個類實例出對象時, 每個對象都會有一個虛函數表指針_vfptr 。虛表本質上是一個在編譯時就已經確定好了的void* 類型的指針數組 。
    注意 : 虛函數表為了標志結尾, 會在虛表最后一個元素位置保存一個空指針。所以看到的虛表元素個數比實際虛函數個數多一個。

    2.繼承中的虛函數表

    在有虛函數的類被繼承后, 虛表也會被拷貝給派生類。編譯器會給派生類新分配一片空間來拷貝基類的虛表, 將這個虛表的指針給派生類, 而并不是沿用基類的虛表。在發生虛函數的重寫時, 重寫的是派生類為了拷貝基類虛表新創建的虛表中的虛函數地址。 虛表為所有這個類的對象所共享,是通過給每個對象一個虛表指針_vfptr共享到的虛表。

    2.1單繼承中的虛函數表

  • 單繼承中未重寫虛函數: 會繼承基類的虛表, 如果派生類中新增了虛函數, 則會加繼承的虛表后面。
  • 單繼承中重寫虛函數: 繼承的虛表中被重寫的虛函數地址會在繼承虛表時被修改為派生類函數的地址。(注意: 此時基類的虛表并沒有被修改, 修改的是派生類自己的虛表)
  • 所以, 重寫實際上就是在繼承基類虛表時, 把基類的虛函數地址修改為派生類虛函數的地址。

    #include<iostream> using namespace std;class Base {int m_a; public:virtual void func() {cout << "類A的func" << endl;}virtual int func1() {cout << "類A的func1" << endl;return 0;} }; class Derive :public Base { public:virtual void func() {cout << "類B的func" << endl;}virtual void func2() {cout << "類B的func2" << endl;} }; int main() {Base a1;Base a2;Derive b;system("pause");return 0; }

    基類對象a1,a2中,虛表中的地址相同(虛函數func()和func1()的地址),是因為虛表為類的所有對象共享,是通過給每個對象一個虛表指針_vfptr共享到的虛表。
    派生類對象b,繼承了基類的虛表,虛函數指針_vptr卻和a1,a2的不同,這是因為編譯器新分配了一片空間來拷貝基類的虛表。派生類中重寫了虛函數func(),由于被重寫的虛函數地址會在繼承虛表時被修改為派生類函數的地址。所以派生類的虛表中func()的地址被改變了。

    我們還發現,派生類中的虛函數func2()卻沒有出現在派生類中的虛表中。按理來說, 如果派生類中新增了虛函數, 則會加繼承的虛表后面。其實這個虛函數地址是存在的,我們可以發現箭頭所指的虛函數表vftable[4],其中應該有四個元素,除去虛表中多出的一個空指針,還有另外三個func(),func1(),func2(),只不過這里沒有顯示func2()。我們可以通過調用監視窗口來查看func2()。

    2.2多繼承中的虛函數表

  • 多繼承中不重寫虛函數: 繼承的多個基類中有多張虛表, 派生類會全部拷貝下來, 成為派生類的多張虛表, 如果派生類有新的虛函數, 會加在派生類拷貝的第一張虛表的后面(拷貝的第一張虛表是繼承的第一個有虛函數或虛表的基類的)
  • 多繼承中重寫虛函數 : 規則與不重寫虛函數相同, 但需要注意的是, 如果多個基類中含有相同的虛函數, 例如func(),當派生類重寫func()這個虛函數后, 所有含有這個函數的基類虛表都會被重寫 (改的是派生類自己拷貝的基類虛表, 并不是基類自己的虛表)
  • #include<iostream> using namespace std; class Base {int m_a; public:virtual void funcA() {cout << "基類Base的funcA()" << endl;}virtual void func() {cout << "基類Base的func()" << endl;} }; class Base2 { public:virtual void funcB() {cout << "基類Base2的funcB()" << endl;}virtual void func() {cout << "基類Base2的func()" << endl;} }; class Derive :public Base, public Base2 { public:virtual void func() {cout << "派生類重寫的func()" << endl;}virtual void funcC() {cout << "派生類中新增的虛函數funcC()" << endl;} }; int main() {Derive d;Base2 c;Base a;system("pause");return 0; }

    派生類繼承了兩張虛表。派生類中重寫了func()函數,派生類自己拷貝基類虛表中含func()的地址都被改變了。

    第一張虛表vftable[4],說明其中含有四個元素,除了funcA()、func()、多出來的一個空指針外,還有派生類中新的虛函數funcC()。調用監視窗口可以看到,派生類中新增的虛函數funcC()被加在了派生類拷貝基類的第一張虛表的后面。

    3.多態的原理

    多態的構成條件:

  • 通過基類對象的指針或者引用調用虛函數
  • 基類中必須包含虛函數,并且派生類中一定要對基類中的虛函數進行重寫。
  • 原理: 利用虛函數可以重寫的特性, 當一個有虛函數的基類有多個派生類時, 通過各個派生類對基類虛函數的不同重寫, 實現指向派生類對象的基類指針或基類引用調用同一個虛函數, 去實現不同功能的特性。抽象來說就是, 為了完成某個行為, 不同的對象去完成時會產生多種不同的狀態。

    4.總結

  • 當一個類中有虛函數時, 在編譯期間,就會為這個類分配一片連續的內存 (虛表vftable), 來存放虛函數的地址。
  • 對象中存放的是虛函數指針_vfptr,并非虛表。_vptr是虛表的首地址,指向虛表。
  • 虛表中存放的是虛函數地址,不是虛函數。虛函數和普通函數一樣存放在代碼段。
  • 虛表是在編譯階段生成的,一般存放在代碼段中。
  • 虛表本質上是一個在編譯時就已經確定好的void* 類型的指針數組 。
  • 派生類的虛表生成:
    單繼承:
    ①先將基類中的虛表內容拷貝一份到派生類虛表中。
    ②如果派生類重寫了基類中某個虛函數,派生類中的虛函數地址替換虛表中基類的虛函數地址。
    ③派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后。
    多繼承:
    ①繼承的多個基類中有多張虛表, 派生類會全部拷貝下來, 成為派生類的多張虛表 。
    ②如果派生類重寫了基類中的某個虛函數,所有含有這個函數的基類虛表都會被重寫 (改的是派生類自己拷貝的基類虛表, 并不是基類自己的虛表)。
    ③派生類自己新增加的虛函數加在派生類拷貝的第一張虛表后
  • 總結

    以上是生活随笔為你收集整理的C++多态的原理(虚函数指针和虚函数表)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。