天天看點

【C++】C++虛函數表詳細分析(下)

00. 目錄

00. 目錄01. 前言02. 一般繼承(無虛函數覆寫)03. 一般繼承(有虛函數覆寫)04. 多重繼承(無虛函數覆寫)05. 多重繼承(有虛函數覆寫)06. 安全性

01. 前言

上一篇部落格我們詳細講解了虛函數表,下面我将分别說明“無覆寫”和“有覆寫”時的虛函數表的樣子。沒有覆寫父類的虛函數是毫無意義的。我之是以要講述沒有覆寫的情況,主要目的是為了給一個對比。在比較之下,我們可以更加清楚地知道其内部的具體實作。

02. 一般繼承(無虛函數覆寫)

假設有如下所示的一個繼承關系:

請注意,在這個繼承關系中,子類沒有重載任何父類的函數。那麼,在派生類的執行個體中,其虛函數表如下所示:

【C++】C++虛函數表詳細分析(下)

對于執行個體:Derive d; 的虛函數表如下:

【C++】C++虛函數表詳細分析(下)

我們可以看到下面幾點:

1)虛函數按照其聲明順序放于表中。

2)父類的虛函數在子類的虛函數前面。

​測試程式如下​

class Base{
    public: 
        typedef void (Base::*Fun)();
        virtual void f(){
            Fun q=&Base::f;
            cout<<"Base::f -> "<<(int)*(void**)&q<<endl;
        }   
        virtual void g(){
            Fun q=&Base::g;
            cout<<"Base::g -> "<<(int)*(void**)&q<<endl;
        }   
        virtual void h(){
            Fun q=&Base::h;
            cout<<"Base::h -> "<<(int)*(void**)&q<<endl;
        }
};  
class Derive:public Base{
    public:
        typedef void (Derive::*Fun)();
        virtual void f1(){
            Fun q=&Derive::f1;
            cout<<"Derive::f1 -> "<<(int)*(void**)&q<<endl;
        }
        virtual void g1(){
            Fun q=&Derive::g1;
            cout<<"Derive::g1 -> "<<(int)*(void**)&q<<endl;
        }
        virtual void h1(){
            Fun q=&Derive::h1;
            cout<<"Derive::h1 -> "<<(int)*(void**)&q<<endl;
        }
};      
 int test2(){
     typedef void (*Fun)();
     Base b;
     Fun pFun=NULL;
     int *add;
     cout<<"虛函數表位址:"<<(int*)&b<<endl;
     add=(int*)*(int*)&b;
     cout<<"虛函數表-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
     cout<<"虛函數表-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
     cout<<"虛函數表-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
     for(int i=0;i<3;i++)
     {
     pFun=(Fun)*((int*)*(int*)&b+i);
     pFun();
     }
     cout<<"=================================="<<endl;
     Derive d;
     cout<<"虛函數表位址:"<<(int*)&d<<endl;
     add=(int*)*(int*)&d;
     cout<<"虛函數表-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
     cout<<"虛函數表-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
     cout<<"虛函數表-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
     cout<<"虛函數表-第4個函數位址:"<<add+3<<" -> "<<(int*)*(add+3)<<endl;
     cout<<"虛函數表-第5個函數位址:"<<add+4<<" -> "<<(int*)*(add+4)<<endl;
     cout<<"虛函數表-第6個函數位址:"<<add+5<<" -> "<<(int*)*(add+5)<<endl;     
     for(int i=0;i<6;i++)
     {
     pFun=(Fun)*((int*)*(int*)&d+i);
     pFun();
     }
     return 0;
 }      

​測試結果​

從下圖可以看到,派生類的前三個函數就是從基類中繼承來的,函數的位址是不一樣的,但是所指向的記憶體中函數的位址是一緻的。

【C++】C++虛函數表詳細分析(下)

03. 一般繼承(有虛函數覆寫)

覆寫父類的虛函數是很顯然的事情,不然,虛函數就變得毫無意義。下面,我們來看一下,如果子類中有虛函數重載了父類的虛函數,會是一個什麼樣子?假設,我們有下面這樣的一個繼承關系。

【C++】C++虛函數表詳細分析(下)

為了讓大家看到被繼承過後的效果,在這個類的設計中,我隻覆寫了父類的一個函數:f()。那麼,對于派生類的執行個體,其虛函數表會是下面的一個樣子

【C++】C++虛函數表詳細分析(下)

我們從表中可以看到下面幾點,

1)覆寫的f()函數被放到了虛表中原來父類虛函數的位置。

2)沒有被覆寫的函數依舊。

​測試程式​

class Derive2:public Base{
    public: 
        typedef void (Derive::*Fun)();
        virtual void f(){
            Fun q=&Derive::f;
            cout<<"Derive::f -> "<<(int)*(void**)&q<<endl;
        }
        virtual void g1(){
            Fun q=&Derive::g1;
            cout<<"Derive::g1 -> "<<(int)*(void**)&q<<endl;
        }
        virtual void h1(){
            Fun q=&Derive::h1;
            cout<<"Derive::h1 -> "<<(int)*(void**)&q<<endl;
        }
};      
int test3(){
    typedef void (*Fun)();
    Base b;
    Fun pFun=NULL;
    int *add;
    cout<<"虛函數表位址:"<<(int*)&b<<endl;
    add=(int*)*(int*)&b;                                                        
    cout<<"虛函數表-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    for(int i=0;i<3;i++)
    {
    pFun=(Fun)*((int*)*(int*)&b+i);
    pFun();
    }
    cout<<"=================================="<<endl;
    Derive2 d;
    cout<<"虛函數表位址:"<<(int*)&d<<endl;
    add=(int*)*(int*)&d;
    cout<<"虛函數表-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    cout<<"虛函數表-第4個函數位址:"<<add+3<<" -> "<<(int*)*(add+3)<<endl;
    cout<<"虛函數表-第5個函數位址:"<<add+4<<" -> "<<(int*)*(add+4)<<endl;
    cout<<"虛函數表-第6個函數位址:"<<add+5<<" -> "<<(int*)*(add+5)<<endl;
    for(int i=0;i<5;i++)
    {
    pFun=(Fun)*((int*)*(int*)&d+i);
    pFun();
    }
    return 0;
}      

​執行結果:​

【C++】C++虛函數表詳細分析(下)

從上圖我們可以看到在派生類中的函數f已經覆寫了基類中對應的函數f,并且派生類的虛表的函數位址換成了派生類函數的位址。因為不存在第6個函數,是以最後的位址為0

這樣,我們就可以看到對于下面這樣的程式,

Base *b = new Derive();

b->f();

由b所指的記憶體中的虛函數表的f()的位置已經被Derive::f()函數位址所取代,于是在實際調用發生時,是Derive::f()被調用了。這就實作了多态。

04. 多重繼承(無虛函數覆寫)

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

【C++】C++虛函數表詳細分析(下)

對于子類執行個體中的虛函數表,是下面這個樣子:

【C++】C++虛函數表詳細分析(下)

我們可以看到:

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

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

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

​測試代碼​

#include <iostream>
using namespace std;
class Base1{
    public:
        typedef void (Base1::*Fun)();
        virtual void f(){
            Fun q=&Base1::f;
            cout<<"Base1::f -> "<<(int)*(void**)&q<<endl;
        }
        virtual void g(){
            Fun q=&Base1::g;
            cout<<"Base1::g -> "<<(int)*(void**)&q<<endl;
        }
        virtual void h(){
            Fun q=&Base1::h;
            cout<<"Base1::h -> "<<(int)*(void**)&q<<endl;
        }
};
class Base2{
    public:
        typedef void (Base2::*Fun)();
        virtual void f(){
            Fun q=&Base2::f;
            cout<<"Base2::f -> "<<(int)*(void**)&q<<endl;
        }
        virtual void g(){
            Fun q=&Base2::g;
            cout<<"Base2::g -> "<<(int)*(void**)&q<<endl;
        }
        virtual void h(){
            Fun q=&Base2::h;
            cout<<"Base2::h -> "<<(int)*(void**)&q<<endl;
        }
};
class Base3{
    public:
        typedef void (Base3::*Fun)();
        virtual void f(){
            Fun q=&Base3::f;
            cout<<"Base3::f -> "<<(int)*(void**)&q<<endl;
        }
        virtual void g(){
            Fun q=&Base3::g;
            cout<<"Base3::g -> "<<(int)*(void**)&q<<endl;
        }
        virtual void h(){
            Fun q=&Base3::h;
            cout<<"Base3::h -> "<<(int)*(void**)&q<<endl;
        }
};
class Derive:public Base1,public Base2,public Base3{
    public:
        typedef void (Derive::*Fun)();
        virtual void f1(){
            Fun q=&Derive::f1;
            cout<<"Derive::f1 -> "<<(int)*(void**)&q<<endl;
        }
        virtual void g1(){
            Fun q=&Derive::g1;
            cout<<"Derive::g1 -> "<<(int)*(void**)&q<<endl;
        }
};
int test2(){
    typedef void (*Fun)();
    Fun pFun=NULL;
    int *add;
    Base1 b1;
    add=(int*)*(int*)&b1;
    cout<<"Base1-虛函數表位址:"<<(int*)&b1<<endl;
    cout<<"虛函數表-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    for(int i=0;i<3;i++)
    {
    pFun=(Fun)*((int*)*(int*)&b1+i);
    pFun();
    }
    Base2 b2;
    add=(int*)*(int*)&b2;
    cout<<"Base2-虛函數表位址:"<<(int*)&b2<<endl;
    cout<<"虛函數表-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    for(int i=0;i<3;i++)
    {
    pFun=(Fun)*(add+i);
    pFun();
    }
    Base3 b3;
    add=(int*)*(int*)&b3;
    cout<<"Base3-虛函數表位址:"<<(int*)&b3<<endl;
    cout<<"虛函數表-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    for(int i=0;i<3;i++)
    {
    pFun=(Fun)*(add+i);
    pFun();
    }
    cout<<"=================================="<<endl;
    Derive d;
    add=(int*)*(int*)&d;
    cout<<"Derive-虛函數表1位址:"<<(int*)&d<<endl;
    cout<<"虛函數表1-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表1-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表1-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    cout<<"虛函數表1-第4個函數位址:"<<add+3<<" -> "<<(int*)*(add+3)<<endl;
    cout<<"虛函數表1-第5個函數位址:"<<add+4<<" -> "<<(int*)*(add+4)<<endl;
    for(int i=0;i<5;i++)
    {
    pFun=(Fun)*(add+i);
    pFun();
    }

    add=(int*)*((int*)&d+1);
    cout<<"Derive-虛函數表2位址:"<<(int*)&d+1<<endl;
    cout<<"虛函數表2-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表2-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表2-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    cout<<"虛函數表2-第4個函數位址:"<<add+3<<" -> "<<(int*)*(add+3)<<endl;
    cout<<"虛函數表2-第5個函數位址:"<<add+4<<" -> "<<(int*)*(add+4)<<endl;
    for(int i=0;i<3;i++)
    {
    pFun=(Fun)*(add+i);
    pFun();
    }

    add=(int*)*((int*)&d+2);
    cout<<"Derive-虛函數表3位址:"<<(int*)&d+2<<endl;
    cout<<"虛函數表3-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表3-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表3-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    cout<<"虛函數表3-第4個函數位址:"<<add+3<<" -> "<<(int*)*(add+3)<<endl;
    cout<<"虛函數表3-第5個函數位址:"<<add+4<<" -> "<<(int*)*(add+4)<<endl;
    for(int i=0;i<3;i++)
    {
    pFun=(Fun)*(add+i);
    pFun();
    }
    return 0;
}
int main(){
    test2();
}      

​測試結果:​

【C++】C++虛函數表詳細分析(下)

從上圖可以看到派生類中确實有3個虛表,分别儲存來自3個基類的虛函數。派生類的虛表2和虛表3隻有三個函數,是以圖中對于虛表2和虛表3的第4個和第5個函數指向的位址都是無效的。

05. 多重繼承(有虛函數覆寫)

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

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

【C++】C++虛函數表詳細分析(下)

下面是對于子類執行個體中的虛函數表的圖:

【C++】C++虛函數表詳細分析(下)

​測試程式​

