深度探索c++對象模型 小結【轉】

Chapter 2 構造函數語義學習小結

1、 C++編譯器何時會爲C++中的類生成缺省的構造函數(Default constructor)?

如果程序員沒有定義構造函數,編譯器會在下面四種情況爲類生成缺省的構造函數:

?        類中聚合的元素有構造函數(可以是程序員自定義的或者編譯器生成的);

?        類的基類有構造函數(可以是程序員自定義的或者編譯器生成的);

?        類中有虛函數;

?        類虛繼承其他類。

對於不符合以上四種情況,C++編譯器並不會生成一個構造函數,那類實例化的對象怎麼初始化?實際上編譯器都是直接分配內存而已,並沒有初始化對象。

2、 C++編譯器何時會爲C++中的類生成缺省的拷貝構造函數(Copy constructor)?

拷貝構造函數的合成也使用於上述四種情況。對於符合上述四種情況拷貝構造函數執行按成員拷貝(memberwise copy),對於不符合上述四種情況執行按位拷貝(bitwise copy)。【這是個人區分memberwisebitwise,而書中的意思好像是都是memberwise,在memberwise中分爲是否進行bitwise】。那些bitwise拷貝帶來的後果是什麼?對於一般的類型intdouble等都不會有問題,但是對於指針類型就會發生兩個對象指向同一個指針變量【也就是所謂的淺拷貝】。從語義角度來看,當含有指針數據成員必須提供構造函數。

3、 什麼時間需要程序員提供一個拷貝構造函數?

對於含有指針數據成員必須提供構造函數,那麼對於其他情況是否都不需要?從語義上講,是這樣的。但是對於編譯器來講涉及到編譯優化中的返回值優化【Named return Value】,這中優化是指至少可以減少一次返回值構造,但NRV的前提是程序員已經定義好拷貝構造函數,這需要根據對效率的需求來決定。

4、 書中的一個示例,爲什麼在類的構造函數中使用高效的memcpy()或者memset()庫函數對對象進行拷貝有時候會發生錯誤【P73】。

原因是當類中含有vptr或者vbtl時,如果直接使用庫函數,會修改這些指針。

5、 對於NRV的討論,到底是否必須存在顯示的Copy ctor才能實行NRV

問:67頁,最下面兩行:這個程式的第一個版本不能實施NRV最佳化,因爲test class缺少一個copy constructor。但是在66頁「在編譯器層面做最佳化」那一段中所列的碼顯示,當編譯器把xx__result取代,變成__result.X::X();default constructor被喚起。喚起default constructor是可以理解的,可是編譯器轉換後的碼並沒有使用到copy constructor呀,爲什麼67頁最後兩行卻說缺少一個copy constructor,就不能實施這個最佳化了呢?

觀點1如同63頁與64頁「返回值的初始化」這一段,編譯器可能將63頁下面的X bar()函數定義轉換成64頁的虛擬碼,其中有一行__result.X::X(xx); 這會使用到copy constructor 轉換成64頁的碼後,65頁與66頁分述了兩種後續可能出現的最佳化動作,其中一種即是66頁的編譯器層面做最佳化。如此,雖然66頁最佳化後的碼看起來並不使用到copy constructor,但是這些碼是根據像64頁那種樣子的碼最佳化而來的,而若沒有copy constructor,根本無法轉換成64頁那種虛擬碼,因爲其中有一個呼叫copy constructor的動作。所以,雖然66頁經過編譯器最佳化的結果省去了__result.X::X(xx);這個copy constructor的呼喚動作(因爲根本沒有xx了),但若沒有明白提供一個copy constructor,卻無法讓編譯器進行這樣的最佳化。【第5章,205頁最下面一段話:一般而言如果你的設計之中,有許多函式都需要以傳值(by value),傳回一個local class object....那麼提供一個copy constructor,就比較合理--甚至即使default memberwise語意已經足夠。它的出現會觸發NRV最佳化。然而,就像我在前一個例子中所展現的那樣,NRV最佳化後將不再需要喚起copy constructor,因爲運算結果已經被直接計算於「將被傳回的object」體內了。」】

觀點2Lippmanp67最後一行所言『這個程式的第一個版本不能實施NRV最佳化,因爲test class缺少一個copy constructor』,此語錯誤。如果程式沒有explicit copy constructor,編譯器會自動爲我們做出來(如爲trivial,則直接bitwise copy;如爲nontrivial,則由編譯器爲我們合成出一個copy constructor)。因此,有沒有explicit copy constructor並不影響 NRV 最佳化的實施。NRV 最佳化主要是由編譯器option來決定要不要實施。做了一些實驗,判斷VCGCC都沒有做到NRV最佳化,而其不做的理由不是因爲技術上的困難,是爲了避免造成「user defined copy constructor之副作用失效」-所謂副作用是指,例如「在user defined copy constructor中做一個cout出」之類這種「與memberwise copy 無關」的動作。

