拆一下C++ 的對象模型 (現在來看主要拆的是G++的)

1. 關於類中的成員數據和成員函數:

#include <iostream>

using   namespace       std;

class   ClassA {
public:
        ClassA () {
        }
        ~ClassA () {
        }
        void publicFunc () {
        }
        virtual void vPublicFunc () {
        }
        int publicV1;
        int publicV2;
        static int staticClassA;
private:
        void privateFunc () {
        }
        virtual void vPrivateFunc () {
        }
        int privateV1;
        int privateV2;
};

class   ClassB : public ClassA {
public:
        ClassB () {
        }
        void publicFunc () {
        }
        int publicV;
        static int staticClassB;
private:
        void privateFunc () {
        }
        int privateV;
};

int
main (int argc, char *argv[]) {
        ClassA cA;
        ClassB cB;

        cout << "sizeof ClassA is : " << sizeof (ClassA) << endl;
        cout << "sizeof ClassB is : " << sizeof (ClassB) << endl;

        return 0;
}


結果(環境是MinGW Shell):

成員函數不是存在對象中的,sizeof ClassA的結果來看有點奇怪,應該是4個變量和1個靜態變量——但是這樣的話如何靜態呢。

詳細看看類的內部有什麼:

用gdb 打印一下就能看到爲什麼ClassA 和ClassB 的sizeof 會得到這樣的值了,首先都包涵了一個_vptr.ClassName 的指針,這貨指向了一個虛表。

然後是5個變量,其中靜態變量不在類中。

同樣,很容易看出來,ClassB 中繼承自ClassA,其中先存放基類的數據成員,然後自己的數據成員再按照一定順序存放。

關於順序,打印下cA 中變量的地址如下:

和p cA 指令得到的一個樣,cA 的數據結構是:

privateV2 high addr
privateV1  
publicV2  
publicV1  
vptr low addr

其中成員函數在類外,一般編譯器這樣實現成員函數:

如果有類ClassA,其中有公有函數pFunc (void)。

編譯時該函數會在類外定義,原本的調用語句

cA.pFunc ();

會變爲_cA_pFunc (&cA) // 這裏不同的編譯器實現不一樣,但是一定會傳入一個this 指針

雖然成員函數都在類外,然而普通函數和虛函數還不一樣(記得虛表指針麼)。

至於虛函數,請看2。

2. 關於類開始的虛表指針和類的私有變量:

#include <iostream>

using   namespace       std;

class   ClassA {
public:
        ClassA (int pV1, int pV2) : pV1 (pV1), pV2 (pV2) {
        }
        virtual void vFunc () {
                cout << "虛函數vFunc 被執行" << endl;
        }
        virtual void vFunc2 () {
                cout << "虛函數vFunc2 被執行" << endl;
        }
        void printV () {
                cout << "pV1 = " << pV1 << " | pV2 = " << pV2 << endl;
        }
private:
        int pV1, pV2;
};

int
main (int argc, char *argv[]) {
        ClassA cA (1, 2);

        // 看看私有變量
        cA.printV ();
        // C++ 類的私有類型果真是”私有“的麼?
        ((int *)&cA)[1] = 2, ((int *)&cA)[2] = 1;
        // 再看看私有變量
        cA.printV ();

        // 傳言第一個4byte 是一個指向虛表的指針,虛表顧名思義就是虛擬函數表
        // 把指向的虛表每4byte 作爲函數調用,看看會得到什麼
        for (int i = 0; ((int *)*((int *)&cA))[i]; ++i) {
                (*((void (*) ())((int *)*((int *)&cA))[i])) ();
        }

        getchar ();
        return 0;
}


結果(環境是wxDev C++ 7.4.1.13):

注意順序哦親。

現在我們可以瞭解虛表是什麼東東了,大約是下面的樣子

                 類ClassA                             虛表                        虛函數

(high addr)      pV2                                  NULL ①
                 pV1                                  *pFunc2  -----------------> vFunc2
