C++ 虛函數及虛函數表詳解

多態”的關鍵在於通過基類指針或引用調用一個虛函數時,編譯時不確定到底調用的是基類還是派生類的函數,運行時才確定。

#include <iostream>
using namespace std;
class A
{
public:
    int i;
    virtual void func() {}
    virtual void func2() {}
};
class B : public A
{
    int j;
    void func() {}
};
int main()
{
    cout << sizeof(A) << ", " << sizeof(B);  //輸出 8,12
    return 0;
}

 在 32 位編譯模式下,程序的運行結果是:

8, 12

如果將程序中的 virtual 關鍵字去掉,輸出結果變為:

4, 8

A * p = new B() 實現多態

對比發現,有瞭虛函數以後,對象所占用的存儲空間比沒有虛函數時多瞭 4 個字節。實際上,任何有虛函數的類及其派生類的對象都包含這多出來的 4 個字節,這 4 個字節就是實現多態的關鍵——它位於對象存儲空間的最前端,其中存放的是虛函數表的地址。

每一個有虛函數的類(或有虛函數的類的派生類)都有一個虛函數表,該類的任何對象中都放著該虛函數表的指針(可以認為這是由編譯器自動添加到構造函數中的指令完成的)。 

沒有覆蓋時的子類,可以看到子類的虛函數表的前面是基類離得虛函數

有覆蓋就是

子類對象地址為什麼能賦值給父類對象指針?

因為,子類對象地址賦值給父類對象指針,父類對象指針就指向瞭子類的對象空間,父類操作子類的范圍是有限制的,隻能操作到子類中父類的范圍。

基類和子類各有自己的虛函數表vtbl;不管是基類還是子類實例都會在其內存的開頭自動創對象即虛函數表指針vptr, 用來訪問所在類的虛函數表

想要實現多態,需要動態綁定,需要父類的指針或父類的引用

父類方法為虛方法,子類覆蓋父類的虛方法,才能達到多態

子類中父類沒有的方法,父類指針也無法訪問到,父類指針隻能訪問到父類自己有的范圍

子類要覆蓋父類的方法,就是要函數名參數都必須一樣才叫覆蓋

再看一個例子

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;
};

子類繼承父類,子類中有父類的同名方法,訪問的是子類的方法,子類會隱藏父類所有的同名方法,即使父類有一個同名的參數不同的方法也是如此。 

多重繼承(無虛函數覆蓋)

下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關系。註意:子類並沒有覆蓋父類的函數。 

對於子類實例中的虛函數表,是下面這個樣子:

我們可以看到:

1)  每個父類都有自己的虛表。

2)  子類的成員函數被放到瞭第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的) 

這樣做就是為瞭解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。 

多重繼承(有虛函數覆蓋) 

下面我們再來看看,如果發生虛函數覆蓋的情況。 

下圖中,我們在子類中覆蓋瞭父類的f()函數。 

下面是對於子類實例中的虛函數表的圖: 

我們可以看見,三個父類虛函數表中的f()的位置被替換成瞭子類的函數指針。這樣,我們就可以任一靜態類型的父類來指向子類,並調用子類的f()瞭。

任何妄圖使用父類指針想調用子類中的未覆蓋父類的成員函數的行為都會被編譯器視為非法,

總結

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: