深入探索C++對象模型

一個博客伏穆,這個博客記錄了他讀這本書的筆記耻卡,總結(jié)得不錯屿脐。
《深度探索C++對象模型》筆記匯總

1. C++對象模型與內(nèi)存布局

參考資料

在C++中有兩種類的數(shù)據(jù)成員:static和nonstatic挺益,以及三種類的成員函數(shù):static歉糜、nonstatic和virtual。在C++對象模型中望众,非靜態(tài)數(shù)據(jù)成員被配置于每一個類的對象之中匪补,靜態(tài)數(shù)據(jù)成員則被存放在所有的類對象之外;靜態(tài)及非靜態(tài)成員函數(shù)也唄放在類對象之外烂翰,虛函數(shù)則通過以下兩個步驟支持:

  1. 每一個類產(chǎn)生出一堆指向虛函數(shù)的指針夯缺,放在表格之中,這個表格被稱為虛函數(shù)表(virtual table, vtbl)甘耿。
  2. 每一個類對象被添加了一個指針踊兜,指向相關(guān)的虛函數(shù)表,通常這個指針被稱為vptr佳恬。vptr的設(shè)定和重置都由每一個類的構(gòu)造函數(shù)捏境、析構(gòu)函數(shù)和拷貝賦值運算符自動完成于游。另外,虛函數(shù)表地址的前面設(shè)置了一個指向type_info的指針垫言,RTTI(Run Time Type Identification)運行時類型識別是由編譯器在編譯器生成的特殊類型信息贰剥,包括對象繼承關(guān)系,對象本身的描述筷频,RTTI是為多態(tài)而生成的信息蚌成,所以只有具有虛函數(shù)的對象在會生成。
class Point
{
    public:
        Point(float xval);
        virtual ~Point();
        
        float x() const;
        static int PointCount();
        
    protected:
        virtual ostream& printf(ostream& os) const;
        
        float _x;
        static int _point_count;
}
C++對象模型

這個模型的主要優(yōu)點在于它的空間和存取時間的效率凛捏;主要缺點則是担忧,如果應(yīng)用程序代碼本身未曾改變,但所用到的類對象的非靜態(tài)數(shù)據(jù)成員有所增加坯癣、移除或修改涵妥,那么這些應(yīng)用程序的代碼同樣得重新編譯。

  • 非static成員變量被放置于每一個類對象中坡锡,非static成員函數(shù)放在類的對象之外蓬网,且非static成員變量在內(nèi)存中的存放順序與其在類內(nèi)的聲明順序一致。
  • static成員變量存放在類的對象之外鹉勒,static成員函數(shù)也放在類的對象之外帆锋。
  • C++中的虛函數(shù)是通過虛函數(shù)表(vtbl)來實現(xiàn),每一個類為每一個virtual函數(shù)產(chǎn)生一個指針禽额,放在表格中锯厢,這個表格就是虛函數(shù)表。每一個類對象會被安插一個指針(vptr)脯倒,指向該類的虛函數(shù)表实辑。vptr的設(shè)定和重置都由每一個類的構(gòu)造函數(shù)、析構(gòu)函數(shù)和復(fù)制賦值運算符自動完成藻丢。在本人的環(huán)境中剪撬,類的對象的安插的vptr放在該對象所占內(nèi)存的最前面。
  • 虛函數(shù)表中函數(shù)指針存放的順序與虛函數(shù)在類中聲明的順序一致悠反。

加上繼承

