《深入探索C++對象模型》讀書筆記——第二章 構造函數語意學

第二章 構造函數語意學The Semantics of Constructors

1. Jerry Schwarz,iostream函數庫建構師,曾爲了讓cin能夠求得一個真假值,於是他爲它定義了一個conversion運算符operator int()。但在語句cin << intVal中,其行爲出乎意料:程序原本要的是cout而不是cin!但是編譯器卻找到一個正確的詮釋:將cin轉型爲整型,現在left shift operator <<就可以工作了!這就是所謂的“Schwarz Error”。Jerry最後以operator void *()取代operator int()。
2. 引入關鍵詞explicit的目的,就是爲了提供程序員一種方法,使他們能夠制止單一參數的constructor被當作一個conversion運算符。其引入是明智的,但其測試應該是殘酷的!
2.1 Default Constructor的構建
1.global objects的內存保證會在程序激活的時候被清爲0。local objects配置於程序的堆棧中,heap objects配置於自由空間中,都不一定會被清爲0,它們的內容將是內存上次被使用後的遺蹟。
2.“default constructors在需要的時候被編譯器產生出來”。關鍵字眼是“在需要的時候”。是被編譯器需要。
對於class X,如果沒有任何user-declared constructor,那麼會有一個default constructor被隱式聲明出來。一個被被隱式聲明出來的default constructor將是一個trivial(沒啥用的)constructor……
3. 以下四種情況,編譯器必須爲未聲明constructor的classes合成一個implicit nontrivial default constructor:
(1)帶有default constructor的member class object
(2)帶有default constructor的base class
(3)帶有virtual function
(4)帶有virtual base class
其它各種情況且沒有聲明任何constructor的classes,它們擁有的是implicit trival default constructors,它們實際上並不會被合成出來。
4、帶有default constructor的member class object
(1)在各個不同的版本模塊中,編譯器避免合成出多個default constructor的方法如下:
把合成的default constructor、copy constructor、assignment copy operator都以inline方式完成。一個inline函數有靜態鏈接,不會被檔案以外者看到。如果函數過於複雜,不適合做成inline,就會合成一個explicit non-inline static實體。
(2)

被合成出來的Bar default constructor內含必要的代碼,能夠調用class Foo的default constructor來處理member object Bar::foo,但它並不產生任何代碼來初始化Bar::str。
將Bar::foo初始化是編譯器的責任,將Bar::str初始化則是程序員的責任。
被合成出來的default constructor只滿足編譯器的需要,而不是程序員的需要。爲了讓程序片段正確執行,字符指針str也需要被初始化。
編譯器會擴張已存在的constructor,在其中安插一些代碼,是的user code被執行之前,先調用必要的default constructors。

(3)如果有多個class member objects都要求constructor初始化操作,C++語言要求以“member objects在class中的聲明順序”來調用各個constructors。

5、帶有default constructor的base class
如果一個沒有任何constructor的class派生自一個“帶有default constructor”的base class,那麼這個derived class的default constructor會被視爲nontrivial,並因此需要被合成出來。它將調用上一層base classes的default constructor(根據他們的聲明順序)。
6、帶有一個Virtual Function的class
有兩種情況,也需要合成出default constructor
(1)class聲明(或繼承)一個virtual function
(2)class 派生自一個繼承串鏈,其中有一個或更多的virtual base classes。
由於缺乏由user聲明的constructors,編譯器會詳細記錄合成一個default constructor的必要信息。

此外,widget.flip()的虛擬調用操作會被重新改寫,以使用widget的vptr和vtbl中的flip()條目:
(*widget.vptr[1])(&widget)
其中:(1)1表示flip()在virtual table中的固定索引
(2)&widget代表要交給“被調用的某個flip()函數實例”的this指針。
7、帶有一個virtual base class的class

必須使virtual base class在每一個derived class object中的位置,能夠於執行期準備妥當。
所有經由reference或pointer來存取一個virtual base class的操作都可以通過相關指針來完成。

其中_vbcX表示編譯器所產生的指針,指向virtual base class X
_vbcX(或編譯器所做出的某個東西)是在class object構造期間被完成的。對於class所定義的每一個constructor,編譯器會安插那些“允許每一個virtual base class的執行器存取操作”的代碼。如果class沒有聲明任何constructors,編譯器必須爲它合成一個default constructor。

總結:
有四種情況會造成“編譯器必須爲聲明constructor的classes合成一個default constructor”。把這些合成物稱爲implicit nontrivial default constructors。被合成出來的constructor只能滿足編譯器(而非程序)的需要。
沒有存在那四種情況而又沒有聲明任何constructor的classes,我們說他們擁有的是implicit trivial default constructors,他們實際上不會被合成出來。
在合成的default constructor中,只有base class subobjects和member class objects會被初始化。所有其他的nonstatic data member(如整數、整數指針、整數數組等)都不會被初始化。這些初始化操作對程序而言或許有需要,但對編譯器則非必要。

新手誤解:
(1)任何class如果沒有定義default constructor,就會被合成出一個來。
(2)編譯器合成出來的default constructor會顯式設定“class內每一個data member的默認值”。
——都不對。

