C++物件模型詳解

吳秦(Tyler)發表於2015-05-12

何為C++物件模型?

C++物件模型可以概括為以下2部分:

1. 語言中直接支援物件導向程式設計的部分

2. 對於各種支援的底層實現機制

語言中直接支援物件導向程式設計的部分,如建構函式、解構函式、虛擬函式、繼承(單繼承、多繼承、虛繼承)、多型等等,這也是組裡其他同學之前分享過的內容。第一部分這裡我簡單過一下,重點在底層實現機制。

在c語言中,“資料”和“處理資料的操作(函式)”是分開來宣告的,也就是說,語言本身並沒有支援“資料和函式”之間的關聯性。在c++中,通過抽象資料型別(abstract data type,ADT),在類中定義資料和函式,來實現資料和函式直接的繫結。

概括來說,在C++類中有兩種成員資料:static、nonstatic;三種成員函式:static、nonstatic、virtual。

如下面的Base類定義:

#pragma once
#include<iostream>
using namespace std;
class Base
{
public:
    Base(int);
    virtual ~Base(void);

    int getIBase() const;
    static int instanceCount();
    virtual void print() const;

protected:

    int iBase;
    static int count;
};

Base類在機器中我們如何構建出各種成員資料和成員函式的呢?

基本C++物件模型

在介紹C++使用的物件模型之前,介紹2種物件模型:簡單物件模型(a simple object model)、表格驅動物件模型(a table-driven object model)。

簡單物件模型(a simple object model)

所有的成員佔用相同的空間(跟成員型別無關),物件只是維護了一個包含成員指標的一個表。表中放的是成員的地址,無論上成員變數還是函式,都是這樣處理。物件並沒有直接儲存成員而是儲存了成員的指標。

表格物件模型(a table-driven object model)

這個模型在簡單物件的基礎上又新增了一個間接層。將成員分成函式和資料,並且用兩個表格儲存,然後物件只儲存了兩個指向表格的指標。這個模型可以保證所有的物件具有相同的大小,比如簡單物件模型還與成員的個數相關。其中資料成員表中包含實際資料;函式成員表中包含的實際函式的地址(與資料成員相比,多一次定址)。

C++物件模型

這個模型從結合上面2中模型的特點,並對記憶體存取和空間進行了優化。在此模型中,non static 資料成員被放置到物件內部,static資料成員, static and nonstatic 函式成員均被放到物件之外。對於虛擬函式的支援則分兩步完成:

1. 每一個class產生一堆指向虛擬函式的指標,放在表格之中。這個表格稱之為虛擬函式表(virtual table,vtbl)。

2. 每一個物件被新增了一個指標,指向相關的虛擬函式表vtbl。通常這個指標被稱為vptr。vptr的設定(setting)和重置(resetting)都由每一個class的建構函式,解構函式和拷貝賦值運算子自動完成。

另外,虛擬函式表地址的前面設定了一個指向type_info的指標,RTTI(Run Time Type Identification)執行時型別識別是有編譯器在編譯器生成的特殊型別資訊,包括物件繼承關係,物件本身的描述,RTTI是為多型而生成的資訊,所以只有具有虛擬函式的物件在會生成。

這個模型的優點在於它的空間和存取時間的效率;缺點如下:如果應用程式本身未改變,但當所使用的類的non static資料成員新增刪除或修改時,需要重新編譯。

模型驗證測試

為了驗證上述C++物件模型,我們編寫如下測試程式碼。

