1. 程式人生 > >解析虛擬函式表和虛繼承

解析虛擬函式表和虛繼承

之前大二在學C++的時候一直對虛擬函式和虛繼承有些暈(其實好像就是對virtual這個關鍵字不太熟悉)現在又學習到了一些,對虛擬函式表和虛繼承的機制有了一點更深入的瞭解。
關於虛擬函式以及虛繼承的基礎知識,我自己也總結了一下,點選淺談C++多型C++繼承可檢視,在繼承的總結的時候,我沒有總結關於虛繼承的知識,而且在多型總結也沒有設計到太多的虛擬函式的知識,我是想把這兩塊集中在一起講下,也算是自己對virtual關鍵字有個比較深入的瞭解吧。(本文所有程式碼均在VS2013編譯器win32下測試
另外對於虛擬函式表尤其是後面的菱形繼承等參考了陳皓老師的C++ 物件的記憶體佈局

虛繼承

在談虛繼承前,我們先看這樣一段程式碼:

class B
{
public:
    int _b;
};
class C1 : public B
{
public:
    int _c1;
};
class C2 : public B
{
public:
    int _c2;
};
class D :public C1, public C2
{
public:
    int _d;
};

int main()
{
    D d;  
    //d._b = 10;錯誤,訪問不明確  
    d.C1::_b = 10;//正確  
    d.C2::_b = 10;//正確  
    return 0;
}

為什麼會出現這樣的問題?
我們知道它們的繼承層次如下圖所示:

這裡寫圖片描述

這種看似菱形的多繼承會帶來二義性:也就是說D中_b到底是從C1這條路繼承而來的還是從C2這條路繼承而來的?C++中為了避免這種訪問不明確,從而引入了虛擬繼承的機制。
虛擬繼承是多重繼承中特有的概念。虛擬基類是為解決多重繼承而出現的。如上述類D繼承自類C1、C2,而類C1、C2都繼承自類B,因此在類D中兩次出現類B中的變數。為了節省記憶體空間,可以將C1、C2對B的繼承定義為虛擬繼承,而B就成了虛擬基類。實現程式碼如下:

class B
{
public:
    int _b;
};
class C1 :virtual public B
{
public:
    int _c1;
};
class C2 :virtual
public B { public: int _c2; }; class D :public C1, public C2 { public: int _d; }; int main() { D d; d._d = 4; return 0; }

這樣就可以達到我們的要求了,直接使用d._d訪問到_d。然而虛繼承到底是一種怎麼樣的實現機制?我們不妨在加不加virtual這兩中情況下看下在記憶體中D d這個物件模型是怎麼樣的?
對於普通繼承,我們通過VS2013的記憶體視窗可以看到:

這裡寫圖片描述

先是C1類中的成員,再是C2類中的成員,最後是D類自己的成員,此時sizeof(D) = 20。而一旦加了虛繼承了,變化就比較明顯了,如下圖:

這裡寫圖片描述

最後再看幾道有關的虛繼承的題目:

這裡寫圖片描述

對這四種情況分別求sizeof(a), sizeof(b)。結果是什麼樣的呢?我在VS2013的win32平臺測試結果為:
第一種:4,12
第二種:4,4
第三種:8,16
第四種:8,8
這是為什麼???首先我們看a類,我們知道每個存在虛擬函式的類都要有一個4位元組的指標指向自己的虛擬函式表,再加上如果有資料,根據記憶體對齊機制,四種情況的類a所佔的位元組數應該是沒有什麼問題的。我們再看sizeof(B),我們先看普通繼承,對於普通繼承僅僅是在原來的基礎對虛表指標指向的虛擬函式表進行改寫,類B依舊只有一個虛表指標,再加上如果有資料,根據記憶體對齊機制,所以第二種和第四種情況下,sizeof(B)分別為4和8。然而對於虛擬繼承,會增加了一個偏移指標,而且由於類B中新增了虛擬函式,所以它的一般物件模型為這樣(具體為什麼是這樣本文菱形虛擬繼承會講):

這裡寫圖片描述

根據圖示,在第一種的情況下,由於沒有對應的資料成員,所以大小為12個位元組。在第三種情況下,子類有自己的資料成員,而基類沒有,所以刪去最後一項,大小就是16個位元組了。這樣子對於虛擬繼承應該就沒問題了吧。

虛擬函式

C++中的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父類型別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式。這種技術可以讓父類的指標有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的程式碼來實現可變的演算法。比如:模板技術,RTTI技術,虛擬函式技術,要麼是試圖做到在編譯時決議,要麼試圖做到執行時決議。(參考文章

通過以上這段話,我們知道動態多型(什麼是動態多型,靜態多型以及他們的差別?)主要是通過虛擬函式實現,而虛擬函式(Virtual Function)則是通過一張虛擬函式表(Virtual Table)來實現的。簡稱為V-Table。在這個表中,主是要一個類的虛擬函式的地址表,這張表解決了繼承、覆蓋的問題。這樣,在有虛擬函式的類的例項中這個表被分配在了這個例項的記憶體中,所以,當我們用父類的指標來操作一個子類的時候,這張虛擬函式表就顯得由為重要了,它就像一個地圖一樣,指明瞭實際所應該呼叫的函式。我們通過一些程式碼塊來了解這個概念:

typedef void(*pFun)(void);//函式指標
class Base {
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }

};

void FunTest()
{
    Base b;
    cout << "虛擬函式表地址:" << (int*)(&b) << endl;

    pFun* fun = (pFun*)*(int*)&b;
    while (*fun) {
        (*fun)();
        fun++;
    }
}

int main()
{
    FunTest();
    getchar();
    return 0;
}

在VS2013編譯器win32的測試結果為:

這裡寫圖片描述

我們來看看虛擬函式表的地址0x00DF820裡面存了什麼?

這裡寫圖片描述

通過上圖我們知道,通過Base類例項化的物件b裡面(有3個虛擬函式)有一個指向虛擬函式表的指標,也就是我們上面的0x00DF820,而在這個虛擬函式表中,分別存了3個虛擬函式的地址,我們通過函式指標fun可以訪問到這些函式,因此就得到我們的輸出結果了。通過sizeof(Base)=4也說明此時b物件僅僅存有一個指標,指向虛擬函式表。
所以就得到了我們的物件模型:

這裡寫圖片描述

注意:在上面這個圖中,虛擬函式表的最後多加了一個結點,這是虛擬函式表的結束結點,就像字串的結束符“\0”一樣,其標誌了虛擬函式表的結束,也就是我們這裡虛擬函式表的最後地址存的全是0。注意這個結束標誌的值在不同的編譯器下是不同的

補充一點

如果基類定義了虛同名函式,那麼派生類中的同名函式自動變成了虛擬函式,比如以下程式碼:

class C {
public:
    virtual string toString()
    {
        return "class C";
    }
};

class B : public C {
public:
    /*virtual*/ string toString()
    {
        return "class B";
    }
};

class A : public B {
public:
    /*virtual*/ string toString()
    {
        return "class A";
    }
};

有了這些知識我們再來看看虛擬函式的繼承體系是怎麼樣的:

一般繼承(無虛擬函式覆蓋)

這裡寫圖片描述

注意到:
1. 虛擬函式按照其宣告順序放於表中。
2. 父類的虛擬函式在子類的虛擬函式前面。

一般繼承(有虛擬函式覆蓋)

這裡寫圖片描述

注意到:
1. 覆蓋的f()函式被放到了虛表中原來父類虛擬函式的位置。
2. 沒有被覆蓋的函式依舊。
因此對於程式:
Base *b = new Derive();
b->f();
由b所指的記憶體中的虛擬函式表的f()的位置已經被Derive::f()函式地址所取代,於是在實際呼叫發生時,是Derive::f()被呼叫了。這就實現了多型。

多重繼承(無虛擬函式覆蓋)

這裡寫圖片描述

注意到:
1. 對於例項Derived d的物件,每個父類都有存有一個指標,指向對應的虛擬函式表。
2. 子類的成員函式被放到了第一個父類的表中。(第一個父類是按照宣告順序來判斷的)

多重繼承(有虛擬函式覆蓋)

這裡寫圖片描述

我們可以寫一段程式碼對上圖進行測試:

typedef void(*pFun)(void);

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

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

};

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

};