8、編譯器合成implicit nontrivial default constructor,不過是暗地裏作了一些重要的事情以保證程序正確合理地運行。如果程序員提供了多個constructors,但其中都沒有default constructor,編譯器同樣會在這些constructors中插入一些相同功能的代碼,這些代碼都將被安插在explicit user code之前。

2.2 Copy Constructor的構造操作
0、一個class object可以從兩種方式複製得到:初始化和指定,從概念上而言,這兩個操作分別是以copy constructor和copy assignment operator完成的。
1、Default memberwise initialization
如果class沒有提供一個explicit copy constructor又當如何?當class object以“相同class的另一個object”作爲初值,其內部是以所謂的default memberwise initialization首發完成的,也就是把每一個內建的或派生的data member的值,從某一個object拷貝一份到另一個object身上。不過他並不會拷貝其中的member class object,而是以遞歸的方式施行memberwise initialization。
Default constructors和copy constructors在必要的時候才由編譯器產生出來。“必要”是指當class不展現bitwise copy semanics時。
決定一個copy constructor是否爲trival的標準在於class是否展現出所謂的“bitwise copy semanics”。
2、Bitwise Copy Semanics(位逐次拷貝)

這種情況並不需要一個default copy constructor,因爲上述聲明展現了“default copy semanics”

這種情況下,編譯器必須合成出一個copy constructor,以便調用member class string object的copy constructor:

3.不要 “bitwise copy semantics”
以下四種情況,一個class不展現bitwise copy semantics:
(1)class內含一個member object而後者的class聲明有或被編譯器合成有一個copy constructor時;
(2)class繼承自一個base class而後者存在或被編譯器合成有一個copy constructor時;
(3)當class聲明瞭一個或多個virtual functions時;
(4)當class派生自一個繼承串鏈,其中有一個或多個virtual base classes時。
前兩種情況中,編譯器必須將member或base class的copy constructors調用操作安插到被合成的copy constructor中。
4、重新設定virtual Table的指針

(1)一旦一個class object中必須引入vptr,編譯器就必須爲它的vptr正確地設置好初值。此時,該class就不再展現bitwise semantics。
(2)當一個base class object以其derived class object內容作初始化操作時,其vptr複製操作必須保證安全。
5、處理virtual Base Class Subject
在每一個編譯器對於虛擬繼承的支持承諾,都代表必須讓“derived class object中的virtual base class subobject位置”在執行期就準備妥當。維護位置的完整性是編譯器的責任。

2.3 程序轉化語意學
1. 每一個明確的初始化操作都會有兩個必要的程序轉化階段:先重寫每一個定義,剝除其中的初始化操作,然後安插class的copy constructor調用操作。
2. 把一個class object當作參數傳給一個函數或是作爲一個函數的返回值,相當於以下形式的初始化操作:
X xx = arg; 其中xx代表形式參數或返回值,而arg代表真正的參數值。
3. 函數定義如下:X bar(){X xx; return xx;},bar()的返回值通過一個雙階轉化從局部對象xx中拷貝出來:
ü 首先爲bar添加一個額外參數,類型是class object的一個reference,這個參數用來放置被拷貝構建而得的返回值。
ü 然後在return指令之前安插一個copy constructor調用操作,以便將欲傳回之object的內容當作上述新增參數的初值,同時重寫函數使它不返回任何值。
4. Named Return Value(NRV)優化如今被視爲是標準C++編譯器的一個義不容辭的優化操作,它的特點是直接操作新添加的額外參數。注意只有copy constructor的出現纔會激活C++編譯器的NRV優化!NRV優化雖然極大地改善了效率,但還是飽受批評:一是優化由編譯器默默完成,而是否完成以及其完成程度完全透明;二是一旦函數變得比較複雜,優化就變得較難施行;三是優化由可能使程序產生錯誤——有時並不是對稱地調用constructor和destructor,而是copy constructor未被調用!
5. 在編譯器提供NRV優化的前提下,如果可以預見class需要大量的memberwise初始化操作,比如以by value的方式傳回objects,那麼提供一個explicit inline copy constructor的函數實體就非常合理。此種情況下,沒有必要同時提供explicit assignment operator定義。
6. copy constructor的應用迫使編譯器多多少少對程序代碼作部分優化,尤其是當一個函數以by value的方式傳回一個class object,而該class有一個copy constructor(或定義或合成)時,無論在函數的定義還是在使用上將導致深奧的程序轉化。此外,編譯器將實施NRV優化。
7. 注意正確使用memset()和memcpy(),它們都只有在classes不含任何由編譯器產生的內部members如vptr時纔能有效運行!

2.4 成員初始化列表
1. 當寫下一個constructor時,就有機會設定class members的初值。不是經由member initialization list,就是在constructor函數本身之內。
2. 下列情況,爲了讓程序能被順利編譯,必須使用member initialization list:
ü 初始化一個reference member時;
ü 初始化一個const member時;
ü 調用一個base class的constructor,而它擁有一組參數時;
ü 調用一個member class的constructor,而它擁有一組參數時。
3. 編譯器會對initialization list一一處理並可能重新排序,以反映出members的聲明次序,它會安插一些代碼到constructor內,並置於任何explicit user code之前。
4. 一個忠告:請使用“存在於constructor體內的一個member”,而不是“存在於member initialization list中的一個member”,來爲另一個member設定初值。

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