void test_base_model()
{
    Base b1(1000);
    cout << "物件b1的起始記憶體地址:" << &b1 << endl;
    cout << "type_info資訊:" << ((int*)*(int*)(&b1) - 1) << endl;
    RTTICompleteObjectLocator str=
        *((RTTICompleteObjectLocator*)*((int*)*(int*)(&b1) - 1));
    //abstract class name from RTTI
    string classname(str.pTypeDescriptor->name);
    classname = classname.substr(4,classname.find("@@")-4);
    cout << classname <<endl;
    cout << "虛擬函式表地址:\t\t\t" << (int*)(&b1) << endl;
    cout << "虛擬函式表 — 第1個函式地址:\t" << (int*)*(int*)(&b1) << "\t即解構函式地址:" << (int*)*((int*)*(int*)(&b1)) << endl;
    cout << "虛擬函式表 — 第2個函式地址:\t" << ((int*)*(int*)(&b1) + 1) << "\t";
    typedef void(*Fun)(void);
    Fun pFun = (Fun)*(((int*)*(int*)(&b1)) + 1);
    pFun();
    b1.print();
    cout << endl;
    cout << "推測資料成員iBase地址:\t\t" << ((int*)(&b1) +1) << "\t通過地址取值iBase的值:" << *((int*)(&b1) +1) << endl;
    cout << "Base::getIBase(): " << b1.getIBase() << endl;

    b1.instanceCount();
    cout << "靜態函式instanceCount地址: " << b1.instanceCount << endl;
}

根據C++物件模型,例項化物件b1的起始記憶體地址,即虛擬函式表地址。

  • 虛擬函式表的中第1個函式地址是虛解構函式地址;
  • 虛擬函式表的中第2個函式地址是虛擬函式print()的地址,通過函式指標可以呼叫,進行驗證;
  • 推測資料成員iBase的地址,為虛擬函式表的地址 + 1,((int*)(&b1) +1);
  • 靜態資料成員和靜態函式所在記憶體地址,與物件資料成員和函式成員位段不一樣;

下面是測試程式碼輸出:(從下面2個圖驗證了,上面的觀點。)

注意:本測試程式碼及後面的測試程式碼中寫的函式地址,是對應虛擬函式表項的地址,不是實際的函式地址。

圖:測試程式碼輸出結果

圖:vs斷點觀察(注意看虛擬函式表中第一個函式的地址,名稱與測試程式碼輸出一致)

上面介紹並驗證了基本的C++物件模型,引入繼承之後,C++物件模型又是怎樣的?

C++物件模型中加入單繼承

不管是單繼承、多繼承,還是虛繼承,如果基於“簡單物件模型”,每一個基類都可以被派生類中的一個slot指出,該slot內包含基類物件的地址。這個機制的主要缺點是,因為間接性而導致空間和存取時間上的額外負擔;優點則是派生類物件的大小不會因其基類的改變而受影響。

如果基於“表格驅動模型”,派生類中有一個slot指向基類表,表格中的每一個slot含一個相關的基類地址(這個很像虛擬函式表,內含每一個虛擬函式的地址)。這樣每個派生類物件汗一個bptr,它會被初始化,指向其基類表。這種策略的主要缺點是由於間接性而導致的空間和存取時間上的額外負擔;優點則是在每一個派生類物件中對繼承都有一致的表現方式,每一個派生類物件都應該在某個固定位置上放置一個基類表指標,與基類的大小或數量無關。第二個優點是,不需要改變派生類物件本身,就可以放大,縮小、或更改基類表。

不管上述哪一種機制,“間接性”的級數都將因為整合的深度而增加。C++實際模型是,對於一般繼承是擴充已有存在的虛擬函式表;對於虛繼承新增一個虛擬函式表指標。

無重寫的單繼承

無重寫,即派生類中沒有於基類同名的虛擬函式。

#pragma once
#include "base.h"

class Derived :
    public Base
{
public:
    Derived(int);
    virtual ~Derived(void);
    virtual void derived_print(void);

protected:
    int iDerived;
};

Base、Derived的類圖如下所示:

Base的模型跟上面的一樣,不受繼承的影響。Derived不是虛繼承,所以是擴充已存在的虛擬函式表,所以結構如下圖所示:

為了驗證上述C++物件模型,我們編寫如下測試程式碼。

