C++對象模型詳解

原文連接:吳秦大神的C++對象模型html

何爲C++對象模型?

C++對象模型能夠歸納爲如下2部分:ios

一、語言中直接支持面向對象程序設計的部分;git

二、對於各類支持的底層實現機制。github

語言中直接支持面向對象程序設計的部分,如構造函數、析構函數、虛函數、繼承(單繼承、多繼承、虛繼承)、多態等等。本文重點介紹底層實現機制。ide

在C語言中,「數據」和「處理數據的操做(函數)」是分開聲明的,也就是說,語言自己並無支持「數據和函數」之間的關聯性。在C++中,經過抽象數據類型(Abstract Data Type,ADT),在類中定義數據和函數,來實現數據和函數直接的綁定。歸納來講,在C++類中有兩種數據成員:static,nonstatic;三種成員函數:static、nonstatic、virtual。函數


clip_image002[4]

以下面的Base類定義:佈局

//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類在機器中咱們如何構建出各類成員數據和成員函數的呢?post

基本C++對象模型

在介紹C++使用的對象模型以前,介紹2種對象模型:簡單對象模型(A Simple Object Model)、表格驅動對象模型(A Table-Drive Object Model)。測試

簡單對象模型(a simple object model

全部的成員佔用相同的空間(跟成員類型無關),對象只是維護了一個包含成員指針的一個表。表中放的是成員的地址,不管是成員變量仍是函數,都是一樣處理。對象並無直接保存成員而只是保存了成員的指針。優化

表格對象模型(a table-driven object model

 

 

這個模型在簡單對象的基礎上又添加了一個間接層。將函數和數據分別存儲在兩個表中,並保存了兩個指向表格的指針。這個模型能夠保證全部的對象具備相同的大小,好比簡單對象模型還與成員的個數有關。其中數據成員表中包含實際數據;函數成員表中包含實際函數的地址(與數據成員相比,多一次尋址)。

C++對象模型

這個模型結合了上面兩個模型的特色,並對內存存取和空間進行了優化。在此模型中,nonstatic數據成員被放置到對象內部,static數據成員、static和nonstatic函數成員軍備放到對象以外。對於虛函數的支持則分兩部分完成:

一、每個class產生一堆指向虛函數的指針,並存放在虛函數表中(Virtual Table,vtbl);

二、每一個對象被添加了一個指針,指向相關的虛函數表vtbl。一般這個指針被稱爲vptr。vptr的設定和重置都由每個class的構造函數,析構函數和拷貝賦值運算符自動完成。

另外,虛函數表地址的前面設置了一個指向type_info的指針,RTTI(Run Time Type Identification)運行時類型識別是由編譯器在編譯時生成的特殊類型信息,包括對象繼承關係,對象自己的描述。RTTI是爲多態而生成的信息,因此只有具備虛函數的對象纔會生成。

這個模型的優勢在於它的空間和存取時間的效率;缺點以下:若是應用程序自己未改變,當所使用的類的nonstatic數據成員添加刪除或修改時,須要從新編譯。

模型驗證測試

爲了驗證上述C++對象模型,test_base_model函數:

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

    cout << classname << endl;
    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) << "\n";
    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的起始內存地址,即虛函數表地址。

虛函數表中的第一個函數地址是虛析構函數的地址,即(int *)*(int *)(&b1);

type_info的地址,等於第一個函數地址減一,即((int *)*(int *)(&b1)-1);

虛函數表中的第二個函數地址是虛函數print()的地址,經過函數指針能夠調用,進行驗證:

    cout << "虛函數表 — 第2個函數地址:\t" << ((int*)*(int*)(&b1) + 1) << "\n";
    typedef void(*Fun)(void);
    Fun pFun = (Fun)*(((int*)*(int*)(&b1)) + 1);
    pFun();
    b1.print();

推測數據成員IBase的地址,即爲虛函數表的地址+1,((int *)(&b)+1);

靜態數據成員和靜態函數所在的內存地址,與數據成員和函數成員位段不同。

運行結果:

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

 

 

 

 

 

圖: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;

};

BaseDerived的類圖以下所示: 

 

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

 

驗證上述C++對象模型,test_single_norewrite():

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