Chapter 3 成員數據語義學習小結

1、 C++編譯器對類增加那些數據?

?        類沒有定義任何數據成員【編譯器會爲類增加1byte,原因是爲了區分類實例化的多個對象都有不同的地址】;

?        含有虛函數【編譯器會爲類添加一個vptr指針,指向虛函數表,virtual table】;

?        虛繼承其他類【編譯器會爲類添加一個vbtl指針,指向虛基類】。

2、 C++編譯器如何處理類的非靜態數據成員?

如果我們直接訪問類的非靜態數據成員的地址,發現它僅僅是數據成員在類中的偏移量,如果要訪問某一對象數據成員,那麼該數據的地址是對象的地址加上數據成員在類中的偏移量。在P98中的表示是:&original + &Point3d::_y -1),就是訪問的是Point3d實例化的對象original中的數據成員_y,爲什麼會有一個減一的動作?這個的目的是爲了區分指向第一個member的指針和一個指向數據成員,但沒有指向任何member【如Point3d::*pM = NULLPoint3d中的第一個數據成員的偏移量不能都是0,目前在Dev-CppVC6.0中都不是這樣處理的,他們都是對一個數據成員的空指針減去一,變成0xFFFFFFFF,而編譯器對數據成員的偏移量都不變化,直接按照他們在類中的聲明順序】。

3、 C++編譯器如何處理多繼承時的vptr

如果一個類含有虛函數或者單繼承有虛函數,編譯器還是比較容易處理的,只要設置一個vptr就可以訪問到對應的函數。但是當類多繼承其他類(且都有虛函數),那麼編譯器必須處理指向派生類對象的指針能夠指向多個基類。派生類和第一個基類共享vptr,對於第二個或者以後的基類,這個指向派生對象的指針必須加上一個偏移量來指向對應的類的vptr數據。【第二類的偏移量爲sizeof(Base_Class1),後面的依次類推】。

Chapter 4 成員函數語義學習小結

C++編譯器中有一個技術爲了支持多態、命名空間等,叫做Name-mangling,就是把一個名字轉化爲一個編譯器可以唯一識別的名字。

1、 C++編譯器把成員函數編譯成什麼樣子?

C++編譯器把成員函數分爲兩類,靜態和非靜態成員函數。假定CExampleClass中有3個函數原型如下:

?        int NormalFun(parameter…)

?        int NormalFunConst(parameter…) const;

?        static int SNormalFun(parameter…);

C++編譯器編譯後會把這個函數都編譯成全局函數,如下:

?        int CExampleClass13NormalFun1p(CExampleClass * const this, parameter…);

?        int CExampleClass13NormalFunConst1p(const CExampleClass * const this, parameter…);

?        int CExampleClass13 SNormalFun1p(parameter…);

2、 C++編譯器如何調用成員函數?

C++編譯器在調用方面也可以總結爲三類調用方式,靜態函數(不能爲constvirtual等修飾),非靜態非虛成員函數,單繼承的虛函數,多繼承的虛函數。

?        靜態成員:直接調用,如CExampleClass::SNormalFun(parameter…)或者根據對象可以調用,編譯器把這種調用直接轉化爲上面的CExampleClass13 SNormalFun1p(…)形式;

?        非靜態非虛成員函數,必須通過對象調用,如Obj.NormalFun(…),編譯器把這種形式轉化爲CExampleClass13NormalFun1p(&Obj, parameter…)的形式;

?        對於虛函數的調用,是通過vptr進行的,如ptr->vFun(pararmeter…),編譯器將轉爲爲:(*ptr->vptr[index])(ptrparameter…)形式;

?        對於多繼承下虛函數的調用,必須調整後面基類的偏移量。主要有兩種方式:第一種形式爲(*ptr->vptr[index].addr)(ptr+ptr->vptr[index].offsetparameter…)【這個offset在編譯器中生成的是一個負數】,這個設計的目的增加一個結構,保證派生類同時override多個基類的虛函數都能指向同一個函數;第二種形式是使用thunk技術,vptr中對應的index存放的是簡單虛函數的地址或者是指向一個相關的thunk(用於調整this指針);

?        C++編譯器對函數指針的翻譯:對於多繼承下一個函數指針翻譯,(pClass.*pfm)()被轉化爲pfm.index < 0 ? (*pfm.fadd)(&pClass + pmf.offset) : (*pClass.vptr[pfm.index].faddr) (&pClass + pClass.vptr[pfm.index].offset)index小於0表示該函數不是虛函數】。