void test_single_inherit_norewrite()
{
    Derived d(9999);
    cout << "物件d的起始記憶體地址:" << &d << endl;
    cout << "type_info資訊:" << ((int*)*(int*)(&d) - 1) << endl;
    RTTICompleteObjectLocator str=
        *((RTTICompleteObjectLocator*)*((int*)*(int*)(&d) - 1));
    //abstract class name from RTTI
    string classname(str.pTypeDescriptor->name);
    classname = classname.substr(4,classname.find("@@")-4);
    cout << classname <<endl;
    cout << "虛擬函式表地址:\t\t\t" << (int*)(&d) << endl;
    cout << "虛擬函式表 — 第1個函式地址:\t" << (int*)*(int*)(&d) << "\t即解構函式地址" << endl;
    cout << "虛擬函式表 — 第2個函式地址:\t" << ((int*)*(int*)(&d) + 1) << "\t";
    typedef void(*Fun)(void);
    Fun pFun = (Fun)*(((int*)*(int*)(&d)) + 1);
    pFun();
    d.print();
    cout << endl;

    cout << "虛擬函式表 — 第3個函式地址:\t" << ((int*)*(int*)(&d) + 2) << "\t";
    pFun = (Fun)*(((int*)*(int*)(&d)) + 2);
    pFun();
    d.derived_print();
    cout << endl;

    cout << "推測資料成員iBase地址:\t\t" << ((int*)(&d) +1) << "\t通過地址取得的值:" << *((int*)(&d) +1) << endl;
    cout << "推測資料成員iDerived地址:\t" << ((int*)(&d) +2) << "\t通過地址取得的值:" << *((int*)(&d) +2) << endl;
}

輸出結果如下圖所示:

有重寫的單繼承

派生類中重寫了基類的print()函式。

#pragma once
#include "base.h"
class Derived_Overrite :
    public Base
{
public:
    Derived_Overrite(int);
    virtual ~Derived_Overrite(void);
    virtual void print(void) const;

protected:
    int iDerived;
};

Base、Derived_Overwrite的類圖如下所示:

重寫print()函式在虛擬函式表中表現如下:

為了驗證上述C++物件模型,我們編寫如下測試程式碼。

void test_single_inherit_rewrite()
{
    Derived_Overrite d(111111);
    cout << "物件d的起始記憶體地址:\t\t" << &d << endl;
    cout << "虛擬函式表地址:\t\t\t" << (int*)(&d) << endl;
    cout << "虛擬函式表 — 第1個函式地址:\t" << (int*)*(int*)(&d) << "\t即解構函式地址" << endl;
    cout << "虛擬函式表 — 第2個函式地址:\t" << ((int*)*(int*)(&d) + 1) << "\t";
    typedef void(*Fun)(void);
    Fun pFun = (Fun)*(((int*)*(int*)(&d)) + 1);
    pFun();
    d.print();
    cout << endl;

    cout << "虛擬函式表 — 第3個函式地址:\t" << *((int*)*(int*)(&d) + 2) << "【結束】\t";
    cout << endl;

    cout << "推測資料成員iBase地址:\t\t" << ((int*)(&d) +1) << "\t通過地址取得的值:" << *((int*)(&d) +1) << endl;
    cout << "推測資料成員iDerived地址:\t" << ((int*)(&d) +2) << "\t通過地址取得的值:" << *((int*)(&d) +2) << endl;
}

輸出結果如下圖所示:

特別注意下,前面的模型虛擬函式表中最後一項沒有列印出來,本例項中共2個虛擬函式,列印虛擬函式表第3項為0。其實虛擬函式表以0×0000000結束,類似字串以’’結束。

C++物件模型中加入多繼承

從單繼承可以知道,派生類中只是擴充了基類的虛擬函式表。如果是多繼承的話,又是如何擴充的?

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

2) 子類的成員函式被放到了第一個基類的表中。

3) 記憶體佈局中,其父類佈局依次按宣告順序排列。

4) 每個基類的虛表中的print()函式都被overwrite成了子類的print ()。這樣做就是為了解決不同的基類型別的指標指向同一個子類例項,而能夠呼叫到實際的函式。

上面3個類,Derived_Mutlip_Inherit繼承自Base、Base_1兩個類,Derived_Mutlip_Inherit的結構如下所示:

為了驗證上述C++物件模型,我們編寫如下測試程式碼。