class Derived : public Base1, public Base2, public Base3 {
public:
    virtual void f() { cout << "Derived::f" << endl; }
    virtual void g1() { cout << "Derived::g1" << endl; }
};

void FunTest()
{
    Derived d;
    cout << sizeof(Derived) << endl;
    //訪問Base1虛擬函式表
    pFun* fun = (pFun*)*((int*)&d + 0);
    while (*fun) {
        (*fun)();
        fun++;
    }
    cout << endl;
    //訪問Base2虛擬函式表
    fun = (pFun*)*((int*)&d + 1);
    while (*fun) {
        (*fun)();
        fun++;
    }
    cout << endl;
    //訪問Base3虛擬函式表
    fun = (pFun*)*((int*)&d + 2);
    while (*fun) {
        (*fun)();
        fun++;
    }
}

int main()
{
    FunTest();
    return 0;
}

最後顯示結果為:

這裡寫圖片描述

虛擬函式表總結

  1. Base虛表:Base類如果有虛擬函式的話,就按照虛函數出現的先後次序來填寫續表
  2. Derived虛表:對於繼承Base類的物件,首先按照Base類的虛表格式複製,如果有重寫(覆蓋)基類的虛擬函式,則在對應的位置修改,不改變次序。如果派生類中新增虛擬函式,則將這虛擬函式填寫到第一個父類虛擬函式後面即可。

