原文鏈接:吳秦大神的C++對象模型,。 何為C++對象模型?C++對象模型可以概括為以下2部分: 1,、語言中直接支持面向?qū)ο蟪绦蛟O(shè)計的部分,; 2、對于各種支持的底層實現(xiàn)機制,。 語言中直接支持面向?qū)ο蟪绦蛟O(shè)計的部分,,如構(gòu)造函數(shù)、析構(gòu)函數(shù),、虛函數(shù),、繼承(單繼承、多繼承,、虛繼承),、多態(tài)等等。本文重點介紹底層實現(xiàn)機制,。 在C語言中,,“數(shù)據(jù)”和“處理數(shù)據(jù)的操作(函數(shù))”是分開聲明的,也就是說,,語言本身并沒有支持“數(shù)據(jù)和函數(shù)”之間的關(guān)聯(lián)性,。在C++中,通過抽象數(shù)據(jù)類型(Abstract Data Type,,ADT),,在類中定義數(shù)據(jù)和函數(shù),來實現(xiàn)數(shù)據(jù)和函數(shù)直接的綁定,。概括來說,,在C++類中有兩種數(shù)據(jù)成員:static,nonstatic,;三種成員函數(shù):static,、nonstatic,、virtual。 如下面的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類在機器中我們?nèi)绾螛?gòu)建出各種成員數(shù)據(jù)和成員函數(shù)的呢,? 基本C++對象模型在介紹C++使用的對象模型之前,,介紹2種對象模型:簡單對象模型(A Simple Object Model)、表格驅(qū)動對象模型(A Table-Drive Object Model),。 簡單對象模型(a simple object model)
所有的成員占用相同的空間(跟成員類型無關(guān)),,對象只是維護了一個包含成員指針的一個表。表中放的是成員的地址,,無論是成員變量還是函數(shù),,都是同樣處理。對象并沒有直接保存成員而只是保存了成員的指針,。 表格對象模型(a table-driven object model)
這個模型在簡單對象的基礎(chǔ)上又添加了一個間接層,。將函數(shù)和數(shù)據(jù)分別存儲在兩個表中,并保存了兩個指向表格的指針,。這個模型可以保證所有的對象具有相同的大小,,比如簡單對象模型還與成員的個數(shù)有關(guān)。其中數(shù)據(jù)成員表中包含實際數(shù)據(jù),;函數(shù)成員表中包含實際函數(shù)的地址(與數(shù)據(jù)成員相比,,多一次尋址)。 C++對象模型
這個模型結(jié)合了上面兩個模型的特點,,并對內(nèi)存存取和空間進行了優(yōu)化,。在此模型中,,nonstatic數(shù)據(jù)成員被放置到對象內(nèi)部,,static數(shù)據(jù)成員、static和nonstatic函數(shù)成員軍備放到對象之外,。對于虛函數(shù)的支持則分兩部分完成: 1,、每一個class產(chǎn)生一堆指向虛函數(shù)的指針,并存放在虛函數(shù)表中(Virtual Table,,vtbl),; 2、每個對象被添加了一個指針,,指向相關(guān)的虛函數(shù)表vtbl,。通常這個指針被稱為vptr。vptr的設(shè)定和重置都由每一個class的構(gòu)造函數(shù),,析構(gòu)函數(shù)和拷貝賦值運算符自動完成,。 另外,虛函數(shù)表地址的前面設(shè)置了一個指向type_info的指針,,RTTI(Run Time Type Identification)運行時類型識別是由編譯器在編譯時生成的特殊類型信息,,包括對象繼承關(guān)系,,對象本身的描述。RTTI是為多態(tài)而生成的信息,,所以只有具有虛函數(shù)的對象才會生成,。 這個模型的優(yōu)點在于它的空間和存取時間的效率;缺點如下:如果應(yīng)用程序本身未改變,,當所使用的類的nonstatic數(shù)據(jù)成員添加刪除或修改時,,需要重新編譯。 模型驗證測試為了驗證上述C++對象模型,,test_base_model函數(shù): void test_base_model() { Base b1(1000); cout << "對象b1的起始內(nèi)存地址:" << &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 << "虛函數(shù)表地址:\t\t\t" << (int*)(&b1) << endl; cout << "虛函數(shù)表 — 第1個函數(shù)地址:\t" << (int*)*(int*)(&b1) << "\t即析構(gòu)函數(shù)地址:" << (int*)*((int*)*(int*)(&b1)) << endl; cout << "虛函數(shù)表 — 第2個函數(shù)地址:\t" << ((int*)*(int*)(&b1) + 1) << "\n"; typedef void(*Fun)(void); Fun pFun = (Fun)*(((int*)*(int*)(&b1)) + 1); pFun(); b1.print(); cout << endl; cout << "推測數(shù)據(jù)成員iBase地址:\t\t" << ((int*)(&b1) +1) << "\t通過地址取值iBase的值:" << *((int*)(&b1) +1) << endl; cout << "Base::getIBase(): " << b1.getIBase() << endl; b1.instanceCount(); cout << "靜態(tài)函數(shù)instanceCount地址: " << b1.instanceCount << endl; } 根據(jù)C++對象模型,,實例化對象b1的起始內(nèi)存地址,即虛函數(shù)表地址,。 虛函數(shù)表中的第一個函數(shù)地址是虛析構(gòu)函數(shù)的地址,,即(int *)*(int *)(&b1); type_info的地址,,等于第一個函數(shù)地址減一,,即((int *)*(int *)(&b1)-1); 虛函數(shù)表中的第二個函數(shù)地址是虛函數(shù)print()的地址,,通過函數(shù)指針可以調(diào)用,,進行驗證: cout << "虛函數(shù)表 — 第2個函數(shù)地址:\t" << ((int*)*(int*)(&b1) + 1) << "\n"; typedef void(*Fun)(void); Fun pFun = (Fun)*(((int*)*(int*)(&b1)) + 1); pFun(); b1.print(); 推測數(shù)據(jù)成員IBase的地址,即為虛函數(shù)表的地址+1,,((int *)(&b)+1),; 靜態(tài)數(shù)據(jù)成員和靜態(tài)函數(shù)所在的內(nèi)存地址,與數(shù)據(jù)成員和函數(shù)成員位段不一樣,。 運行結(jié)果: 注意:本測試代碼及后面的測試代碼中寫的函數(shù)地址,,是對應(yīng)虛函數(shù)表項的地址,不是實際的函數(shù)地址,。
圖:vs斷點觀察(注意看虛函數(shù)表中第一個函數(shù)的地址,,名稱與測試代碼輸出一致) 上面介紹并驗證了基本的C++對象模型,引入繼承之后,,C++對象模型又是怎樣的,? C++對象模型中加入單繼承不管是單繼承、多繼承,,還是虛繼承,,如果基于“簡單對象模型”,每一個基類都可以被派生類中的一個slot指出,,該slot內(nèi)包含基類對象的地址,。這個機制的主要缺點是,因為間接性而導致空間和存取時間上的額外負擔;優(yōu)點則是派生類對象的大小不會因其基類的改變而受影響,。 如果基于“表格驅(qū)動模型”,,派生類中有一個slot指向基類表,表格中的每一個slot含一個相關(guān)的基類地址(這個很像虛函數(shù)表,,內(nèi)含每一個虛函數(shù)的地址),。這樣每個派生類對象都有一個bptr,它會被初始化,,指向其基類表,。這種策略的主要缺點是由于間接性而導致的空間和存取時間上的額外負擔;優(yōu)點則是在每一個派生類對象中對繼承都有一致的表現(xiàn)方式,,每一個派生類對象都應(yīng)該在某個固定位置上放置一個基類表指針,,與基類的大小或數(shù)量無關(guān)。第二個優(yōu)點是,,不需要改變派生類對象本身,,就可以放大,縮小,、或更改基類表,。 不管上述哪一種機制,“間接性”的級數(shù)都將因為集成的深度而增加,。C++實際模型是,,對于一般繼承是擴充已有存在的虛函數(shù)表;對于虛繼承添加一個虛函數(shù)表指針,。 無重寫的單繼承無重寫,,即派生類中沒有于基類同名的虛函數(shù)。 #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不是虛繼承,,所以是擴充已存在的虛函數(shù)表,,所以結(jié)構(gòu)如下圖所示:
驗證上述C++對象模型,test_single_norewrite(): void test_single_inherit_norewrite() { Derived d(9999); cout << "對象d的起始內(nèi)存地址:" << &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 << "虛函數(shù)表地址:\t\t\t" << (int*)(&d) << endl; cout << "虛函數(shù)表 — 第1個函數(shù)地址:\t" << (int*)*(int*)(&d) << "\t即析構(gòu)函數(shù)地址" << endl; cout << "虛函數(shù)表 — 第2個函數(shù)地址:\t" << ((int*)*(int*)(&d) + 1) << "\t"; typedef void(*Fun)(void); Fun pFun = (Fun)*(((int*)*(int*)(&d)) + 1); pFun(); d.print(); cout << endl; cout << "虛函數(shù)表 — 第3個函數(shù)地址:\t" << ((int*)*(int*)(&d) + 2) << "\t"; pFun = (Fun)*(((int*)*(int*)(&d)) + 2); pFun(); d.derived_print(); cout << endl; cout << "推測數(shù)據(jù)成員iBase地址:\t\t" << ((int*)(&d) +1) << "\t通過地址取得的值:" << *((int*)(&d) +1) << endl; cout << "推測數(shù)據(jù)成員iDerived地址:\t" << ((int*)(&d) +2) << "\t通過地址取得的值:" << *((int*)(&d) +2) << endl; } 輸出結(jié)果如下圖所示:
有重寫的單繼承派生類中重寫了基類的print()函數(shù),。 #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()函數(shù)在虛函數(shù)表中表現(xiàn)如下:
驗證上述C++對象模型,,test_single_inherit_rewrite(): void test_single_inherit_rewrite() { Derived_Overrite d(111111); cout << "對象d的起始內(nèi)存地址:\t\t" << &d << endl; cout << "虛函數(shù)表地址:\t\t\t" << (int*)(&d) << endl; cout << "虛函數(shù)表 — 第1個函數(shù)地址:\t" << (int*)*(int*)(&d) << "\t即析構(gòu)函數(shù)地址" << endl; cout << "虛函數(shù)表 — 第2個函數(shù)地址:\t" << ((int*)*(int*)(&d) + 1) << "\t"; typedef void(*Fun)(void); Fun pFun = (Fun)*(((int*)*(int*)(&d)) + 1); pFun(); d.print(); cout << endl; cout << "虛函數(shù)表 — 第3個函數(shù)地址:\t" << *((int*)*(int*)(&d) + 2) << "【結(jié)束】\t"; cout << endl; cout << "推測數(shù)據(jù)成員iBase地址:\t\t" << ((int*)(&d) +1) << "\t通過地址取得的值:" << *((int*)(&d) +1) << endl; cout << "推測數(shù)據(jù)成員iDerived地址:\t" << ((int*)(&d) +2) << "\t通過地址取得的值:" << *((int*)(&d) +2) << endl; } 輸出結(jié)果如下圖所示:
特別注意下,,前面的模型虛函數(shù)表中最后一項沒有打印出來,本實例中共2個虛函數(shù),,打印虛函數(shù)表第3項為0,。其實虛函數(shù)表以0x0000000結(jié)束,類似字符串以’\0’結(jié)束。 C++對象模型中加入多繼承從單繼承可以知道,,派生類中只是擴充了基類的虛函數(shù)表,。如果是多繼承的話,又是如何擴充的,?
上面3個類,Derived_Mutlip_Inherit繼承自Base,、Base_1兩個類,,Derived_Mutlip_Inherit的結(jié)構(gòu)如下所示:
為了驗證上述C++對象模型,我們編寫如下測試代碼,。 void test_multip_inherit() { Derived_Mutlip_Inherit dmi(3333); cout << "對象dmi的起始內(nèi)存地址:\t\t" << &dmi << endl; cout << "虛函數(shù)表_vptr_Base地址:\t" << (int*)(&dmi) << endl; cout << "_vptr_Base — 第1個函數(shù)地址:\t" << (int*)*(int*)(&dmi) << "\t即析構(gòu)函數(shù)地址" << endl; cout << "_vptr_Base — 第2個函數(shù)地址:\t" << ((int*)*(int*)(&dmi) + 1) << "\t"; typedef void(*Fun)(void); Fun pFun = (Fun)*(((int*)*(int*)(&dmi)) + 1); pFun(); cout << endl; cout << "_vptr_Base — 第3個函數(shù)地址:\t" << ((int*)*(int*)(&dmi) + 2) << "\t"; pFun = (Fun)*(((int*)*(int*)(&dmi)) + 2); pFun(); cout << endl; cout << "_vptr_Base — 第4個函數(shù)地址:\t" << *((int*)*(int*)(&dmi) + 3) << "【結(jié)束】\t"; cout << endl; cout << "推測數(shù)據(jù)成員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 << "虛函數(shù)表_vptr_Base1地址:\t" << ((int*)(&dmi) +2) << endl; cout << "_vptr_Base1 — 第1個函數(shù)地址:\t" << (int*)*((int*)(&dmi) +2) << "\t即析構(gòu)函數(shù)地址" << endl; cout << "_vptr_Base1 — 第2個函數(shù)地址:\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個函數(shù)地址:\t" << *((int*)*(int*)((int*)(&dmi) +2) + 2) << "【結(jié)束】\t"; cout << endl; cout << "推測數(shù)據(jù)成員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 << "推測數(shù)據(jù)成員iDerived地址:\t" << ((int*)(&dmi) +4) << "\t通過地址取得的值:" << *((int*)(&dmi) +4) << endl; } 輸出結(jié)果如下圖所示:
C++對象模型中加入虛繼承虛繼承是為了解決重復(fù)繼承中多個間接父類的問題的,,所以不能使用上面簡單的擴充并為每個虛基類提供一個虛函數(shù)指針(這樣會導致重復(fù)繼承的基類會有多個虛函數(shù)表)形式。 虛繼承的派生類的內(nèi)存結(jié)構(gòu),,和普通繼承完全不同,。虛繼承的子類,有單獨的虛函數(shù)表,,另外也單獨保存一份父類的虛函數(shù)表,,兩部分之間用一個四個字節(jié)的0x00000000來作為分界。派生類的內(nèi)存中,,首先是自己的虛函數(shù)表,,然后是派生類的數(shù)據(jù)成員,然后是0x0,,之后就是基類的虛函數(shù)表,,之后是基類的數(shù)據(jù)成員。 如果派生類沒有自己的虛函數(shù),,那么派生類就不會有虛函數(shù)表,,但是派生類數(shù)據(jù)和基類數(shù)據(jù)之間,還是需要0x0來間隔,。 因此,,在虛繼承中,派生類和基類的數(shù)據(jù),,是完全間隔的,,先存放派生類自己的虛函數(shù)表和數(shù)據(jù),,中間以0x分界,最后保存基類的虛函數(shù)和數(shù)據(jù),。如果派生類重載了父類的虛函數(shù),,那么則將派生類內(nèi)存中基類虛函數(shù)表的相應(yīng)函數(shù)替換 簡單虛繼承(無重復(fù)繼承情況)簡單虛繼承的2個類Base、Derived_Virtual_Inherit1的關(guān)系如下所示:
Derived_Virtual_Inherit1的對象模型如下圖:
為了驗證上述C++對象模型,,我們編寫如下測試代碼,。 void test_single_vitrual_inherit() { Derived_Virtual_Inherit1 dvi1(88888); cout << "對象dvi1的起始內(nèi)存地址:\t\t" << &dvi1 << endl; cout << "虛函數(shù)表_vptr_Derived..地址:\t\t" << (int*)(&dvi1) << endl; cout << "_vptr_Derived — 第1個函數(shù)地址:\t" << (int*)*(int*)(&dvi1) << endl; typedef void(*Fun)(void); Fun pFun = (Fun)*((int*)*(int*)(&dvi1)); pFun(); cout << endl; cout << "_vptr_Derived — 第2個函數(shù)地址:\t" << *((int*)*(int*)(&dvi1) + 1) << "【結(jié)束】\t"; cout << endl; cout << "=======================:\t" << ((int*)(&dvi1) +1) << "\t通過地址取得的值:" << (int*)*((int*)(&dvi1) +1) << "\t" <<*(int*)*((int*)(&dvi1) +1) << endl; cout << "推測數(shù)據(jù)成員iDerived地址:\t" << ((int*)(&dvi1) +2) << "\t通過地址取得的值:" << *((int*)(&dvi1) +2) << endl; cout << "=======================:\t" << ((int*)(&dvi1) +3) << "\t通過地址取得的值:" << *((int*)(&dvi1) +3) << endl; cout << "虛函數(shù)表_vptr_Base地址:\t" << ((int*)(&dvi1) +4) << endl; cout << "_vptr_Base — 第1個函數(shù)地址:\t" << (int*)*((int*)(&dvi1) +4) << "\t即析構(gòu)函數(shù)地址" << endl; cout << "_vptr_Base — 第2個函數(shù)地址:\t" << ((int*)*((int*)(&dvi1) +4) +1) << "\t"; pFun = (Fun)*((int*)*((int*)(&dvi1) +4) +1); pFun(); cout << endl; cout << "_vptr_Base — 第3個函數(shù)地址:\t" << ((int*)*((int*)(&dvi1) +4) +2) << "【結(jié)束】\t" << *((int*)*((int*)(&dvi1) +4) +2); cout << endl; cout << "推測數(shù)據(jù)成員iBase地址:\t\t" << ((int*)(&dvi1) +5) << "\t通過地址取得的值:" << *((int*)(&dvi1) +5) << endl; } 輸出結(jié)果如下圖所示: 菱形繼承(含重復(fù)繼承、多繼承情況)菱形繼承關(guān)系如下圖:
Derived_Virtual的對象模型如下圖:
為了驗證上述C++對象模型,,我們編寫如下測試代碼,。 void test_multip_vitrual_inherit() { Derived_Virtual dvi1(88888); cout << "對象dvi1的起始內(nèi)存地址:\t\t" << &dvi1 << endl; cout << "虛函數(shù)表_vptr_inherit1地址:\t\t" << (int*)(&dvi1) << endl; cout << "_vptr_inherit1 — 第1個函數(shù)地址:\t" << (int*)*(int*)(&dvi1) << endl; typedef void(*Fun)(void); Fun pFun = (Fun)*((int*)*(int*)(&dvi1)); pFun(); cout << endl; cout << "_vptr_inherit1 — 第2個函數(shù)地址:\t" << ((int*)*(int*)(&dvi1) + 1) << endl; pFun = (Fun)*((int*)*(int*)(&dvi1) + 1); pFun(); cout << endl; cout << "_vptr_inherit1 — 第3個函數(shù)地址:\t" << ((int*)*(int*)(&dvi1) + 2) << "\t通過地址取得的值:" << *((int*)*(int*)(&dvi1) + 2) << "【結(jié)束】\t"; cout << endl; cout << "======指向=============:\t" << ((int*)(&dvi1) +1) << "\t通過地址取得的值:" << (int*)*((int*)(&dvi1) +1)<< "\t" <<*(int*)*((int*)(&dvi1) +1) << endl; cout << "推測數(shù)據(jù)成員iInherit1地址:\t" << ((int*)(&dvi1) +2) << "\t通過地址取得的值:" << *((int*)(&dvi1) +2) << endl; // cout << "虛函數(shù)表_vptr_inherit2地址:\t" << ((int*)(&dvi1) +3) << endl; cout << "_vptr_inherit2 — 第1個函數(shù)地址:\t" << (int*)*((int*)(&dvi1) +3) << endl; pFun = (Fun)*((int*)*((int*)(&dvi1) +3)); pFun(); cout << endl; cout << "_vptr_inherit2 — 第2個函數(shù)地址:\t" << (int*)*((int*)(&dvi1) +3) + 1 <<"\t通過地址取得的值:" << *((int*)*((int*)(&dvi1) +3) + 1) << "【結(jié)束】\t" << endl; cout << endl; cout << "======指向=============:\t" << ((int*)(&dvi1) +4) << "\t通過地址取得的值:" << (int*)*((int*)(&dvi1) +4) << "\t" <<*(int*)*((int*)(&dvi1) +4)<< endl; cout << "推測數(shù)據(jù)成員iInherit2地址:\t" << ((int*)(&dvi1) +5) << "\t通過地址取得的值:" << *((int*)(&dvi1) +5) << endl; cout << "推測數(shù)據(jù)成員iDerived地址:\t" << ((int*)(&dvi1) +6) << "\t通過地址取得的值:" << *((int*)(&dvi1) +6) << endl; cout << "=======================:\t" << ((int*)(&dvi1) +7) << "\t通過地址取得的值:" << *((int*)(&dvi1) +7) << endl; // cout << "虛函數(shù)表_vptr_Base地址:\t" << ((int*)(&dvi1) +8) << endl; cout << "_vptr_Base — 第1個函數(shù)地址:\t" << (int*)*((int*)(&dvi1) +8) << "\t即析構(gòu)函數(shù)地址" << endl; cout << "_vptr_Base — 第2個函數(shù)地址:\t" << ((int*)*((int*)(&dvi1) +8) +1) << "\t"; pFun = (Fun)*((int*)*((int*)(&dvi1) +8) +1); pFun(); cout << endl; cout << "_vptr_Base — 第3個函數(shù)地址:\t" << ((int*)*((int*)(&dvi1) +8) +2) << "【結(jié)束】\t" << *((int*)*((int*)(&dvi1) +8) +2); cout << endl; cout << "推測數(shù)據(jù)成員iBase地址:\t\t" << ((int*)(&dvi1) +9) << "\t通過地址取得的值:" << *((int*)(&dvi1) +9) << endl; } 輸出結(jié)果如下圖所示: 至此,C++對象模型介紹的差不多了,,清楚了C++對象模型之后,,很多疑問就能迎刃而解了。下面結(jié)合模型介紹一些典型問題,。 如何訪問成員,?前面介紹了C++對象模型,下面介紹C++對象模型的對訪問成員的影響,。其實清楚了C++對象模型,,就清楚了成員訪問機制。下面分別針對數(shù)據(jù)成員和函數(shù)成員是如何訪問到的,,給出一個大致介紹,。 對象大小問題
其中:3個類中的函數(shù)都是虛函數(shù) 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中包含虛函數(shù)表指針,所有size為4,;Derived繼承Base,,只是擴充基類的虛函數(shù)表,不會新增虛函數(shù)表指針,,所以size也是4,;Derived_Virtual虛繼承Base,根據(jù)前面的模型知道,,派生類有自己的虛函數(shù)表及指針,,并且有分隔符(0x00000000),然后才是虛基類的虛函數(shù)表等信息,,故大小為4+4+4=12,。 Empty p,sizeof(p)的大小是多少,?事實上并不是空的,,它有一個隱晦的1byte,那是被編譯器安插進去的一個char,。這將使得這個class的兩個對象得以在內(nèi)中有獨一無二的地址。 數(shù)據(jù)成員如何訪問(直接取址)跟實際對象模型相關(guān)聯(lián),根據(jù)對象起始地址+偏移量取得,。 靜態(tài)綁定與動態(tài)綁定程序調(diào)用函數(shù)時,,將使用那個可執(zhí)行代碼塊呢?編譯器負責回答這個問題,。將源代碼中的函數(shù)調(diào)用解析為執(zhí)行特定的函數(shù)代碼塊被稱為函數(shù)名綁定(binding,,又稱聯(lián)編)。在C語言中,,這非常簡單,,因為每個函數(shù)名都對應(yīng)一個不同的額函數(shù)。在C++中,,由于函數(shù)重載的緣故,,這項任務(wù)更復(fù)雜。編譯器必須查看函數(shù)參數(shù)以及函數(shù)名才能確定使用哪個函數(shù),。然而編譯器可以再編譯過程中完成這種綁定,,這稱為靜態(tài)綁定(static binding),又稱為早期綁定(early binding),。 然而虛函數(shù)是這項工作變得更加困難,。使用哪一個函數(shù)不是能在編譯階段時確定的,因為編譯器不知道用戶將選擇哪種類型,。所以,,編譯器必須能夠在程序運行時選擇正確的虛函數(shù)的代碼,這被稱為動態(tài)綁定(dynamic binding),,又稱為晚期綁定(late binding),。 使用虛函數(shù)是有代價的,在內(nèi)存和執(zhí)行速度方面是有一定成本的,,包括: l 每個對象都將增大,,增大量為存儲虛函數(shù)表指針的大小,; l 對于每個類,,編譯器都創(chuàng)建一個虛函數(shù)地址表; l 對于每個函數(shù)調(diào)用,,都需要執(zhí)行一項額外的操作,,即到虛函數(shù)表中查找地址。 雖然非虛函數(shù)比虛函數(shù)效率稍高,,單不具備動態(tài)聯(lián)編能力,。 函數(shù)成員如何訪問(間接取址)跟實際對象模型相關(guān)聯(lián),普通函數(shù)(nonstatic,、static)根據(jù)編譯,、鏈接的結(jié)果直接獲取函數(shù)地址,;如果是虛函數(shù)根據(jù)對象模型,取出對于虛函數(shù)地址,,然后在虛函數(shù)表中查找函數(shù)地址,。 多態(tài)如何實現(xiàn)?多態(tài)(Polymorphisn)在C++中是通過虛函數(shù)實現(xiàn)的。通過前面的模型【參見“有重寫的單繼承”】知道,,如果類中有虛函數(shù),,編譯器就會自動生成一個虛函數(shù)表,對象中包含一個指向虛函數(shù)表的指針,。能夠?qū)崿F(xiàn)多態(tài)的關(guān)鍵在于:虛函數(shù)是允許被派生類重寫的,,在虛函數(shù)表中,派生類函數(shù)對覆蓋(override)基類函數(shù),。除此之外,,還必須通過指針或引用調(diào)用方法才行,將派生類對象賦給基類對象,。
上面2個類,,基類Base、派生類Derived中都包含下面2個方法: void print() const; virtual void print_virtual() const; 這個2個方法的區(qū)別就在于一個是普通成員函數(shù),,一個是虛函數(shù),。編寫測試代碼如下: void test_polmorphisn()
{
Base b;
Derived d;
b = d;
b.print();
b.print_virtual();
Base *p;
p = &d;
p->print();
p->print_virtual();
}
根據(jù)模型推測只有p->print_virtual()才實現(xiàn)了動態(tài),其他3調(diào)用都是調(diào)用基類的方法,。原因如下: l b.print();b.print_virtual();不能實現(xiàn)多態(tài)是因為通過基類對象調(diào)用,,而非指針或引用所以不能實現(xiàn)多態(tài)。 l p->print();不能實現(xiàn)多態(tài)是因為,,print函數(shù)沒有聲明為虛函數(shù)(virtual),,派生類中也定義了print函數(shù)只是隱藏了基類的print函數(shù)。
為什么析構(gòu)函數(shù)設(shè)為虛函數(shù)是必要的析構(gòu)函數(shù)應(yīng)當都是虛函數(shù),,除非明確該類不做基類(不被其他類繼承),。基類的析構(gòu)函數(shù)聲明為虛函數(shù),,這樣做是為了確保釋放派生對象時,,按照正確的順序調(diào)用析構(gòu)函數(shù)。 從前面介紹的C++對象模型可以知道,,如果析構(gòu)函數(shù)不定義為虛函數(shù),,那么派生類就不會重寫基類的析構(gòu)函數(shù),在有多態(tài)行為的時候,,派生類的析構(gòu)函數(shù)不會被調(diào)用到(有內(nèi)存泄漏的風險,!)。 例如,,通過new一個派生類對象,,賦給基類指針,,然后delete基類指針。 void test_vitual_destructor() { Base *p = new Derived(); delete p; } 如果基類的析構(gòu)函數(shù)不是析構(gòu)函數(shù):
注意,,缺少了派生類的析構(gòu)函數(shù)調(diào)用,。把析構(gòu)函數(shù)聲明為虛函數(shù),,調(diào)用就正常了:
相關(guān)資料[1] 深度探索C++對象模型,,侯捷 [2] 測試代碼下載:https://github.com/saylorzhu/CppObjectDataModelTestCode [3] 關(guān)于虛函數(shù)的實現(xiàn)原理也可以參考關(guān)于虛函數(shù)的那些事兒
|
|