void test_multip_inherit()
{
    Derived_Mutlip_Inherit dmi(3333);
    cout << "物件dmi的起始記憶體地址:\t\t" << &dmi << endl;
    cout << "虛擬函式表_vptr_Base地址:\t" << (int*)(&dmi) << endl;
    cout << "_vptr_Base — 第1個函式地址:\t" << (int*)*(int*)(&dmi) << "\t即解構函式地址" << endl;
    cout << "_vptr_Base — 第2個函式地址:\t" << ((int*)*(int*)(&dmi) + 1) << "\t";
    typedef void(*Fun)(void);
    Fun pFun = (Fun)*(((int*)*(int*)(&dmi)) + 1);
    pFun();
    cout << endl;
    cout << "_vptr_Base — 第3個函式地址:\t" << ((int*)*(int*)(&dmi) + 2) << "\t";
    pFun = (Fun)*(((int*)*(int*)(&dmi)) + 2);
    pFun();
    cout << endl;
    cout << "_vptr_Base — 第4個函式地址:\t" << *((int*)*(int*)(&dmi) + 3) << "【結束】\t";
    cout << endl;
    cout << "推測資料成員iBase地址:\t\t" << ((int*)(&dmi) +1) << "\t通過地址取得的值:" << *((int*)(&dmi) +1) << endl;

    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_INTENSITY | FOREGROUND_GREEN);
    cout << "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" << endl;
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_INTENSITY | FOREGROUND_RED);
    cout << "虛擬函式表_vptr_Base1地址:\t" << ((int*)(&dmi) +2) << endl;
    cout << "_vptr_Base1 — 第1個函式地址:\t" << (int*)*((int*)(&dmi) +2) << "\t即解構函式地址" << endl;
    cout << "_vptr_Base1 — 第2個函式地址:\t" << ((int*)*((int*)(&dmi) +2) + 1) << "\t";
    typedef void(*Fun)(void);
    pFun = (Fun)*((int*)*((int*)(&dmi) +2) + 1);
    pFun();
    cout << endl;
    cout << "_vptr_Base1 — 第3個函式地址:\t" << *((int*)*(int*)((int*)(&dmi) +2) + 2) << "【結束】\t";
    cout << endl;  
    cout << "推測資料成員iBase1地址:\t" << ((int*)(&dmi) +3) << "\t通過地址取得的值:" << *((int*)(&dmi) +3) << endl;
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_INTENSITY | FOREGROUND_GREEN);
    cout << "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" << endl;
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_INTENSITY | FOREGROUND_RED);
    cout << "推測資料成員iDerived地址:\t" << ((int*)(&dmi) +4) << "\t通過地址取得的值:" << *((int*)(&dmi) +4) << endl;
}

輸出結果如下圖所示:

C++物件模型中加入虛繼承

虛繼承是為了解決重複繼承中多個間接父類的問題的,所以不能使用上面簡單的擴充併為每個虛基類提供一個虛擬函式指標(這樣會導致重複繼承的基類會有多個虛擬函式表)形式。

虛繼承的派生類的記憶體結構,和普通繼承完全不同。虛繼承的子類,有單獨的虛擬函式表,另外也單獨儲存一份父類的虛擬函式表,兩部分之間用一個四個位元組的0×00000000來作為分界。派生類的記憶體中,首先是自己的虛擬函式表,然後是派生類的資料成員,然後是0×0,之後就是基類的虛擬函式表,之後是基類的資料成員。

如果派生類沒有自己的虛擬函式,那麼派生類就不會有虛擬函式表,但是派生類資料和基類資料之間,還是需要0×0來間隔。

因此,在虛繼承中,派生類和基類的資料,是完全間隔的,先存放派生類自己的虛擬函式表和資料,中間以0x分界,最後儲存基類的虛擬函式和資料。如果派生類過載了父類的虛擬函式,那麼則將派生類記憶體中基類虛擬函式表的相應函式替換。

簡單虛繼承(無重複繼承情況)

簡單虛繼承的2個類Base、Derived_Virtual_Inherit1的關係如下所示:

Derived_Virtual_Inherit1的物件模型如下圖:

為了驗證上述C++物件模型,我們編寫如下測試程式碼。