3、 C++編譯器把vptr放在類的什麼位置?

C++標準認爲可以放在任何位置,可以在類的頭部,目前VCDEV-Cpp都是如此,爲什麼?如果把vptr放在尾部,其實是可以直接兼容C語言中的struct結構,但是C++是爲了節省空間,便於操縱vptr沒有這樣做。如何節約空間,當有繼承的時候,放在頭部時,派生類是可以共享基類的vptr

4、 C++編譯器中的那些操作會增加代碼?

?        宏展開:宏是一定會被展開的,這一定會增加代碼量;

?        Inline函數:如果編譯器決定把該函數Inline,那麼也會增加代碼,其中涉及到增加參數、局部變量和代碼,特別是對於在一個表達式中又多次inline函數調用,則inline函數的局部變量會被擴展多次,然後合成一個scope

?        Deconstructor函數:因爲C++保證資源獲得即初始化(RAII),所以如果在源代碼中又多個出口,編譯器都會在出口點增加析構變量的操作;

?        異常,C++爲了異常的trycatch處理,必須增加代碼;

?        Template:當模板函數或者模板類被使用的時候,C++編譯器會保證實例化模板類或者模板函數。

Chapter 5 構造、析構和拷貝語義學習小結

1、 C++編譯器是怎麼樣實現虛繼承的構造函數?

假定Derived繼承於Base1Base2Base1Base2繼承於Base,那麼Derived的構造函數和析構函數編譯器是如何生成?

Derived7DerivedVDerived * const this, bool _most_derived

{

       if( _most_derived != false ) this->Base::Base();

       this->Base1::Base1(false);

       this->Base2::Base2(false);

       this->vptr = _vbtl_derived; //設置vptr

       this->vptr_Base = _vbtl_Base_derived; //設置虛基類指針

       //user code

       return this;

}

Derived7DectorVDerived * const this, bool _most_derived

{

       this->vptr =  _vbtl_derived;

       //user code

       this->Base1::Base1(false);

       this->Base2::Base2(false);

       if( _most_derived != false ) this->Base::Base();

}

注:VC中就是這麼實現的,在DEV_Cpp中的實現與此不同,它是產生兩個版本的constructor,一個是設定vptr,並調用虛基類,另外一個是不調用虛基類也不設置vptr

注:P234中的譯者加了譯註,實際上是不對的,因爲譯者沒有考慮多繼承的情況。譯者的理解對於是單繼承的情況是正確的,對於多繼承必須考慮到先設置vptr,因爲可能是一個Base2的指針指向Derived對象的,如果不先設置則不能正確的調用Derived的析構函數】。

注:【後來在網上看到一篇文章《<深度探索C++對象模型>>(簡體版)中的蛇足》,作者viperhttp://blog.csdn.net/Viper/。作者文中描述的第二點,不過我是認爲侯先生加的譯註是錯誤的,而不是太理論化。侯先生加的譯註第三點的意思應該是指正確的設置基類的vptr,其實在調用基類的析構函數中都會設置的。在http://dev.csdn.net/article/10/10874.shtm有關於這篇文章的討論,很有意思的。

2、 C++編譯器把成員編譯後結果是什麼樣子?

成員主要指的是靜態數據、靜態成員函數、非靜態數據、非靜態成員函數和全局數據和heap數據。

Class

Data

Static Data

Static Fun

Function

Virtual Fun

Global Data

Static Data

Fun(this,…)

Static Fun(…)

Vptr

Vbcb

Data

編譯後的結果,不考慮Name-mangling

全局可見

.Data

全局

可見

類對象可見,局部和heap對象數據

3、 C++如何處理全局對象、靜態對象?

對於全局變量,針對特定平臺的C++編譯器的一種處理方法,增加兩個Sections,分別爲.init.fini,處理全局對象的構造和析構。【全局對象要求在main函數之前就存在】。.init section主要完成的是調用對象的構造函數,爲了保證一個文件中的所有的全局對象都能夠初始化,一般會爲每個文件生成一個_sti(),該函數負責初始化該文件中所有的全局對象。全局對象的析構是在main函數結束之前完成【.fini中析構】

【全局對象要求在main函數之前完成初始化,如果在執行全局對象的構造函數時,發生異常,那麼C++編譯器將直接調用terminate()函數,main函數將不會執行。VC6.0DEV-Cpp都是如此】

靜態對象如果是全局的,那麼初始化的過程和上面的過程是一致的。如果是靜態局部對象,他的初始化是在該函數第一次執行的時候才完成初始化。C++編譯器怎麼知道該函數是第一次執行?C++編譯器設置一個全局的指針,如果沒有初始化該指針爲NULL,如果初始化則該指針爲靜態對象的地址,當完成初始化的時候改變指針的狀態就能區分。

全局靜態對象的析構和全局對象的析構一樣。局部靜態對象的析構也需要根據指針是NULL還是對象的地址來判斷是否析構。【對於局部靜態對象的處理VC6.0DEV-Cpp都是通過一個byte存放標誌位來完成】。

Chapter6 執行期語義學習小結

1、 New[]的學習和討論。

C++編譯器如何完成New[]New operator實際上完成兩步操作,第一:根據對象類型分配內存【如調用free來完成】,調用構造函數初始化對象【對於New[]構造函數限定爲default ctor或者帶有構造函數的參數都有缺省值】。對於New來說可以一次完成,但是對於New[]來說必須藉助一個新的函數來完成【原因很簡單:可能存在異常,那麼必須析構已經完成構造的對象,異常可能發生在任何時候】。一般會把New[]修改爲什麼樣子然後調用?一般封裝爲:vec_new (pVoid ptrArray, int elemCount, int objSize, pVoid ctor, pVoid dtor)【當ptrArray不爲0,表示placement operator new語義】。New[]存放的數組的長度一般在真正存儲對象地址的前4Byte中【VCGCC都是如此】。

討論:在網上《Const的思考一文》中的一個例子:

class A

{

public:

A(int i=0):test[2]({1,2}) {} //你認爲行嗎?

private:

const int test[2];

}

vc6下編譯通不過,爲什麼呢?

觀點1:編譯器堆初始化列表的操作是在構造函數之內,顯式調用可用代碼之前,初始化的次序依據數據聲明的次序。初始化時機應該沒有什麼問題,那麼就只有是編譯器對數組做了什麼手腳!其實做什麼手腳,我也不知道,我只好對他進行猜測:編譯器搜索到test發現是一個非靜態的數組,於是,爲他分配內存空間,這裏需要注意了,它應該是一下分配完,並非先分配test[0],然後利用初始化列表初始化,再分配test[1],這就導致數組的初始化實際上是賦值!然而,常量不允許賦值,所以無法通過。

觀點2:認爲上一個觀點錯誤【我第一次看到也是上面的解釋,汗先】,C++標準有一個規定,不允許無序對象在類內部初始化,數組顯然是一個無序的,所以這樣的初始化是錯誤的!對於他,只能在類的外部進行初始化,如果想讓它通過,只需要聲明爲靜態的,然後初始化。

2、 臨時對象的討論。

C++中有很一部分工作是爲程序員的代碼添加一些臨時對象完成語義/語法上的要求。例如:使用轉換函數,不同對象之間的賦值(包括一些返回值和目標對象不一致),還有一些是程序員沒有明確的指定運算的結果等等。

但是C++編譯器在一些情況下生成臨時對象會帶來一些問題【效率降低或者語義的複雜】。例如:在一個複合語句中有臨時對象,C++編譯器一定會生成一些判斷代碼,爲什麼?要判斷何時處理對象析構;臨時對象的析構一般來說是語句結束以後,有兩種例外,在生成臨時對象用來初始化另外的對象,那麼必須等待初始化結束後臨時對象才能析構,還有一些情況是C++生成的臨時對象的作用域等同於目標對象的作用域啦,C++編譯器生成的臨時對象初始化reference對象,析構必須是在臨時對象作用域和reference作用域取小者才能析構。

Chapter7 站在對象的頂端學習小結

1、 異常中trycatch是如何實現?

對於異常的處理,構造program counter-range。對於try block來說,把一個函數的try block的起始位置和結束位置保存在上述表格中,當發生異常時,當前的program counter(也就是程序執行的位置)和program counter-range進行比較,以判斷出是否在try block中,如果是,就要找到對應的catch,否則當前的函數會從程序的棧(ESPEBP等寄存器信息)中彈出,並從新設置program counter爲調用的地址,然後繼續上述的判斷過程。

對於拋出的異常對象,編譯器產生一個類型描述符,對異常的類型產生編碼。編譯器還必須爲catch子句產生類型描述符,執行期的異常處理模塊則會比較拋出的對象的類型描述符和catch子句的類型描述符,找到合適的catch或者最後到terminate()處理。

2、 爲什麼向下轉換(downcast)中對於Pointerreference的處理不一致?

對於downcast來說(dyanmaic_cast<type>(Object)),對於這兩種的處理分別是:

?        對於Pointer來說,當轉換成功時,成功的返回派生類對象的指針,當發生轉化錯誤時候,返回0(也就是NULL);

?        對於reference來說,當轉換成功時,成功的返回派生類對象,當發生轉化錯誤時候,拋出一個bad_cast exception(爲什麼不是0?很簡單,對於reference0會被轉換成臨時對象,然後reference到這個臨時對象)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章