C++支持單一繼承和多重繼承残黑,甚至繼承關(guān)系也可以指定為virtual(也就是共享的意思)。在虛擬繼承的情況下斋否,基類不管在繼承串鏈中被派生多少次梨水,永遠只會存在一個實體。

  1. 單繼承(只有一個父類)
    類的繼承關(guān)系為:class Derived : public Base


    Derived類的對象的內(nèi)存布局為:虛函數(shù)表指針茵臭、Base類的非static成員變量疫诽、Derived類的非static成員變量。

  2. 多重繼承(多個父類)
    類的繼承關(guān)系如下:class Derived : public Base1, public Base2


    Derived類的對象的內(nèi)存布局為:基類Base1子對象和基類Base2子對象及Derived類的非static成員變量組成∑嫱剑基類子對象包括其虛函數(shù)表指針和其非static的成員變量雏亚。

  3. 重復(fù)繼承(繼承的多個父類中其父類有相同的超類)
    類的繼承關(guān)系如下:
    class Base1 : public Base
    class Base2: public Base
    class Derived : public Base1, public Base2


    Derived類的對象的內(nèi)存布局與多繼承相似,但是可以看到基類Base的子對象在Derived類的對象的內(nèi)存中存在一份拷貝逼龟。這樣直接使用Derived中基類Base的相關(guān)成員時评凝,就會引發(fā)歧義追葡,可使用多重虛擬繼承消除之腺律。

  4. 多重虛擬繼承(使用virtual方式繼承,為了保證繼承后父類的內(nèi)存布局只會存在一份)
    類的繼承關(guān)系如下:
    class Base1 : virtual public Base
    class Base2: virtual public Base
    class Derived : public Base1, public Base2


    Derived類的對象的內(nèi)存布局與重復(fù)繼承的類的對象的內(nèi)存分布類似宜肉,但是基類Base的子對象沒有拷貝一份匀钧,在對象的內(nèi)存中僅存在在一個Base類的子對象。但是它的非static成員變量放置在對象的末尾處谬返。


2. 構(gòu)造函數(shù)語意學(xué)

默認構(gòu)造函數(shù)的建構(gòu)操作

通常很多C++程序員存在兩種誤解:

  • 沒有定義默認構(gòu)造函數(shù)的類都會被編譯器生成一個默認構(gòu)造函數(shù)之斯。
  • 編譯器生成的默認構(gòu)造函數(shù)會明確初始化類中每一個數(shù)據(jù)成員。

C++標準規(guī)定:如果類的設(shè)計者并未為類定義任何構(gòu)造函數(shù)遣铝,那么會有一個默認 構(gòu)造函數(shù)被暗中生成佑刷,而這個暗中生成的默認構(gòu)造函數(shù)通常是不做什么事的(無用的),下面四種情況除外酿炸。