void test_single_vitrual_inherit()
{
    Derived_Virtual_Inherit1 dvi1(88888);
    cout << "物件dvi1的起始記憶體地址:\t\t" << &dvi1 << endl;
    cout << "虛擬函式表_vptr_Derived..地址:\t\t" << (int*)(&dvi1) << endl;
    cout << "_vptr_Derived — 第1個函式地址:\t" << (int*)*(int*)(&dvi1) << endl;
    typedef void(*Fun)(void);
    Fun pFun = (Fun)*((int*)*(int*)(&dvi1));
    pFun();
    cout << endl;
    cout << "_vptr_Derived — 第2個函式地址:\t" << *((int*)*(int*)(&dvi1) + 1) << "【結束】\t";
    cout << endl;
    cout << "=======================:\t" << ((int*)(&dvi1) +1) << "\t通過地址取得的值:" << (int*)*((int*)(&dvi1) +1) << "\t" <<*(int*)*((int*)(&dvi1) +1) << endl;
    cout << "推測資料成員iDerived地址:\t" << ((int*)(&dvi1) +2) << "\t通過地址取得的值:" << *((int*)(&dvi1) +2) << endl;
    cout << "=======================:\t" << ((int*)(&dvi1) +3) << "\t通過地址取得的值:" << *((int*)(&dvi1) +3) << endl;
    cout << "虛擬函式表_vptr_Base地址:\t" << ((int*)(&dvi1) +4) << endl;
    cout << "_vptr_Base — 第1個函式地址:\t" << (int*)*((int*)(&dvi1) +4) << "\t即解構函式地址" << endl;
    cout << "_vptr_Base — 第2個函式地址:\t" << ((int*)*((int*)(&dvi1) +4) +1) << "\t";
    pFun = (Fun)*((int*)*((int*)(&dvi1) +4) +1);
    pFun();
    cout << endl;
    cout << "_vptr_Base — 第3個函式地址:\t" << ((int*)*((int*)(&dvi1) +4) +2) << "【結束】\t" << *((int*)*((int*)(&dvi1) +4) +2);
    cout << endl;
    cout << "推測資料成員iBase地址:\t\t" << ((int*)(&dvi1) +5) << "\t通過地址取得的值:" << *((int*)(&dvi1) +5) << endl;
}

輸出結果如下圖所示:

菱形繼承(含重複繼承、多繼承情況)

菱形繼承關係如下圖:

Derived_Virtual的物件模型如下圖:

為了驗證上述C++物件模型,我們編寫如下測試程式碼。