根據以上知識,再理解下面的物件模型就不難了:

單一的一般繼承

class Parent {
public:
    int iparent;
    Parent ():iparent (10) {}
    virtual void f() { cout << " Parent::f()" << endl; }
    virtual void g() { cout << " Parent::g()" << endl; }
    virtual void h() { cout << " Parent::h()" << endl; }

};

class Child : public Parent {
public:
    int ichild;
    Child():ichild(100) {}
    virtual void f() { cout << "Child::f()" << endl; }
    virtual void g_child() { cout << "Child::g_child()" << endl; }
    virtual void h_child() { cout << "Child::h_child()" << endl; }
};

class GrandChild : public Child{
public:
    int igrandchild;
    GrandChild():igrandchild(1000) {}
    virtual void f() { cout << "GrandChild::f()" << endl; }
    virtual void g_child() { cout << "GrandChild::g_child()" << endl; }
    virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }
};

對於Grandchildren gc這個物件,它的記憶體模型如下:

這裡寫圖片描述

多重繼承

class Base1 {
public:
    int ibase1;
    Base1():ibase1(10) {}
    virtual void f() { cout << "Base1::f()" << endl; }
    virtual void g() { cout << "Base1::g()" << endl; }
    virtual void h() { cout << "Base1::h()" << endl; }

};

class Base2 {
public:
    int ibase2;
    Base2():ibase2(20) {}
    virtual void f() { cout << "Base2::f()" << endl; }
    virtual void g() { cout << "Base2::g()" << endl; }
    virtual void h() { cout << "Base2::h()" << endl; }
};

class Base3 {
public:
    int ibase3;
    Base3():ibase3(30) {}
    virtual void f() { cout << "Base3::f()" << endl; }
    virtual void g() { cout << "Base3::g()" << endl; }
    virtual void h() { cout << "Base3::h()" << endl; }
};

class Derive : public Base1, public Base2, public Base3 {
public:
    int iderive;
    Derive():iderive(100) {}
    virtual void f() { cout << "Derive::f()" << endl; }
    virtual void g1() { cout << "Derive::g1()" << endl; }
};

對於Derive d這個物件,它的記憶體模型如下:

這裡寫圖片描述

重複繼承

class B
{
    public:
        int ib;
        char cb;
    public:
        B():ib(0),cb('B') {}

        virtual void f() { cout << "B::f()" << endl;}
        virtual void Bf() { cout << "B::Bf()" << endl;}
};
class B1 :  public B
{
    public:
        int ib1;
        char cb1;
    public:
        B1():ib1(11),cb1('1') {}

