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