c++ 虛函數,虛表相關總結

面向對象,從單一的類開始說起。

class A
{
private:
    int m_a;
    int m_b;
}; 

這個類中有兩個成員變量,都是int類型,所以這個類在內存中占用多大的內存空間呢?

sizeof(A), 8個字節,一個int占用四個字節。下圖驗證:

這兩個數據在內存中是怎樣排列的呢?

原來是這樣,我們根據debug出來的地址畫出a對象在內存的結構圖

如果 class A 中包含成員函數呢? A 的大小又是多少?

class A
{
public:
    void func1() {}    
private:
    int m_a;
    int m_b;
}; 

直接告訴你答案,類的成員函數多大? 沒人能回答你,並且不是本文的重點,類的成員函數是放在代碼區的,不算在類的大小內。

類的對象共享這一段代碼,試想,如果每一個對象都有一段代碼,光是存儲這些代碼得占用多少空間?所以同一個類的對象共用一段代碼。

共用同一段代碼怎麼區分不同的對象呢?

實際上,你在調用成員函數時,a.func1() 會被編譯器翻譯為 A::func1(&a),也就是A* const this, this 就是 a 對象的地址。

所以根據this指針就能找到對應的數據,通過這同一段代碼來處理不同的數據。

接下來我們討論一下繼承,子類繼承父類,將會繼承父類的數據,以及父類函數的調用權。

以下的測試可以驗證這個情況。

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中的數據繼承過來,再有一份自己的數據。

每個包含虛函數的類都有一個虛表,虛表是屬於類的,而不是屬於某個具體的對象,一個類隻需要一個虛表即可。同一個類的所有對象都使用同一個虛表。

為瞭指定對象的虛表,對象內部包含指向一個虛表的指針,來指向自己所使用的虛表。為瞭讓每個包含虛表的類的對象都擁有一個虛表指針,編譯器在類中添加瞭一個指針,*__vptr,用來指向虛表。這樣,當類的對象在創建時便擁有瞭這個指針,且這個指針的值會自動被設置為指向類的虛表。

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個字節。

a的內存結構如下:

虛表是一個函數指針數組,數組裡存放的都是函數指針,指向虛函數所在的位置。

對象調用虛函數時,會根據虛指針找到虛表的位置,再根據虛函數聲明的順序找到虛函數在數組的哪個位置,找到虛函數的地址,從而調用虛函數。

調用普通函數則不像這樣,普通函數在編譯階段就指定好瞭函數位置,直接調用即可。

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中又定義瞭一個虛函數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, 無參數的函數指針 avfunc1, 變量名代表我們想要取A類的vfunc1這個虛函數。

右半部分的第一部分,(void(*)()) 代表我們最後要轉換成對應上述類型的指針,右邊需要給一個地址。

我們看 (*int(*)&a), 把a的地址強轉成int*, 再解引用得到 虛指針的地址。

*(int*) (*(int*)&a) 再強轉解引用得到虛表的地址,最後強轉成函數指針。

同理得到 bvfunc1, bvfunc2, +4是因為一個指針占4個字節,+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;
}; 

子類重寫父類的虛函數,需要函數簽名保持一致,該種情況在內存中的結構為:

多態

父類指針指向子類對象的情況下,如果指針調用的是虛函數,則編譯器會將會從虛指針所指的虛函數表中找到對應的地址執行相應的函數。

子類很多的話,每個子類都覆蓋瞭對應的虛函數,則通過虛表找到的虛函數執行後不就執行瞭不同的代碼嘛,表現出多態瞭嘛。

我們把經過虛表調用虛函數的過程稱為動態綁定,其表現出來的現象稱為運行時多態。動態綁定區別於傳統的函數調用,傳統的函數調用我們稱之為靜態綁定,即函數的調用在編譯階段就可以確定下來瞭。

那麼,什麼時候會執行函數的動態綁定?這需要符合以下三個條件。

  • 通過指針來調用函數
  • 指針 upcast 向上轉型(繼承類向基類的轉換稱為 upcast)
  • 調用的是虛函數

為什麼父類指針可以指向子類?

子類繼承自父類,子類也屬於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++ 虛函數,虛表相關總結的詳細內容,更多關於c++ 虛函數,虛表的資料請關註WalkonNet其它相關文章!

推薦閱讀: