C++虛函數表的分析

轉載出處:

https://www.cnblogs.com/hushpa/p/5707475.html

https://blog.csdn.net/lihao21/article/details/50688337


先看代碼:

複製代碼
#include <iostream>

using namespace std;

class Base {
public:
    virtual void f() {cout<<"base::f"<<endl;}
    virtual void g() {cout<<"base::g"<<endl;}
    virtual void h() {cout<<"base::h"<<endl;}
};

class Derive : public Base{
public:
    void g() {cout<<"derive::g"<<endl;}
};

//可以稍後再看
int main () {
    cout<<"size of Base: "<<sizeof(Base)<<endl;

    typedef void(*Func)(void);
    Base b;
    Base *d = new Derive();

    long* pvptr = (long*)d;
    long* vptr = (long*)*pvptr;
    Func f = (Func)vptr[0];
    Func g = (Func)vptr[1];
    Func h = (Func)vptr[2];

    f();
    g();
    h();

    return 0;
}
複製代碼

 

    都知道C++中的多態是用虛函數實現的: 子類覆蓋父類的虛函數, 然後聲明一個指向子類對象的父類指針, 如Base *b = new Derive();
當調用b->f()時, 調用的是子類的Derive::f()。 
這種機制內部由虛函數表實現,下面對虛函數表結構進行分析,並且用GDB驗證。
 
1. 基礎知識:
(1) 32位os 指針長度爲4字節, 64位os 指針長度爲8字節, 下面的分析環境爲64位 linux & g++ 4.8.4.
(2) new一個對象時, 只爲類中成員變量分配空間, 對象之間共享成員函數。

2. _vptr
    運行下上面的代碼發現sizeof(Base) = 8, 說明編譯器在類中自動添加了一個8字節的成員變量, 這個變量就是_vptr, 指向虛函數表的指針。
_vptr有些文章裏說gcc是把它放在對象內存的末尾,VC是放在開始, 我編譯是用的g++,驗證了下是放在開始的:

驗證代碼:取對象a的地址與a第一個成員變量n的地址比較,如果不等,說明對象地址開始放的是_vptr. 也可以用gdb直接print a 會發現_vptr在開始

複製代碼
class A
{
public:
      int n;
      virtual void Foo(void){}
};

int main()
{
     A a;
     char *p1 = reinterpret_cast<char*>(&a);
     char *p2 = reinterpret_cast<char*>(&a.n);
     if(p1 == p2)
     {
        cout<<"vPtr is in the end of class instance!"<<endl;
     }else
     {
        cout<<"vPtr is in the head of class instance!"<<endl;
     }
     return 1;
}
複製代碼

(3) 虛函數表
    包含虛函數的類纔會有虛函數表, 同屬於一個類的對象共享虛函數表, 但是有各自的_vptr.
    虛函數表實質是一個指針數組,裏面存的是虛函數的函數指針。

Base中虛函數表結構:

Derive中虛函數表結構:

 

(4)驗證
運行上面代碼結果:
    size of Base: 8
    base::f
    derive::g
    base::h

說明Derive的虛函數表結構跟上面分析的是一致的:
    d對象的首地址就是vptr指針的地址-pvptr,
    取pvptr的值就是vptr-虛函數表的地址
    取vptr中[0][1][2]的值就是這三個函數的地址
    通過函數地址就直接可以運行三個虛函數了。
    函數表中Base::g()函數指針被Derive中的Derive::g()函數指針覆蓋, 所以執行的時候是調用的Derive::g()

 

(5)多繼承

 

 

複製代碼
附 GDB調試:

(1) #生成帶有調試信息的可執行文件
g++ test.cpp -g -o test    

(2) #載入test
gdb test

(3) #列出Base類代碼
(gdb) list Base
1       #include <iostream>
2
3       using namespace std;
4
5       class Base {
6       public:
7           virtual void f() {cout<<"base::f"<<endl;}
8           virtual void g() {cout<<"base::g"<<endl;}
9           virtual void h() {cout<<"base::h"<<endl;}
10      };

4) #查看Base函數地址
(gdb) info line 7 
Line 7 of "test.cpp" starts at address 0x400ac8 <Base::f()> and ends at 0x400ad4 <Base::f()+12>.
(gdb) info line 8
Line 8 of "test.cpp" starts at address 0x400af2 <Base::g()> and ends at 0x400afe <Base::g()+12>.
(gdb) info line 9
Line 9 of "test.cpp" starts at address 0x400b1c <Base::h()> and ends at 0x400b28 <Base::h()+12>.