void test_multip_vitrual_inherit()
{
    Derived_Virtual dvi1(88888);
    cout << "物件dvi1的起始記憶體地址:\t\t" << &dvi1 << endl;
    cout << "虛擬函式表_vptr_inherit1地址:\t\t" << (int*)(&dvi1) << endl;
    cout << "_vptr_inherit1 — 第1個函式地址:\t" << (int*)*(int*)(&dvi1) << endl;
    typedef void(*Fun)(void);
    Fun pFun = (Fun)*((int*)*(int*)(&dvi1));
    pFun();
    cout << endl;
    cout << "_vptr_inherit1 — 第2個函式地址:\t" << ((int*)*(int*)(&dvi1) + 1) << endl;
    pFun = (Fun)*((int*)*(int*)(&dvi1) + 1);
    pFun();
    cout << endl;
    cout << "_vptr_inherit1 — 第3個函式地址:\t" << ((int*)*(int*)(&dvi1) + 2) << "\t通過地址取得的值:" << *((int*)*(int*)(&dvi1) + 2) << "【結束】\t";
    cout << endl;
    cout << "======指向=============:\t" << ((int*)(&dvi1) +1) << "\t通過地址取得的值:" << (int*)*((int*)(&dvi1) +1)<< "\t" <<*(int*)*((int*)(&dvi1) +1) << endl;
    cout << "推測資料成員iInherit1地址:\t" << ((int*)(&dvi1) +2) << "\t通過地址取得的值:" << *((int*)(&dvi1) +2) << endl;
    //
    cout << "虛擬函式表_vptr_inherit2地址:\t" << ((int*)(&dvi1) +3) << endl;
    cout << "_vptr_inherit2 — 第1個函式地址:\t" << (int*)*((int*)(&dvi1) +3) << endl;
    pFun = (Fun)*((int*)*((int*)(&dvi1) +3));
    pFun();
    cout << endl;
    cout << "_vptr_inherit2 — 第2個函式地址:\t" << (int*)*((int*)(&dvi1) +3) + 1 <<"\t通過地址取得的值:" << *((int*)*((int*)(&dvi1) +3) + 1) << "【結束】\t" << endl;
    cout << endl;
    cout << "======指向=============:\t" << ((int*)(&dvi1) +4) << "\t通過地址取得的值:" << (int*)*((int*)(&dvi1) +4) << "\t" <<*(int*)*((int*)(&dvi1) +4)<< endl;
    cout << "推測資料成員iInherit2地址:\t" << ((int*)(&dvi1) +5) << "\t通過地址取得的值:" << *((int*)(&dvi1) +5) << endl;
    cout << "推測資料成員iDerived地址:\t" << ((int*)(&dvi1) +6) << "\t通過地址取得的值:" << *((int*)(&dvi1) +6) << endl;
    cout << "=======================:\t" << ((int*)(&dvi1) +7) << "\t通過地址取得的值:" << *((int*)(&dvi1) +7) << endl;
    //
    cout << "虛擬函式表_vptr_Base地址:\t" << ((int*)(&dvi1) +8) << endl;
    cout << "_vptr_Base — 第1個函式地址:\t" << (int*)*((int*)(&dvi1) +8) << "\t即解構函式地址" << endl;
    cout << "_vptr_Base — 第2個函式地址:\t" << ((int*)*((int*)(&dvi1) +8) +1) << "\t";
    pFun = (Fun)*((int*)*((int*)(&dvi1) +8) +1);
    pFun();
    cout << endl;
    cout << "_vptr_Base — 第3個函式地址:\t" << ((int*)*((int*)(&dvi1) +8) +2) << "【結束】\t" << *((int*)*((int*)(&dvi1) +8) +2);
    cout << endl;
    cout << "推測資料成員iBase地址:\t\t" << ((int*)(&dvi1) +9) << "\t通過地址取得的值:" << *((int*)(&dvi1) +9) << endl;
}

輸出結果如下圖所示:

至此,C++物件模型介紹的差不多了,清楚了C++物件模型之後,很多疑問就能迎刃而解了。下面結合模型介紹一些典型問題。

如何訪問成員?

前面介紹了C++物件模型,下面介紹C++物件模型的對訪問成員的影響。其實清楚了C++物件模型,就清楚了成員訪問機制。下面分別針對資料成員和函式成員是如何訪問到的,給出一個大致介紹。

物件大小問題

其中:3個類中的函式都是虛擬函式

  • Derived繼承Base
  • Derived_Virtual虛繼承Base
void test_size()
{
    Base b;
    Derived d;
    Derived_Virtual dv;
    cout << "sizeof(b):\t" << sizeof(b) << endl;
    cout << "sizeof(d):\t" << sizeof(d) << endl;
    cout << "sizeof(dv):\t" << sizeof(dv) << endl;
}

輸出如下:

因為Base中包含虛擬函式表指標,所有size為4;Derived繼承Base,只是擴充基類的虛擬函式表,不會新增虛擬函式表指標,所以size也是4;Derived_Virtual虛繼承Base,根據前面的模型知道,派生類有自己的虛擬函式表及指標,並且有分隔符(0×00000000),然後才是虛基類的虛擬函式表等資訊,故大小為4+4+4=12。

#pragma once
class Empty
{
public:
    Empty(void);
    ~Empty(void);
};

Empty p,sizeof(p)的大小是多少?事實上並不是空的,它有一個隱晦的1byte,那是被編譯器安插進去的一個char。這將使得這個class的兩個物件得以在內中有獨一無二的地址。

資料成員如何訪問(直接取址)

跟實際物件模型相關聯,根據物件起始地址+偏移量取得。

靜態繫結與動態繫結