BaseDerived_Overwrite的類圖以下所示:

 

重寫print()函數在虛函數表中表現以下:

 

 

驗證上述C++對象模型,test_single_inherit_rewrite():

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其實虛函數表以0x0000000結束,相似字符串以’\0’結束

C++對象模型中加入多繼承

從單繼承能夠知道,派生類中只是擴充了基類的虛函數表。若是是多繼承的話,又是如何擴充的?

  1. 每一個基類都有本身的虛表。
  2. 子類的成員函數被放到了第一個基類的表中。
  3. 內存佈局中,其父類佈局依次按聲明順序排列。
  4. 每一個基類的虛表中的print()函數都被overwrite成了子類的print ()。這樣作就是爲了解決不一樣的基類類型的指針指向同一個子類實例,而可以調用到實際的函數。

 

上面3個類,Derived_Mutlip_Inherit繼承自BaseBase_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++對象模型中加入虛繼承

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

虛繼承的派生類的內存結構,和普通繼承徹底不一樣。虛繼承的子類,有單獨的虛函數表,另外也單獨保存一份父類的虛函數表,兩部分之間用一個四個字節的0x00000000來做爲分界。派生類的內存中,首先是本身的虛函數表,而後是派生類的數據成員,而後是0x0,以後就是基類的虛函數表,以後是基類的數據成員。

若是派生類沒有本身的虛函數,那麼派生類就不會有虛函數表,可是派生類數據和基類數據之間,仍是須要0x0來間隔。

所以,在虛繼承中,派生類和基類的數據,是徹底間隔的,先存放派生類本身的虛函數表和數據,中間以0x分界,最後保存基類的虛函數和數據。若是派生類重載了父類的虛函數,那麼則將派生類內存中基類虛函數表的相應函數替換

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

簡單虛繼承的2個類BaseDerived_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個類中的函數都是虛函數

l  Derived繼承Base

l  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中包含虛函數表指針,全部size4Derived繼承Base,只是擴充基類的虛函數表,不會新增虛函數表指針,因此size也是4Derived_Virtual虛繼承Base,根據前面的模型知道,派生類有本身的虛函數表及指針,而且有分隔符(0x00000000),而後纔是虛基類的虛函數表等信息,故大小爲4+4+4=12

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

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

數據成員如何訪問(直接取址)

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

靜態綁定與動態綁定

程序調用函數時,將使用那個可執行代碼塊呢?編譯器負責回答這個問題。將源代碼中的函數調用解析爲執行特定的函數代碼塊被稱爲函數名綁定(binding,又稱聯編)。C語言中,這很是簡單,由於每一個函數名都對應一個不一樣的額函數。在C++中,因爲函數重載的緣故,這項任務更復雜。編譯器必須查看函數參數以及函數名才能肯定使用哪一個函數。然而編譯器能夠再編譯過程當中完成這種綁定,這稱爲靜態綁定(static binding),又稱爲早期綁定(early binding

然而虛函數是這項工做變得更加困難。使用哪個函數不是能在編譯階段時肯定的,由於編譯器不知道用戶將選擇哪一種類型因此,編譯器必須可以在程序運行時選擇正確的虛函數的代碼,這被稱爲動態綁定(dynamic binding),又稱爲晚期綁定(late binding

使用虛函數是有代價的,在內存和執行速度方面是有必定成本的,包括:

l  每一個對象都將增大,增大量爲存儲虛函數表指針的大小;

l  對於每一個類,編譯器都建立一個虛函數地址表;

l  對於每一個函數調用,都須要執行一項額外的操做,即到虛函數表中查找地址。

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

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

跟實際對象模型相關聯,普通函數(nonstaticstatic)根據編譯、連接的結果直接獲取函數地址;若是是虛函數根據對象模型,取出對於虛函數地址,而後在虛函數表中查找函數地址。

多態如何實現?

多態(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調用都是調用基類的方法。緣由以下:

l  b.print();b.print_virtual();不能實現多態是由於經過基類對象調用,而非指針或引用因此不能實現多態。

l  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

[3]   關於虛函數的實現原理也能夠參考關於虛函數的那些事兒

相關文章
相關標籤/搜索