換句話說瘫絮,有以下四種情況編譯器必須為未聲明構(gòu)造函數(shù)的類生成一個會做點事 的默認構(gòu)造函數(shù)。我們會看到這些默認構(gòu)造函數(shù)僅“忠于編譯器”填硕,而可能不會按 照程序員的意愿程效命麦萤。

  1. 包含有帶默認構(gòu)造函數(shù)的對象成員的類
    若一個類X沒有定義任何構(gòu)造函數(shù),但卻包含一個或以上定義有默認構(gòu)造函數(shù)的 對象成員扁眯,此時編譯器會為X合成默認構(gòu)造函數(shù)壮莹,該默認函數(shù)會調(diào)用對象成員的 默認構(gòu)造函數(shù)為之初始化。如果對象的成員沒有定義默認構(gòu)造函數(shù)姻檀,那么編譯器 合成的默認構(gòu)造函數(shù)將不會為之提供初始化命满。例如類A包含兩個數(shù)據(jù)成員對象, 分別為:string strchar *Cstr绣版,那么編譯器生成的默認構(gòu)造函數(shù)將只提 供對string類型成員的初始化周荐,而不會提供對char*類型的初始化。
    假如類X的設(shè)計者為X定義了默認的構(gòu)造函數(shù)來完成對str的初始化僵娃,形如:A::A({Cstr=”hello”};因為默認構(gòu)造函數(shù)已經(jīng)定義概作,編譯器將不能再生成一 個默認構(gòu)造函數(shù)。但是編譯器將會擴充程序員定義的默認構(gòu)造函數(shù)——在最前面插 入對初始化str的代碼默怨。若有多個定義有默認構(gòu)造函數(shù)的成員對象讯榕,那么這些成員 對象的默認構(gòu)造函數(shù)的調(diào)用將依據(jù)聲明順序排列。

  2. 繼承自帶有默認構(gòu)造函數(shù)的基類的類
    如果一個沒有定義任何構(gòu)造函數(shù)的類派生自帶有默認構(gòu)造函數(shù)的基類,那么編譯 器為它定義的默認構(gòu)造函數(shù)愚屁,將按照聲明順序為之依次調(diào)用其基類的默認構(gòu)造函 數(shù)济竹。若該類沒有定義默認構(gòu)造函數(shù)而定義了多個其他構(gòu)造函數(shù),那么編譯器擴充它的所有構(gòu)造函數(shù)——加入必要的基類默認構(gòu)造函數(shù)霎槐。另外送浊,編譯器會將基類的默認構(gòu)造函數(shù)代碼加在對象成員的默認構(gòu)造函數(shù)代碼之前。

  3. 帶有虛函數(shù)的類
    帶有虛函數(shù)的類丘跌,與其它類不太一樣袭景,因為它多了一個vptr,而vptr的設(shè)置是由 編譯器完成的闭树,因此編譯器會為類的每個構(gòu)造函數(shù)添加代碼來完成對vptr的初始化耸棒。

  4. 帶有一個虛基類的類
    在這種情況下,編譯器要將虛基類在類中的位置準備妥當报辱,提供支持虛基類的機 制与殃。也就是說要在所有構(gòu)造函數(shù)中加入實現(xiàn)前述功能的的代碼。沒有構(gòu)造函數(shù)將 合成以完成上述工作碍现。

總的來說幅疼,編譯器將對構(gòu)造函數(shù)動這些手腳:

  • 如果類虛繼承自基類,編譯器將在所有構(gòu)造函數(shù)中插入準備虛基類位置的代 碼和提供支持虛基類機制的代碼昼接。
  • 如果類聲明有虛函數(shù)爽篷,那么編譯器將為之生成虛函數(shù)表以存儲虛函數(shù)地址, 并將虛函數(shù)指針(vptr)的初始化代碼插入到類的所有構(gòu)造函數(shù)中辩棒。
  • 如果類的父類有默認構(gòu)造函數(shù)狼忱,編譯將會對所有的默認構(gòu)造函數(shù)插入調(diào)用其 父類必要的默認構(gòu)造函數(shù)。必要是指設(shè)計者沒有顯示初始化其父類一睁,調(diào)用順 序钻弄,依照其繼承時聲明順序。
  • 如果類包含帶有默認構(gòu)造函數(shù)的對象成員者吁,那么編譯器將會為所有的構(gòu)造函 數(shù)插入對這些對象成員的默認構(gòu)造函數(shù)進行必要的調(diào)用代碼窘俺,所謂必要是指 類設(shè)計者設(shè)計的構(gòu)造函數(shù)沒有對對象成員進行顯式初始化。成員對象默認構(gòu) 造函數(shù)的調(diào)用順序复凳,依照其聲明順序瘤泪。
  • 若類沒有定義任何構(gòu)造函數(shù),編譯器會為其合成默認構(gòu)造函數(shù)育八,再執(zhí)行上述 四點对途。

拷貝構(gòu)造函數(shù)

拷貝構(gòu)造函數(shù)的定義:有一個參數(shù)類型是其類類型的構(gòu)造函數(shù)。

X::X( const X& x);
Y::Y( const Y& y, int =0 );//可以是多參數(shù)形式髓棋,但其第二個即后繼參數(shù)都有一個默認值

當一個類對象以另一個同類實體作為初值時实檀,大部分情況下會調(diào)用拷貝構(gòu)造函數(shù)惶洲。 一般是這三種具體情況:顯式地以一個類對象作為另一個類對象的初值,形如X xx=x;當類對象被作為參數(shù)交給函數(shù)時膳犹;當函數(shù)返回一個類對象時恬吕。

編譯器何時合成拷貝構(gòu)造函數(shù)

并不是所有未定義有拷貝構(gòu)造函數(shù)的類編譯器都會為其合成拷貝構(gòu)造函數(shù),只有在編譯器在普通手段無法解決“一個類對象以另一個同類實體作為初值”這一問題時须床,編譯器才會合成 拷貝構(gòu)造函數(shù)铐料。如果一個類沒有定義拷貝構(gòu)造函數(shù),通常按照“成員逐一初始化(Default Memberwise Initialization)”的手法來解決這一問題——即把內(nèi)建或派生的數(shù)據(jù)成員從某一個對象拷貝到另一個對象身上豺旬,如果數(shù)據(jù)成員是一個對象钠惩,則遞歸使用“成員逐一初始化”的手法。

成員逐一初始化具體的實現(xiàn)方式則是位逐次拷貝(Bitwise copy semantics)哈垢。也就是在能使用這種常規(guī)方式來解決“一個類對象以另一個同類實體作為初值”的時候妻柒,編譯器是不需要合成拷貝構(gòu)造函數(shù)的扛拨。但有些時候常規(guī)武器不那么管用耘分,我們就得祭出非常規(guī)武器了 ——拷貝構(gòu)造函數(shù)。有以下幾種情況绑警,位逐次拷貝將不能勝任或者不適合來完成 “一個類對象以另一個同類實體作為初值”的工作求泰。此時,如果類沒有定義拷貝構(gòu)造函數(shù)计盒,那么編譯器將必須為類合成一個拷貝構(gòu)造函數(shù)渴频。

  • 當類內(nèi)含一個成員對象,而后者的類聲明有一個拷貝構(gòu)造函數(shù)時(不論是設(shè) 計者定義的還是編譯器合成的)北启。
  • 當類繼承自一個聲明有拷貝構(gòu)造函數(shù)的類時(不論這個拷貝構(gòu)造函數(shù) 是被顯示聲明還是由編譯器合成的)卜朗。
  • 類中聲明有虛函數(shù)。
  • 當類的派生串鏈中包含有一個或多個虛基類咕村。

對于前兩種情況场钉,不論是基類還是對象成員,既然后者聲明有拷貝構(gòu)造函數(shù)時懈涛, 就表明其類的設(shè)計者或者編譯器希望以其聲明的拷貝構(gòu)造函數(shù)來完成“一個類對象以另一個同類實體作為初值”的工作逛万,而設(shè)計者或編譯器這樣做——聲明拷貝構(gòu)造函 數(shù),總有它們的理由批钠,而通常最直接的原因是他們想要做一些額外的工作或“位逐次拷貝”無法勝任宇植。

對于有虛函數(shù)的類,如果兩個對象的類型相同那么位逐次拷貝其實是可以勝任的埋心。 但問題將出現(xiàn)在指郁,如果基類由其繼承類進行初始化時,此時若按照位逐次拷貝來 完成這個工作拷呆,那么基類的vptr將指向其繼承類的虛函數(shù)表闲坎,這將導(dǎo)致無法預(yù)料的后果——調(diào)用一個錯誤的虛函數(shù)實體是無法避免的,輕則帶來程序崩潰,更糟糕的問題可能是這個錯誤被隱藏了箫柳。所以對于有虛函數(shù)的類編譯器將會明確的使被初始化的對象的vptr指向正確的虛函數(shù)表手形。因此有虛函數(shù)的類沒有聲明拷貝構(gòu)造 函數(shù),編譯將為之合成一個悯恍,來完成上述工作库糠,以及初始化各數(shù)據(jù)成員,聲明有拷貝構(gòu)造函數(shù)的話也會被插入完成上述工作的代碼涮毫。

對于繼承串鏈中有虛基類的情況瞬欧,問題同樣出現(xiàn)在繼承類向基類提供初值的情況, 此時位逐次拷貝有可能破壞對象中虛基類子對象的位置罢防。

成員初始化列表

對于初始化列表艘虎,有一個概念是非常重要的:在構(gòu)造函數(shù)中對于對象成員的初始化發(fā)生在初始化列表中——或者我們可以把初始化列表直接看做是對成員的定義,而構(gòu)造函數(shù)體中進行的則是賦值操作咒吐。所以不難理解有四種情況必須用到初始化列表:

  • 有const成員
  • 有引用類型成員
  • 成員對象沒有默認構(gòu)造函數(shù)
  • 基類對象沒有默認構(gòu)造函數(shù)

前兩者因為要求定義時初始化野建,所以必須明確的在初始化隊列中給它們提供初值。后兩者因為不提供默認構(gòu)造函數(shù)恬叹,所以必須顯式調(diào)用它們的帶參構(gòu)造函數(shù)來定義即初始化它們候生。顯而易見的是當類中含有對象成員或者繼承自基類的時候,在初始化隊列中初始化成員對象和基類子對象會在效率上得到提升——省去了一些賦值操作嘛绽昼。


3. Data語意學(xué)

C++類對象的大小

猜猜下面幾個類sizeof的大小唯鸭。

class X{};
class Y:virtual public X{};
class Z:virtual public X{};
class A:public Y, public Z{};

32位系統(tǒng)下VS2010的運行結(jié)果為

sizeof X yielded 1 
sizeof Y yielded 4 
sizeof Z yielded 4 
sizeof Z yielded 8

事實上,對于像X這樣的一個的空類硅确,編譯器會對其動點手腳——隱晦的插入一個字節(jié)目溉。為 什么要這樣做呢?插入了這一個字節(jié)菱农,那么X的每一個對象都將有一個獨一無二的地址缭付。
再看看Y和Z,由于要實現(xiàn)虛繼承大莫,需要額外添加一個虛指針指向虛函數(shù)表蛉腌。到目前為止,對于一個32位的機器來說Y只厘、Z的大小應(yīng)該為5烙丛,而不 是8或者4。我們需要再考慮兩點因素:內(nèi)存對齊(memory alignment)和編譯器的優(yōu)化羔味。

對齊會將數(shù)值調(diào)整到某數(shù)的整數(shù)倍河咽,32位計算機上為4bytes。內(nèi)存對齊可以使得總線的運輸量達到最高效率赋元。所以Y忘蟹、Z的大小被補齊到8飒房。那么在vs2010中為什么Y、Z的大小是4而不是8呢媚值?我們先思考一個問題狠毯,X之所以被插入 1字節(jié)是因為本身為空,需要這一個字節(jié)為其在內(nèi)存中給它占領(lǐng)一個獨一無二的地址褥芒。但 是當這一字節(jié)被繼承到Y(jié)嚼松、Z后呢?它已經(jīng)完全失去了它存在的意義锰扶,為什么献酗?因為Y、Z各 自擁有一個虛基類指針坷牛,它們的大小不是0罕偎。既然這一字節(jié)在Y、Z中毫無意義京闰,那么就沒 必要留著颜及。也就是說vs2010對它們進行了優(yōu)化,優(yōu)化的結(jié)果是去掉了那一個字節(jié)忙干。

影響C++類的大小的三個因素:

  • 支持特殊功能所帶來的額外負擔(對各種virtual的支持)器予。
  • 編譯器對特殊情況的優(yōu)化處理浪藻。
  • 內(nèi)存對齊操作捐迫。

數(shù)據(jù)成員的布局與存取

參考第一章中的兩篇參考文獻。

在VC中數(shù)據(jù)成員的布局順序為:

  • vptr部分(如果基類有爱葵,則繼承基類的)
  • vbptr (如果需要)
  • 基類成員(按聲明順序)
  • 自身數(shù)據(jù)成員
  • 虛基類數(shù)據(jù)成員(按聲明順序)

4. 函數(shù)語意學(xué)

成員函數(shù)的調(diào)用

c++支持三種類型的成員函數(shù)施戴,分別為static,nostatic,virtual。每一種調(diào)用方式都不盡相同萌丈。

非靜態(tài)成員函數(shù)
為保證非靜態(tài)成員函數(shù)具有與普通函數(shù)調(diào)用相同的效率赞哗,編譯器內(nèi)部會將成員函數(shù)等價轉(zhuǎn)換為非成員函數(shù)。具體是這樣實現(xiàn)的:

  1. 改寫成員函數(shù)的簽名辆雾,使得其可以接受一個額外參數(shù)肪笋,this指針。如果成員函數(shù)是const的度迂,插入的參數(shù)類型將為 const Point* 類型藤乙。
float Point::X();
//成員函數(shù)X被插入額外參數(shù)this
float Point:: X(Point* this );
  1. 將每一個對非靜態(tài)數(shù)據(jù)成員的操作都改寫為經(jīng)過this操作。
  2. 將成員函數(shù)寫成一個外部函數(shù)惭墓,對函數(shù)名進行“mangling”處理坛梁,使之成為獨一無二的名稱。

關(guān)鍵在于為函數(shù)提供一個可直接讀寫成員數(shù)據(jù)的通道(this)腊凶,和對函數(shù)名進行修飾划咐,以免名字沖突拴念。

虛成員函數(shù)
如果function()是一個虛擬函數(shù),那么用指針或引用進行的調(diào)用將發(fā)生一點特別的轉(zhuǎn)換——一個中間層被引入進來褐缠。

例如:p->function()將轉(zhuǎn)化為(*p->vptr[1])(p)政鼠。其中vptr為指向虛函數(shù)表的指針,它由編譯器產(chǎn)生队魏。vptr也要進行名字處理缔俄,因為一個繼承體系可能有多個vptr。1是虛函數(shù)在虛函數(shù)表中的索引器躏,通過它關(guān)聯(lián)到虛函數(shù)function()俐载。

當通過指針調(diào)用的時候,要調(diào)用的函數(shù)實體無法在編譯期決定登失,必需待到執(zhí)行期才能獲得遏佣,所以上面引入一個間接層的轉(zhuǎn)換必不可少。但是當我們通過對象(不是引用揽浙,也不是指針)來調(diào)用的時候状婶, 進行上面的轉(zhuǎn)換就顯得多余了,因為在編譯器要調(diào)用的函數(shù)實體已經(jīng)被決定馅巷。此時調(diào)用發(fā)生的轉(zhuǎn)換膛虫,與一個非靜態(tài)成員函數(shù)調(diào)用發(fā)生的轉(zhuǎn)換一致。

靜態(tài)成員函數(shù)
靜態(tài)成員函數(shù)不能直接讀寫其類中的非靜態(tài)成員和調(diào)用非靜態(tài)成員函數(shù)钓猬,不能申明為const稍刀、voliatile或virtual,不需經(jīng)由對象調(diào)用(允許通過對象調(diào)用)敞曹。除了缺乏一個this指針他與非靜態(tài)成員函數(shù)沒有太大的差別账月。在這里通過對象調(diào)用和通過指針或引用調(diào)用,將被轉(zhuǎn)化為同樣的調(diào)用代碼澳迫。

虛成員函數(shù)

單繼承下的虛函數(shù)
虛函數(shù)的實現(xiàn)原理:編譯器為每個有虛函數(shù)的類配一張?zhí)摵瘮?shù)表局齿,用來存儲該類類型信息和所有虛函數(shù)執(zhí)行期的地址;并為每個有虛函數(shù)的類插入一個指針(vptr),這個指針指向該類的虛函數(shù)表橄登,最后 給每一個虛函數(shù)指派一個在表中的索引抓歼。