(low addr)       *vptr  --------------------------->  *pFunc1  -----------------> vFunc:

在G++ 中,這個值爲NULL 代表了只有一個虛表,爲1代表還有下一個虛表

既然我們瞭解虛表了,我們能對虛表指針做些手腳麼?

#include <iostream>

using   namespace       std;

class   ClassA {
public:
        ClassA () {
        }
        virtual void vFunc () {
                cout << "我是ClassA 中的虛函數" << endl;
        }
};

class   ClassB  {
public:
        ClassB () {
        }
        virtual void vFunc () {
                cout << "我是ClassB 中的虛函數" << endl;
        }
} ;

int
main (int argc, char *argv[]) {
        ClassA *cA = new ClassA;
        ClassB *cB = new ClassB;

        cA->vFunc ();
        *((int *)&cA) = *((int *)&cB);
        cA->vFunc ();   // 現在調用的還是原先的虛函數麼?

        delete cA;
        delete cB;

        getchar ();
        return 0;
}


結果(環境wxDev C++ 7.4.1.13):

虛表指針被替換了,指向了ClassB 的虛表。

上面只是測試了public 的虛函數,那麼private 的虛函數呢?

我們把2 中第一段代碼稍稍修改:

#include <iostream>

using   namespace       std;

class   ClassA {
public:
        ClassA (int pV1, int pV2) : pV1 (pV1), pV2 (pV2) {
        }
private:
        virtual void vFunc () {
                cout << "虛函數vFunc 被執行" << endl;
        }
        virtual void vFunc2 () {
                cout << "虛函數vFunc2 被執行" << endl;
        }
        void printV () {
                cout << "pV1 = " << pV1 << " | pV2 = " << pV2 << endl;
        }
private:
        int pV1, pV2;
};

int
main (int argc, char *argv[]) {
        ClassA cA (1, 2);

        for (int i = 0; ((int *)*((int *)&cA))[i]; ++i) {
                (*((void (*) ())((int *)*((int *)&cA))[i])) ();
        }

        getchar ();
        return 0;
}

結果(環境wxDev C++ 7.4.1.13):

既然私有虛函數也在虛表之內,那麼虛析構函數呢?

我們修改下上面的代碼:


#include <iostream>

using   namespace       std;

class   ClassA {
public:
        ClassA () {
        }
        virtual ~ClassA () {
                cout << "虛析構函數被執行" << endl;
        }
        virtual void vFunc () {
                cout << "虛函數vFunc 被執行" << endl;
        }
};

int
main (int argc, char *argv[]) {
        ClassA cA ;

        // 把指向的虛表每4byte 作爲函數調用,看看會得到什麼
        for (int i = 0; ((int *)*((int *)&cA))[i]; ++i) {
                (*((void (*) ())((int *)*((int *)&cA))[i])) ();
        }

        getchar ();
        return 0;
}



結果(環境wxDev C++ 7.4.1.13):

至於爲什麼虛析構函數被調用了2次,我會試圖弄清楚。②

虛函數無論訪問權限如何,都存在於虛表之中,所以你可以通過虛表來調用私有虛函數。

C++ 的類並不能保證數據的封裝性,就像上面調用私有虛函數或者直接修改私有變量一樣。

我開了個帖子,得到一個比較靠譜的回答如下:

第一個“虛析構函數”是vtbl第一個項目的函數調用產生的,這就是多出來的那一個。據《深度探索C++對象模型》一書介紹,vtbl第一個項目通常是一個type_info對象的指針,但g++的類對象內存佈局是否真這樣,偶沒具體研究過g++的內存佈局,也沒找到其它介紹g++內存佈局的資料,無從考究這個多出來的“虛析構函數”是如何產生的。

3. 如果虛函數被繼承……

2中我們瞭解了虛表,但是如果再加上繼承,虛表會怎麼變化呢?

單繼承:

#include <iostream>

using   namespace       std;