程式呼叫函式時,將使用那個可執行程式碼塊呢?編譯器負責回答這個問題。將原始碼中的函式呼叫解析為執行特定的函式程式碼塊被稱為函式名繫結(binding,又稱聯編)。在C語言中,這非常簡單,因為每個函式名都對應一個不同的額函式。在C++中,由於函式過載的緣故,這項任務更復雜。編譯器必須檢視函式引數以及函式名才能確定使用哪個函式。然而編譯器可以再編譯過程中完成這種繫結,這稱為靜態繫結(static binding),又稱為早期繫結(early binding)。

然而虛擬函式是這項工作變得更加困難。使用哪一個函式不是能在編譯階段時確定的,因為編譯器不知道使用者將選擇哪種型別。所以,編譯器必須能夠在程式執行時選擇正確的虛擬函式的程式碼,這被稱為動態繫結(dynamic binding),又稱為晚期繫結(late binding)。

使用虛擬函式是有代價的,在記憶體和執行速度方面是有一定成本的,包括:

  • 每個物件都將增大,增大量為儲存虛擬函式表指標的大小;
  • 對於每個類,編譯器都建立一個虛擬函式地址表;
  • 對於每個函式呼叫,都需要執行一項額外的操作,即到虛擬函式表中查詢地址。

雖然非虛擬函式比虛擬函式效率稍高,單不具備動態聯編能力。

函式成員如何訪問(間接取址)

跟實際物件模型相關聯,普通函式(nonstatic、static)根據編譯、連結的結果直接獲取函式地址;如果是虛擬函式根據物件模型,取出對於虛擬函式地址,然後在虛擬函式表中查詢函式地址。

多型如何實現?

多型的實現

多型(Polymorphisn)在C++中是通過虛擬函式實現的。通過前面的模型【參見“有重寫的單繼承”】知道,如果類中有虛擬函式,編譯器就會自動生成一個虛擬函式表,物件中包含一個指向虛擬函式表的指標。能夠實現多型的關鍵在於:虛擬函式是允許被派生類重寫的,在虛擬函式表中,派生類函式對覆蓋(override)基類函式。除此之外,還必須通過指標或引用呼叫方法才行,將派生類物件賦給基類物件。

上面2個類,基類Base、派生類Derived中都包含下面2個方法:

  • void print() const;
  • virtual void print_virtual() const;

這個2個方法的區別就在於一個是普通成員函式,一個是虛擬函式。編寫測試程式碼如下:

void test_polmorphisn()
{
    Base b;
    Derived d;

    b = d;
    b.print();
    b.print_virtual();

    Base *p;
    p = &d;
    p->print();
    p->print_virtual();
}

根據模型推測只有p->print_virtual()才實現了動態,其他3呼叫都是呼叫基類的方法。原因如下:

  • b.print();b.print_virtual();不能實現多型是因為通過基類物件呼叫,而非指標或引用所以不能實現多型。
  • p->print();不能實現多型是因為,print函式沒有宣告為虛擬函式(virtual),派生類中也定義了print函式只是隱藏了基類的print函式。

為什麼解構函式設為虛擬函式是必要的

解構函式應當都是虛擬函式,除非明確該類不做基類(不被其他類繼承)。基類的解構函式宣告為虛擬函式,這樣做是為了確保釋放派生物件時,按照正確的順序呼叫解構函式。

從前面介紹的C++物件模型可以知道,如果解構函式不定義為虛擬函式,那麼派生類就不會重寫基類的解構函式,在有多型行為的時候,派生類的解構函式不會被呼叫到(有記憶體洩漏的風險!)。

例如,通過new一個派生類物件,賦給基類指標,然後delete基類指標。

void test_vitual_destructor()
{
    Base *p = new Derived();
    delete p;
}

如果基類的解構函式不是解構函式:

注意,缺少了派生類的解構函式呼叫。把解構函式宣告為虛擬函式,呼叫就正常了:

相關資料

[1] 深度探索C++物件模型,侯捷

[2] 測試程式碼下載:https://github.com/saylorzhu/CppObjectDataModelTestCode

相關文章