一個類的虛函數(shù)表中存儲有類型信息(VC2010中存儲在索引為-1的位置)和所有虛函數(shù)地址,這些虛函數(shù)地址包括三種:

  • 這個類定義的虛函數(shù)拢锹,會改寫(overriding)一個可能存在的基類的虛函數(shù)實體——假如基類也定義有這個虛函數(shù)谣妻。
  • 繼承自基類的虛函數(shù)實體,——基類定義有面褐,而這個類卻沒有定義拌禾。直接繼承之。
  • 一個純虛函數(shù)實體展哭。用來在虛函數(shù)表中占座湃窍,有時候也可以當做執(zhí)行期異常處理函數(shù)闻蛀。

當一個類單繼承自有虛函數(shù)的基類的時候,將按如下步驟構(gòu)建虛函數(shù)表:

  1. 繼承基類中聲明的虛函數(shù)——這些虛函數(shù)的實體地址被拷貝到繼承類中的虛函數(shù)表中對于的slot中您市。
  2. 如果有改寫(override)基類的虛函數(shù)觉痛,那么在1中應(yīng)將改寫(override)的函數(shù)實體的地址放入對應(yīng)的slot中而不是拷貝基類的。
  3. 如果有定義新的虛函數(shù)茵休,那么將虛函數(shù)表擴大一個slot以存放新的函數(shù)實體地址薪棒。