// 宏的參數*必須*是對象的指針
#define CALL_V_FUNCS(P_OBJECT)\
        cout << "下面是"<< #P_OBJECT\
                << " 對象中虛表中函數的調用" << endl;\
        for (int i = 0; ((int *)*((int *)(P_OBJECT)))[i]; ++i) {\
                (*((void (*) ())((int *)*((int *)(P_OBJECT)))[i])) ();\
        }

class   ClassA {
public:
        ClassA () {
        }
        virtual void vFunc () {
                cout << "我是ClassA 中的虛函數vFunc" << endl;
        }
        virtual void vFunc1 () {
                cout << "我是ClassA 中的虛函數vFunc1" << endl;
        }
};

class   ClassB : public ClassA {
public:
        ClassB () {
        }
        virtual void vFunc2 () {
                cout << "我是ClassB 中的虛函數vFunc2" << endl;
        }
        virtual void vFunc3 () {
                cout << "我是ClassB 中的虛函數vFunc3" << endl;
        }
} ;

int
main (int argc, char *argv[]) {
        ClassA *cA = new ClassA;
        ClassB *cB = new ClassB;

        CALL_V_FUNCS (cA);
        CALL_V_FUNCS (cB);

        delete cA;
        delete cB;

        getchar ();
        return 0;
}


結果(環境wxDev C++ 7.4.1.13)

可以看出,ClassB 中的虛表還是一個(我的上面代碼只能讀單虛表的情況),ClassA 的虛函數指針在虛表前2個位置,而後是ClassB 自身的2個虛函數指針。

如果再加上虛函數的覆蓋呢?‘

這段代碼幾乎和上面的一段一模一樣,只不過ClassB 中的虛函數覆蓋了ClassA 中的一個:

#include <iostream>

using   namespace       std;

// 宏的參數*必須*是對象的指針
#define CALL_V_FUNCS(P_OBJECT)\
        cout << "下面是"<< #P_OBJECT\
                << " 對象中虛表中函數的調用" << endl;\
        for (int i = 0; ((int *)*((int *)(P_OBJECT)))[i]; ++i) {\
                (*((void (*) ())((int *)*((int *)(P_OBJECT)))[i])) ();\
        }

class   ClassA {
public:
        ClassA () {
        }
        virtual void vFunc () {
                cout << "我是ClassA 中的虛函數vFunc" << endl;
        }
        virtual void vFunc1 () {
                cout << "我是ClassA 中的虛函數vFunc1" << endl;
        }
};

class   ClassB : public ClassA {
public:
        ClassB () {
        }
        virtual void vFunc () {
                cout << "我是ClassB 中的虛函數vFunc" << endl;
        }
        virtual void vFunc3 () {
                cout << "我是ClassB 中的虛函數vFunc3" << endl;
        }
} ;

int
main (int argc, char *argv[]) {
        ClassA *cA = new ClassA;
        ClassB *cB = new ClassB;

        CALL_V_FUNCS (cA);
        CALL_V_FUNCS (cB);

        delete cA;
        delete cB;

        getchar ();
        return 0;
}


結果(環境wxDev C++ 7.4.1.13)

可以看出,ClassB 的虛函數vFunc 的指針覆蓋了ClassA 中vFunc 的指針在虛表中的位置,從而實現了代碼的重用。

如果是多繼承呢:

關於虛表,其實有些地方都是要看編譯器具體實現的。多繼承的虛表太依賴於編譯器實現(傳言VS和GCC就不一樣),因此無法寫出統一的代碼。

然而多繼承能夠確定的就是:

A. 子類會繼承父類的虛表,假設有3個有虛表的父類,子類就會有3個虛表。

B. 子類的虛函數被放在第一個父類的虛表中,規則和上面演示的單繼承一致。

C. 如果多繼承的過程中發生了虛函數的覆蓋,那麼規則也和上文單繼承虛函數的覆蓋規則的一致。

發佈了41 篇原創文章 · 獲贊 13 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章