5)#列出Derive代碼
(gdb) list Derive 
7           virtual void f() {cout<<"base::f"<<endl;}
8           virtual void g() {cout<<"base::g"<<endl;}
9           virtual void h() {cout<<"base::h"<<endl;}
10      };
11
12      class Derive : public Base{
13      public:
14          void g() {cout<<"derive::g"<<endl;}
15      };

6)#查看Derive函數地址
(gdb) info line 14
Line 14 of "test.cpp" starts at address 0x400b46 <Derive::g()> and ends at 0x400b52 <Derive::g()+12>.

7)#start執行程序,n單步執行
(gdb) start
Temporary breakpoint 1, main () at test.cpp:19
19          cout<<"size of Base: "<<sizeof(Base)<<endl;
(gdb) n
size of Base: 8
22          Base b;
(gdb)
23          Base *d = new Derive();
(gdb)
25          long* pvptr = (long*)d;
(gdb)
26          long* vptr = (long*)*pvptr;
(gdb)
27          Func f = (Func)vptr[0];
(gdb)
28          Func g = (Func)vptr[1];
(gdb)
29          Func h = (Func)vptr[2];
(gdb)
31          f();
(gdb)

8) #print d對象, 0x400c90爲成員變量_vptr的值,也就是函數表的地址
(gdb) p *d 
$4 = {_vptr.Base = 0x400c90 <vtable for Derive+16>}
(gdb) p vptr
$6 = (long *) 0x400c90 <vtable for Derive+16>9) #查看函數表值,與之前查看函數地址一致
(gdb) p (long*)vptr[0]
$9 = (long *) 0x400ac8 <Base::f()>
(gdb) p (long*)vptr[1]
$10 = (long *) 0x400b46 <Derive::g()>
(gdb) p (long*)vptr[2]
$11 = (long *) 0x400b1c <Base::h()>
複製代碼

 

另vptr. vtable內存位置, refer  http://www.tuicool.com/articles/iUB3Ebi



補充:

虛表是屬於類的,而不是屬於某個具體的對象,一個類只需要一個虛表即可。同一個類的所有對象都使用同一個虛表。 
爲了指定對象的虛表,對象內部包含一個虛表的指針,來指向自己所使用的虛表。爲了讓每個包含虛表的類的對象都擁有一個虛表指針,編譯器在類中添加了一個指針,*__vptr,用來指向虛表。這樣,當類的對象在創建時便擁有了這個指針,且這個指針的值會自動被設置爲指向類的虛表。

這裏寫圖片描述
圖2:對象與它的虛表

上面指出,一個繼承類的基類如果包含虛函數,那個這個繼承類也有擁有自己的虛表,故這個繼承類的對象也包含一個虛表指針,用來指向它的虛表。

四、動態綁定

說到這裏,大家一定會好奇C++是如何利用虛表和虛表指針來實現動態綁定的。我們先看下面的代碼。

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};

class B : public A {
public:
    virtual void vfunc1();
    void func1();
private:
    int m_data3;
};

class C: public B {
public:
    virtual void vfunc2();
    void func2();
private:
    int m_data1, m_data4;
};

類A是基類,類B繼承類A,類C又繼承類B。類A,類B,類C,其對象模型如下圖3所示。

這裏寫圖片描述
圖3:類A,類B,類C的對象模型

由於這三個類都有虛函數,故編譯器爲每個類都創建了一個虛表,即類A的虛表(A vtbl),類B的虛表(B vtbl),類C的虛表(C vtbl)。類A,類B,類C的對象都擁有一個虛表指針,*__vptr,用來指向自己所屬類的虛表。 
類A包括兩個虛函數,故A vtbl包含兩個指針,分別指向A::vfunc1()和A::vfunc2()。 
類B繼承於類A,故類B可以調用類A的函數,但由於類B重寫了B::vfunc1()函數,故B vtbl的兩個指針分別指向B::vfunc1()和A::vfunc2()。 
類C繼承於類B,故類C可以調用類B的函數,但由於類C重寫了C::vfunc2()函數,故C vtbl的兩個指針分別指向B::vfunc1()(指向繼承的最近的一個類的函數)和C::vfunc2()。 
雖然圖3看起來有點複雜,但是隻要抓住“對象的虛表指針用來指向自己所屬類的虛表,虛表中的指針會指向其繼承的最近的一個類的虛函數”這個特點,便可以快速將這幾個類的對象模型在自己的腦海中描繪出來。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章