每一個虛函數(shù)都被指派一個固定的索引值,這個索引值在整個繼承體系中保持前后關(guān)聯(lián)榕莺,例如,假如z()在Point虛函數(shù)表中的索引值為2俐芯,那么在Point3d虛函數(shù)表中的索引值也為2。

多重繼承下的虛函數(shù)
在多重繼承下钉鸯,繼承類需要為每一條繼承線路維護一個虛函數(shù)表(也有可能這些表被合成為一個吧史,但本質(zhì)意義并沒有變化)。當然這一切都發(fā)生在需要的情況下唠雕。

當使用第一繼承的基類指針來調(diào)用繼承類的虛函數(shù)的時候贸营,與單繼承的情況沒有什么異樣,問題出生在當以第二或后繼的基類指針(或引用)的使用上岩睁。例如:

//假設(shè)有這樣的繼承關(guān)系:class Derived:public base1,public base2;
//base1,base2都定義有虛析構(gòu)函數(shù)钞脂。
base2 *ptr = new derived;
//需要被轉(zhuǎn)換為,這個轉(zhuǎn)換在編譯期完成
base2 *ptr = temp ? temp + sizeof(base1) : 0 ;

如果不做出上面的轉(zhuǎn)換捕儒,那么 ptr 指向的并不是 derived 的 base2 subobject 冰啃。后果是,ptr 將一個derived類型當做base2類型來用肋层。