        virtual void f() { cout << "B1::f()" << endl;}
        virtual void f1() { cout << "B1::f1()" << endl;}
        virtual void Bf1() { cout << "B1::Bf1()" << endl;}

};
class B2:  public B
{
    public:
        int ib2;
        char cb2;
    public:
        B2():ib2(12),cb2('2') {}

        virtual void f() { cout << "B2::f()" << endl;}
        virtual void f2() { cout << "B2::f2()" << endl;}
        virtual void Bf2() { cout << "B2::Bf2()" << endl;}

};

class D : public B1, public B2
{
    public:
        int id;
        char cd;
    public:
        D():id(100),cd('D') {}

        virtual void f() { cout << "D::f()" << endl;}
        virtual void f1() { cout << "D::f1()" << endl;}
        virtual void f2() { cout << "D::f2()" << endl;}
        virtual void Df() { cout << "D::Df()" << endl;}

};

對於D d這個物件,它的記憶體模型如下圖:

這裡寫圖片描述

菱形虛擬繼承

在上面繼承體系下,會出現這樣的情況:
D d;
d.ib = 0; //二義性錯誤
d.B1::ib = 1; //正確
d.B2::ib = 2; //正確
為了避免這種不明確,C++引入了虛基類的概念。這也就是我們文章一開頭講的加virtual關鍵字的解決方法。

class B {……};
class B1 : virtual public B{……};
class B2: virtual public B{……};
class D : public B1, public B2{ …… };

在看菱形虛擬繼承之前,我們先看一下簡單的虛擬單繼承是怎麼樣的,這樣便於我們理解複雜一點的菱形虛擬繼承,我們先看一組程式碼:

class A {
public:
    int _a;
    virtual void fun1() {}
};

class B : public virtual A {
public:
    int _b;
    //virtual void fun1() {}
    //virtual void fun2() {}
};


int main()
{
    B b;
    b._a = 2;
    b._b = 1;
    cout << sizeof(A) << endl;
    cout << sizeof(B) << endl;
    getchar();
    return 0;
}

在VS2013的測試結果為8和16,我們試著去掉//virtual void fun1() {}的註釋,也就是

class B : public virtual A {
public:
    int _b;
    virtual void fun1() {}
    //virtual void fun2() {}
};

此時測試結果仍為8和16,但是當我們去掉//virtual void fun2() {}的註釋,也就是

class B : public virtual A {
public:
    int _b;
    virtual void fun1() {}
    virtual void fun2() {}
};

測試結果為sizeof(A) = 8,sizeof(B) = 20。這是為什麼???為了解決這個問題,我們有必要看看在這幾種情況下的B物件模型,A類物件模型比較簡單,我們知道虛擬函式必有一個指向虛表的指標,再加上A類物件本身有個int型資料加起來就是8。而對於B物件模型,我們可以簡單分幾種情況:
子類有覆蓋(重寫)且沒有新增虛擬函式 and 子類沒有覆蓋(重寫)且沒有新增虛擬函式:這兩種情況並沒有太大差別,對於B物件模型都是下面這種:

這裡寫圖片描述

唯一的區別就是基類A的虛表指標指向的虛表有沒有被重寫而已,因此在第一種和第二種情況下,sizeof(B) = 16。而對於有新增虛擬函式這種情況,對於B的物件模型則是這樣的:

這裡寫圖片描述

因為有重寫基類的虛函數了,所以子類需要額外加一個虛表指標,這樣sizeof(B) =20就不難理解了。有了這些知識,我們再看菱形虛擬繼承就容易多了,首先對於菱形虛擬繼承,它的繼承層次圖大概像下面這個樣子:

這裡寫圖片描述

為了便於分析,我們可以把這個圖拆解下來,也就是說從B到B1,B2是兩個單一的虛擬繼承,而從B1,B2到則是多繼承,這樣一來,問題就變得簡單多了。對於B到B1,B2兩個單一的虛擬繼承,根據前面講的很容易得到B1,B2的物件模型:

這裡寫圖片描述

接下來就是多繼承,這樣終於得到了我們D d的物件模型了:

這裡寫圖片描述