class Derive2:public Base1,public Base2,public Base3{
    public:
        typedef void (Derive2::*Fun)();
        virtual void f(){
            Fun q=&Derive2::f;
            cout<<"Derive2::f -> "<<(int)*(void**)&q<<endl;
        }
        virtual void g1(){
            Fun q=&Derive2::g1;
            cout<<"Derive2::g1 -> "<<(int)*(void**)&q<<endl;
        }
};
int test3(){
    typedef void (*Fun)();
    Fun pFun=NULL;
    int *add;
    Base1 b1;
    add=(int*)*(int*)&b1;
    cout<<"Base1-虛函數表位址:"<<(int*)&b1<<endl;
    cout<<"虛函數表-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    for(int i=0;i<3;i++)
    {
    pFun=(Fun)*((int*)*(int*)&b1+i);
    pFun();
    }
    Base2 b2;
    add=(int*)*(int*)&b2;
    cout<<"Base2-虛函數表位址:"<<(int*)&b2<<endl;
    cout<<"虛函數表-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    for(int i=0;i<3;i++)
    {
    pFun=(Fun)*(add+i);
    pFun();
    }
    Base3 b3;
    add=(int*)*(int*)&b3;
    cout<<"Base3-虛函數表位址:"<<(int*)&b3<<endl;
    cout<<"虛函數表-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    for(int i=0;i<3;i++)
    {
    pFun=(Fun)*(add+i);
    pFun();
    }
    cout<<"=================================="<<endl;
    Derive2 d;
    add=(int*)*(int*)&d;
    cout<<"Derive2-虛函數表1位址:"<<(int*)&d<<endl;
    cout<<"虛函數表1-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表1-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表1-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    cout<<"虛函數表1-第4個函數位址:"<<add+3<<" -> "<<(int*)*(add+3)<<endl;
    cout<<"虛函數表1-第5個函數位址:"<<add+4<<" -> "<<(int*)*(add+4)<<endl;
    for(int i=0;i<4;i++)
    {
    pFun=(Fun)*(add+i);
    pFun();
    }

    add=(int*)*((int*)&d+1);
    cout<<"Derive2-虛函數表2位址:"<<(int*)&d+1<<endl;
    cout<<"虛函數表2-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表2-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表2-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    cout<<"虛函數表2-第4個函數位址:"<<add+3<<" -> "<<(int*)*(add+3)<<endl;
    cout<<"虛函數表2-第5個函數位址:"<<add+4<<" -> "<<(int*)*(add+4)<<endl;
    for(int i=0;i<3;i++)
    {
    pFun=(Fun)*(add+i);
    pFun();
    }

    add=(int*)*((int*)&d+2);
    cout<<"Derive2-虛函數表3位址:"<<(int*)&d+2<<endl;
    cout<<"虛函數表3-第1個函數位址:"<<add<<" -> "<<(int*)(*add)<<endl;
    cout<<"虛函數表3-第2個函數位址:"<<add+1<<" -> "<<(int*)*(add+1)<<endl;
    cout<<"虛函數表3-第3個函數位址:"<<add+2<<" -> "<<(int*)*(add+2)<<endl;
    cout<<"虛函數表3-第4個函數位址:"<<add+3<<" -> "<<(int*)*(add+3)<<endl;
    cout<<"虛函數表3-第5個函數位址:"<<add+4<<" -> "<<(int*)*(add+4)<<endl;
    for(int i=0;i<3;i++)
    {
    pFun=(Fun)*(add+i);
    pFun();
    }
    return 0;
}      

​測試結果如下:​

【C++】C++虛函數表詳細分析(下)

從上圖可以發現,基類和派生類對應方框内的都是第一個函數的位址被替換,另外兩個是不變的。圖中黃色方框内的值是無效的。粉紅色方框内顯示了多态調用的效果。

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

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()

 b1->g(); //Base1::g()
 b2->g(); //Base2::g()
 b3->g(); //Base3::g()      

06. 安全性

每次寫C++的文章,總免不了要批判一下C++。這篇文章也不例外。通過上面的講述,相信我們對虛函數表有一個比較細緻的了解了。水可載舟,亦可覆舟。下面,讓我們來看看我們可以用虛函數表來幹點什麼壞事吧。

​一、通過父類型的指針通路子類自己的虛函數​

我們知道,子類沒有重載父類的虛函數是一件毫無意義的事情。因為多态也是要基于函數重載的。雖然在上面的圖中我們可以看到Base1的虛表中有Derive的虛函數,但我們根本不可能使用下面的語句來調用子類的自有虛函數:

Base1 *b1 = new Derive();

b1->f1(); //編譯出錯

任何妄圖使用父類指針想調用子類中的未覆寫父類的成員函數的行為都會被編譯器視為非法,是以,這樣的程式根本無法編譯通過。但在運作時,我們可以通過指針的方式通路虛函數表來達到違反C++語義的行為。(關于這方面的嘗試,通過閱讀後面附錄的代碼,相信你可以做到這一點)

​二、通路non-public的虛函數​

另外,如果父類的虛函數是private或是protected的,但這些非public的虛函數同樣會存在于虛函數表中,是以,我們同樣可以使用通路虛函數表的方式來通路這些non-public的虛函數,這是很容易做到的。

如:

class Base {
    private:
            virtual void f() { cout << "Base::f" << endl; }

}; 
class Derive : public Base{};

typedef void(*Fun)(void); 
void main() {
    Derive d;
    Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
    pFun();
}      

繼續閱讀