當要delete ptr時又面臨了一次轉(zhuǎn)換亿笤,因為在delete ptr的時候,需要對整個對象而不是其子對象施行delete運算符栋猖,這期間需要調(diào)整ptr指向完整的對象起點,因為不論是調(diào)用正確的析構(gòu)函數(shù)還是delete運算符都需要一個指向?qū)ο笃瘘c的指針汪榔,想一想給予一個derived類的成員函數(shù)指向base2 subobjuect 的this指針會發(fā)生什么吧蒲拉。因為ptr的具體類型并不知道,所以必須要等到執(zhí)行期來完成痴腌。

多繼承下的虛函數(shù)雌团,影響到虛函數(shù)的調(diào)用的實際質(zhì)上為this的調(diào)整。而this調(diào)整一般為兩種:

  • 調(diào)整指針指向?qū)?yīng)的subobject士聪,一般發(fā)生在繼承類類型指針向基類類型指針賦值的情況下锦援。
  • 將指向subobject的指針調(diào)整回繼承類對象的起始點,一般發(fā)生在基類指針對繼承類虛函數(shù)進行調(diào)用的時候剥悟。

第一點灵寺,使得該基類指針指向一個與其指針類型匹配的子對象曼库,唯有如此才能保證使得該指針在執(zhí)行與其指針類型相匹配的特定行為的正確性。比方調(diào)用基類的成員略板,獲得正確的虛函數(shù)地址毁枯。可以想象如果不調(diào)整叮称,用ptr存取base2 subobject的數(shù)據(jù)成員時种玛,會發(fā)生什么?調(diào)用base2的成員函數(shù)的時候瓤檐,其成員函數(shù)接受的this指針指向derived 類型對象赂韵,這又會發(fā)生什么?結(jié)果是整個對象的內(nèi)存結(jié)構(gòu)有可能都被破壞挠蛉。還有別忘了右锨,vptr也可以看做一個數(shù)據(jù)成員,要找到虛函數(shù)碌秸,前提是獲取正確的vptr偏移量绍移。

