有兩個概念可以解釋C++對象模型 1.語言中直接支持面向對象程序設計的部分 包括了構造函數(shù)、析構函數(shù)、多態(tài)、虛函數(shù)等等. 2.對于各種支持的底層實現(xiàn)機制 對象模型研究的是對象在存儲上的空間與時間上的優(yōu)化,并對C++面向對象技術加以支持,如以虛指針、虛表機制支持多態(tài)機制.
C++中虛函數(shù)的作用主要是為了實現(xiàn)多態(tài)機制,多臺,簡單的來說,是指在繼承層次中,父類的指針可以具有多種形態(tài)——當它指向某個子類對象時,通過它能夠調用子類的函數(shù),而非父類的函數(shù)
class Base { virtual void PRint(void);}class Drive1:public Base{virtual void print(void);}class Drive2:public Base{vittual void print(void);}Base * ptr1 = new Base;Base *ptr2 = new Driver1;Base *ptr3 = new Driver2;ptr1->print();//調用Base::print()ptr2->print();//調用Drive1::print()ptr3->print();//調用Driver2::print()這是一種運行期多態(tài),即父類指針唯有在程序運行時才能知道所指的真正類型是什么,這種運行期決議,是通過虛函數(shù)表來實現(xiàn)的.
輸出一詳解:我們強行把類對象的地址轉換為int* 類型,取得了虛函數(shù)指針的地址.虛函數(shù)指針指向虛函數(shù)表,虛函數(shù)表中存儲的是一系列虛函數(shù)的地址,虛函數(shù)地址出現(xiàn)的順序與類中虛函數(shù)聲明的順序一致,對虛函數(shù)指針地址值解引用,可以得到虛函數(shù)表的地址,也即是虛函數(shù)表的第一個虛函數(shù)的地址: 輸出二詳解:
我們把虛表指針的值取出來:(int )(&b);它是一個地址,虛函數(shù)表的地址.把虛函數(shù)表的地址強制轉換成int * :(int )(int *)(&b)再把它轉換成我們Func指針類型:(Fun )(int ) * (int *)(&b);在C++中,有兩種數(shù)據(jù)成員,static和nonstatic,以及三種類成員函數(shù):static、nonstatic 和virtual;
現(xiàn)在我們有一個類Base,它包含了上面這5種類型的數(shù)據(jù)或函數(shù)
概述:在此模型下,nonstatic數(shù)據(jù)成員被置于每一個類對象中,而static數(shù)據(jù)成員被置于類對象之外。static與nonstatic函數(shù)也都放在類對象之外.而對于虛函數(shù),則通過虛函數(shù)表+虛函數(shù)指針來支持,具體如下:
每一個類生成一個表格,稱為虛表,虛表中存放著一堆指針,這些指針指向該類每一個虛函數(shù),虛表中的函數(shù)地址按聲明時的順序排列,不過當子類中有多個重載函數(shù)時例外,后面會討論. 每個類對象都擁有一個虛表指針(vptr),由編譯器為其生成,虛表指針的設定與重置皆由類的復制控制(也即是構造函數(shù)、析構函數(shù)、賦值操作符)來完成,vptr的位置由編譯器來決定,許多編譯器把vptr放在一個類對象的最前端。關于數(shù)據(jù)成員布局的內(nèi)容,在后面會詳細分析。另外,虛函數(shù)表的前面設置了一個指向type_info的指針,用以支持RTTI(Run Time Type Identification,運行時類型識別)。RTTI是為多態(tài)而生成的信息,包括對象繼承關系,對象本身的描述等,只有具有虛函數(shù)的對象在會生成。在此模型下,Base對象的對象模型如下: 
在C++對象模型中,對于一般繼承(這個是相對于虛擬繼承而言),若子類重寫了父類的虛函數(shù),則子類虛函數(shù)將覆蓋虛表中對應父類虛函數(shù)(注意子類和父類擁有各自的一個虛函數(shù)表);若子類并無overwrite父類虛函數(shù),而是聲明了自己的新的虛函數(shù),則該虛函數(shù)地址將擴充到虛函數(shù)表最后,而對于虛繼承,若子類overwrite父類虛函數(shù)m同樣地將覆蓋父類子物體中虛函數(shù)表對應位置,若子類聲明了自己的新的虛函數(shù),則編譯器為其子類增加一個新的虛表指針vptr. 
單繼承中(一般繼承),子類會擴展父類的虛函數(shù)表,在多繼承中,子類含有多個父類的子對象,該往哪個父類的虛函數(shù)表擴展呢?當子類overwrite了父類的函數(shù),需要覆蓋多個父類的虛函數(shù)表嗎?
子類的虛函數(shù)被放在聲明的第一個基類的虛函數(shù)表中.overwrite時,所有基類的print()函數(shù)都被子類的print()函數(shù)覆蓋.內(nèi)存布局中,父類按照其聲明順序排列. class Base{public: Base(int i) :baseI(i){}; virtual ~Base(){} int getI(){ return baseI; } static void countI(){}; virtual void print(void){ cout << "Base::print()"; }private: int baseI; static int baseS;};class Base_2{public: Base_2(int i) :base2I(i){}; virtual ~Base_2(){} int getI(){ return base2I; } static void countI(){}; virtual void print(void){ cout << "Base_2::print()"; }private: int base2I; static int base2S;};class Drive_multyBase :public Base, public Base_2{public: Drive_multyBase(int d) :Base(1000), Base_2(2000) ,Drive_multyBaseI(d){}; virtual void print(void){ cout << "Drive_multyBase::print" ; } virtual void Drive_print(){ cout << "Drive_multyBase::Drive_print" ; }private: int Drive_multyBaseI;};此時Drive_multyBase的對象模型是這樣的: 
菱形繼承也稱為重復繼承,它指的是基類被某個派生類簡單重復繼承了多次,這樣,派生類對象中擁有多份基類實例:看代碼:
class B{public: int ib;public: B(int i=1) :ib(i){} virtual void f() { cout << "B::f()" << endl; } virtual void Bf() { cout << "B::Bf()" << endl; }};class B1 : public B{public: int ib1;public: B1(int i = 100 ) :ib1(i) {} 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;public: B2(int i = 1000) :ib2(i) {} 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;public: D(int i= 10000) :id(i){} 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; }};我們根據(jù)單繼承,我們可以分析出B1,B2類繼承B類時的內(nèi)存布局,又根據(jù)一般多繼承,我們可以分析D類的內(nèi)存布局:
D類對象內(nèi)存布局,圖中綠色表示b1類子對象實例,藍色表示的是b2類子對象實例,紅色表示的是D類子對象實例,從圖中可以看到,由于D類間接繼承了B類兩次,導致D類對象中含有兩個B類的數(shù)據(jù)成員ib,一個屬于來源B1類,一個來源B2類。這樣不僅增大了空間,更重要的是引起了程序歧義:
盡管我們可以通過明確指明調用路徑以消除二義性,但二義性的潛在性還沒有消除,我們可以通過虛繼承來使D類只擁有一個ib實體。
虛繼承解決了菱形繼承中派生類擁有多個間接父類實例的情況,虛繼承中派生類的內(nèi)存布局與普通繼承有很多不同,主要體現(xiàn)在:
虛繼承的子類,如果本身定義新的虛函數(shù),則編譯器會為其生成一個虛函數(shù)指針(vptr)以及一張?zhí)摵瘮?shù)表,該vptr位于對象內(nèi)存最前面. 而非虛繼承,直接擴展父類的虛函數(shù)表.虛繼承的子類單獨保留了父類的vptr與虛函數(shù)表,這部分內(nèi)容與子類內(nèi)容以一個四字節(jié)的0來分界.虛繼承的子類對象中,含有四字節(jié)的虛表指針偏移值.在C++模型中,虛繼承而來的子類會生成一個隱藏的虛基類指針, 虛基類表指針總是在虛函數(shù)表指針之后 因而,對某個類實例來說,如果它有虛基類指針,那么虛基類指針可能在實例的0字節(jié)偏移處(該類沒有vptr時,vbptr就處于類實例內(nèi)存布局的最前面,否則vptr處于類實例內(nèi)存布局的最前面),也可能在類實例的4字節(jié)偏移處。 一個類的虛基類指針指向的虛基類表,與虛函數(shù)一樣,虛基類表也由多個條目組成,條目中存放的是偏移值,第一個條目存放虛基類表指針(vbptr)所在地址到該類內(nèi)存首地址的偏移值,由第一段的分析我們知道,這個偏移值為0(類沒有vptr)或者-4(類有虛函數(shù),此時有vptr),我們通過一張圖來更好的理解.
虛基類表的第二、第三…個條目依次為該類的最左虛繼承父類、次左虛繼承父類…的內(nèi)存地址相對于虛基類表指針的偏移值,這點我們在下面會驗證。
根據(jù)我們前面對虛繼承的派生類的內(nèi)存布局的分析,B1類的對象模型應該是這樣的 
菱形虛擬繼承下,派生類D類的對象模型又有不同的構成的,在D類對象的內(nèi)存構成上,有以下幾點:
在D類對象內(nèi)存中,基類出現(xiàn)的順序是:先是B1(最左父類),然后是B2(次左父類),最后是B(虛祖父類)D類對象的數(shù)據(jù)成員id放在B類前面,兩部分數(shù)據(jù)依舊以0來分隔編譯器沒有為D類生成一個它自己的vptr,而是覆蓋并擴展了最左父類的虛基類表,與簡單繼承的對象模型相同.超類B的內(nèi)容放到了D類對象內(nèi)存布局的最后。 菱形虛擬繼承下的C++對象模型為:
解析: * 編譯器為空類安插1字節(jié)的char,以使該類對象在內(nèi)存配置一個地址。 * b1虛繼承b,編譯器為其安插8字節(jié)的虛基類表指針,此時b1已不為空,編譯器不再為其安插1字節(jié)的char. * b2 同理. * d含有來自b1和b2兩個父類的虛基類表指針,大小為16字節(jié).
新聞熱點
疑難解答