而第二點,顯然是讓一個繼承類的虛函數(shù)獲取一個正確的this指針讥电,因為一個繼承類虛函數(shù)要的是一個指向繼承類對象的this指針蹂窖,而不是指向其子對象。

第一順序繼承類之所以不需要進行調(diào)整的關(guān)鍵在于恩敌,其subobject的起點與繼承類對象的起點一致瞬测。

5. 構(gòu)造、解構(gòu)纠炮、拷貝 語意學(xué)

幾點類的設(shè)計原則
1.即使是一個抽象基類月趟,如果它有非靜態(tài)數(shù)據(jù)成員,也應(yīng)該給它提供一 個帶參數(shù)的構(gòu)造函數(shù)恢口,來初始化它的數(shù)據(jù)成員孝宗。或許你可以通過其派生 類來初始化它的數(shù)據(jù)成員(假如nostatic data member為publish或 protected),但這樣做的后果則是破壞了數(shù)據(jù)的封裝性耕肩,使類的維護和修 改更加困難因妇。由此引申,類的data member應(yīng)當被初始化猿诸,且只在其構(gòu)造函 數(shù)或其member function中初始化婚被。

2.不要將析構(gòu)函數(shù)設(shè)計為純虛的,這不是一個好的設(shè)計梳虽。將析構(gòu)函數(shù) 設(shè)計為純虛函數(shù)意味著址芯,即使純虛函數(shù)在語法上允許我們只聲明而不定義 純虛函數(shù),但還是必須實現(xiàn)該純虛析構(gòu)函數(shù),否則它所有的繼承類都將遇 到鏈接錯誤谷炸。一個不能派生繼承類的抽象類有什么存在的意義北专?必須定義 純虛析構(gòu)函數(shù),而不能僅僅聲明它的原因在于:每一個繼承類的析構(gòu)函數(shù) 會被編譯器加以擴展淑廊,以靜態(tài)調(diào)用方式其每一個基類的析構(gòu)函數(shù)(假如有 的話逗余,不論是顯示的還是編譯器合成的),所以只要任何一個基類的析構(gòu) 函數(shù)缺乏定義季惩,就會導(dǎo)致鏈接失敗录粱。矛盾就在這里,純虛函數(shù)的語法画拾,允 許只聲明而不定義純虛析構(gòu)函數(shù)啥繁,而編譯器則死腦筋的看到一個其基類的 析構(gòu)函數(shù)聲明,則去調(diào)用它的實體青抛,而不管它有沒有被定義旗闽。

3.真的必要的時候才使用虛函數(shù),不要濫用虛函數(shù)蜜另。虛函數(shù)意味著不 小的成本适室,編譯很可能給你的類帶來膨脹效應(yīng):
每一個對象要多負擔一個word的vptr。
給每一個構(gòu)造函數(shù)(不論是顯示的還是編譯器合成的)举瑰,插入一些代 碼來初始化vptr捣辆,這些代碼必須被放在所有基類構(gòu)造函數(shù)的調(diào)用之后, 但需在任意用戶代碼之前此迅。沒有構(gòu)造函數(shù)則需要合成汽畴,并插入代碼。
合成一個拷貝構(gòu)造函數(shù)和一個復(fù)制操作符(如果沒有的話)耸序,并插入 對vptr的初始化代碼忍些,有的話也需要插入vptr的初始化代碼。
意味著坎怪,如果具有bitwise語意罢坝,將不再具有,然后是變大的對象芋忿、沒 有那么高效的構(gòu)造函數(shù)炸客,沒有那么高效的復(fù)制控制。

4.不能決定一個虛函數(shù)是否需要 const ,那么就不要它。

5.決不在構(gòu)造函數(shù)或析構(gòu)函數(shù)中使用虛函數(shù)機制拜姿。在構(gòu)造函數(shù)中互躬,每次 調(diào)用虛函數(shù)會被決議為當前構(gòu)造函數(shù)所對應(yīng)類的虛函數(shù)實體,虛函數(shù)機制并 不起作用拟枚。當一個base類的構(gòu)造函數(shù)含有對虛函數(shù)vf()的調(diào)用薪铜,當其派生類 derived的構(gòu)造函數(shù)調(diào)用基類base的構(gòu)造函數(shù)的時候众弓,其中調(diào)用的虛函數(shù)vf() 是base中的實體,而不是derived中的實體隔箍。這是由vptr初始化的位置決定的 ——在所有基類構(gòu)造函數(shù)調(diào)用之后谓娃,在程序員供應(yīng)的代碼或是成員初始化隊列 之前。因構(gòu)造函數(shù)的調(diào)用順序是:有根源到末端蜒滩,由內(nèi)而外滨达,所以對象的構(gòu) 造過程可以看成是,從構(gòu)建一個最基礎(chǔ)的對象開始俯艰,一步步構(gòu)建成一個目標 對象捡遍。析構(gòu)函數(shù)則有著與構(gòu)造相反的順序,因此在構(gòu)造或析構(gòu)函數(shù)中使用虛 函數(shù)機制竹握,往往不是程序員的意圖画株。若要在構(gòu)造函數(shù)或析構(gòu)函數(shù)中調(diào)用虛函 數(shù),應(yīng)當直接以靜態(tài)方式調(diào)用啦辐,而不要通過虛函數(shù)機制谓传。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市芹关,隨后出現(xiàn)的幾起案子续挟,更是在濱河造成了極大的恐慌,老刑警劉巖充边,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件庸推,死亡現(xiàn)場離奇詭異,居然都是意外死亡浇冰,警方通過查閱死者的電腦和手機贬媒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肘习,“玉大人际乘,你說我怎么就攤上這事∑澹” “怎么了脖含?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長投蝉。 經(jīng)常有香客問我养葵,道長,這世上最難降的妖魔是什么瘩缆? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任关拒,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘着绊。我一直安慰自己谐算,他們只是感情好,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布归露。 她就那樣靜靜地躺著洲脂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪剧包。 梳的紋絲不亂的頭發(fā)上恐锦,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音玄捕,去河邊找鬼踩蔚。 笑死,一個胖子當著我的面吹牛枚粘,可吹牛的內(nèi)容都是我干的馅闽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼馍迄,長吁一口氣:“原來是場噩夢啊……” “哼福也!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起攀圈,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤暴凑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后赘来,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體现喳,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年犬辰,在試婚紗的時候發(fā)現(xiàn)自己被綠了嗦篱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡幌缝,死狀恐怖灸促,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情涵卵,我是刑警寧澤浴栽,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站轿偎,受9級特大地震影響典鸡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜坏晦,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一椿每、第九天 我趴在偏房一處隱蔽的房頂上張望伊者。 院中可真熱鬧英遭,春花似錦间护、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至多律,卻和暖如春痴突,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狼荞。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工辽装, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人相味。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓拾积,卻偏偏與公主長得像,于是被迫代替她去往敵國和親丰涉。 傳聞我的和親對象是個殘疾皇子拓巧,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容