天天看點

Inside C++ object Model--構造函數

預設構造函數

構造函數是幹啥的, 是在構造類對象的時候, 給程式員進行對象初始化操作的機會. 不僅如此, 同時也是給編譯器進行對象初始化的機會. 當然程式員和編譯器的扮演的角色是不一樣的, 考慮的問題也是不一樣的.

當程式員覺得這個類對象沒有任何初始化的必要時, 他就不會特意去聲明構造函數.

那麼對于一個類, 當程式員沒有聲明任何構造函數的時候, 編譯器有可能 會為該類聲明一個default 構造函數. 之是以是'有可能', 是因為編譯器也是很懶的, 如果他也覺得這個類沒有任何初始化的必要時, 他其實也是不會真正構造default 構造函數的. 隻有當他認為這個構造函數為nontrivial, 即他确實需要對類對象進行某些初始化操作時, 才會建立default 構造函數.

下面先舉個例子, 從程式員的角度 , 啥時候需要構造函數,

class foo { public: int val; foo *pnext; };

void foo_bar()

{

    // oops: program needs bar's members zeroed out

    foo bar;

    if ( bar.val || bar.pnext )

        // ... do something

    // ...

}

如上面的代碼, 程式員期望建立對象的時候, 會生成default 構造函數對成員變量進行初始化清零, 而實際上在編譯器看來, 這種活是程式員應該做的. 是以編譯器不會特意構造default 構造函數來初始化類成員.

那麼這段邏輯就是有bug的, 這也是新手很容易範的一個錯,

global objects are guaranteed to have their associated memory "zeroed out" at program start-up. local objects allocated on the program stack and heap objects allocated on the free-store do not have their associated memory zeroed out; rather, the memory retains the arbitrary bit pattern of its previous use.

對于全局變量, 程式開始時會清零, 但對于在stack和heap上配置設定的記憶體, 會保持上次使用時的遺留資料, 是以如果不手工清零, 你有可能得到任意值. 而這步清零操作就是程式員應該負責在構造函數中調用的, 而不是編譯器的責任.

那從編譯器的角度 , 啥時候是一定需要在構造函數裡面加上對象初始化操作的

有以下4種情況,

member class object with default constructor 

對于對象成員也是一個類對象, 并且該類定義了default constructor, 編譯器必須保證該成員對象被正确的初始化, 即default constructor被調用.

base class with default constructor 

同樣編譯器必須保證該base class被正确初始化

class with a virtual function 

這種情況有兩種case,

1. the class either declares (or inherits) a virtual function

2. the class is derived from an inheritance chain in which one or more base classes are virtual

對于有虛函數的類, 編譯器必須額外做下面兩件事, 以保證多态能夠正常工作,

1. a virtual function table (referred to as the class vtbl in the original cfront implementation) is generated and populated with the addresses of the active virtual functions for that class.

2. within each class object, an additional pointer member (the vptr ) is synthesized to hold the address of the associated class vtbl.

說白了, 編譯器必須建立虛表, 并在每個對象中添加指向虛表的指針.

class with a virtual base class 

對于virtual base class的實作, 各個編譯器有很大的不同

what is common to each implementation is the need to make the virtual base class location within each derived class object available at runtime .

是以編譯器必須保證在構造函數中确定虛基類在執行期的位址

對于上面的4種情況, 編譯器必須建立default constructor來做特殊處理, 如果程式員已經定義了構造函數, 編譯器也要在該構造函數中插入特殊處理. 除了上面4種情況, 編譯器不會額外産生default constructor, 也不會對對象做任何額外的初始化操作, 如果想做什麼, 程式員請自己搞定.

拷貝構造函數

有三種情況會使用到copy構造函數, 即以一個對象的内容作為另一個對象的初值.

對一個對象做明确的初始化操作,

class x { ... }; 

x x; 

// explicit initialization of one class object with another 

x xx = x;

另外兩種情況是當object被當作參數交給某個函數, 或者當函數傳回一個對象時.

同樣copy構造函數用于當使用一個類對象初始化另一個類對象時, 給程式員或編譯器一個做某些特殊處理的機會.

同樣程式員和編譯器需要考慮的問題是不同的,

對于程式員舉個典型的例子來說明copy構造函數的意義,

當對象成員為指針時, 如果僅僅指派指針本身, 會導緻多個指針指向同一對象, 當一個指針把對象釋放後, 其他地方就會報錯.

是以程式員在copy構造函數中, 需要新建立指針成員指向對象的copy, 并将新對象的成員指針指向該copy, 這樣才是完全拷貝.

當程式員不特意建立copy構造函數時, 編譯器在大部分情況下預設為bitwise copy semantics, 即把一個對象中的資料原封不動的按bit拷到需初始化的對象中. 是以對上面說的成員是指針的情況, 會導緻兩個對象的成員指針指向同一個對象實體的不合理的情況.

但是對于下面4種情況, 編譯器必須要特意生成copy構造函數來保證copy的正确性,

1. when the class contains a member object of a class for which a copy constructor exists

2. when the class is derived from a base class for which a copy constructor exists

3. when the class declares one or more virtual functions 

4. when the class is derived from an inheritance chain in which one or more base classes are virtual 

1和2很容易了解, 編譯器必須保證member object或base class中程式員所寫的copy constructor被調到

3和4相對複雜一些, 其實對于這兩種情況, 如果是相同類之間的對象互相初始化, 也是不用做特殊處理的, 看下面的例子

class zooanimal {......}

class bear : public zooanimal {......}

bear yogi;

bear winnie = yogi;  //對于這種情況, 直接拷貝就可以了

zooanimal franny = yogi;  //但是這種情況, 編譯器必須要調整franny的虛表指針, 因為yogi的vptr是指向bear的, 要改成zooanimal

對于有虛函數的類, 基類與派生類之間的初始化, 編譯器必須産生copy構造函數去調整vptr的值

對于有虛基類的情況, 同樣也是當基類與派生類之間的初始化時, 編譯器需要産生copy構造函數去仲裁虛基類對象的位置.

總結, 其實對于default構造函數和copy構造函數而言, 隻有當在類成員對象或基類中有特殊的構造函數需要調用, 或類含有虛函數和虛基類需要做特殊處理外, 編譯器不會對類對象做任何初始化操作, 大家不用懷疑編譯背地裡做了太多操作, 他也很懶的...

拷貝構造函數引起的程式轉換

上面說了調用copy構造函數的3種情況, 可是他們都不是顯式的調用, 是以編譯器需要做程式轉換, 下面就分情況說明

1. 顯式的初始化

x x0; 

the following three definitions each explicitly initialize its class object with x0:

void foo_bar() { 

   x x1( x0 ); 

   x x2 = x0; 

   x x3 = x( x0 ); 

   // ... 

對于嚴謹的c++編譯器, 定義就是指占用記憶體, 是以對于它而言, 定義和初始化式分開的

// possible program transformation 

// pseudo c++ code 

   x x1; 

   x x2; 

   x x3; 

   // compiler inserted invocations 

   // of copy constructor for x 

   x1.x::x( x0 ); 

   x2.x::x( x0 ); 

   x3.x::x( x0 ); 

這個轉換很好了解.

2. 參數的初始化

void foo( x x0 ); 

an invocation of the form

x xx; 

// ... 

foo( xx );

對于這樣的參數傳遞, 其實編譯器會做如下轉換

// compiler generated temporary 

x __temp0; 

// compiler invocation of copy constructor 

__temp0.x::x ( xx ); 

// rewrite function call to take temporary 

foo( __temp0 );

這兒會首先把xx的值通過copy構造函數初始化臨時變量_temp0, 之是以需要臨時變量, 是因為xx在函數外, 當進入foo函數後xx是不可見的, 是以先要保留一份xx的備份.

好, 這樣進入foo函數後的第一件事,就是再把_temp0通過拷貝構造函數去初始化x0

這樣傳遞一個參數, 調用了兩遍拷貝構造函數, 挺低效的

是以, 這兒編譯器可以把函數聲明改寫成

void foo( x& x0 );

這樣的話,就可以直接把x0指向_temp0就可以了, 提高了效率

當然這邊的改寫政策是編譯器相關的, 這兒隻是一種方案, 其他的編譯器方案就不介紹了.

3. 傳回值初始化

x bar()

   x xx;

   // process xx ...

   return xx;

對于傳回值, 編譯器的會做如下轉換

// function transformation to reflect

// application of copy constructor

// pseudo c++ code

void

bar( x& __result )

   // compiler generated invocation

   // of default constructor

   xx.x::x();

   // ... process xx

   // of copy constructor

   __result.x::x( xx );

   return;

可見, 所謂的傳回值, 其實編譯器隻是增加了一個result參數, 最後将xx通過拷貝構造函數初始化result

是以對于底下的調用

x xx = bar();

編譯器會改寫成

// note: no default constructor applied

x xx;

bar( xx );

為了提高效率, 對于傳回值, 我們可以想些辦法來避免拷貝構造函數的調用

a. 使用者優化 

x bar( const t &y, const t &z )

   // ... process xx using y and z

優化成,

   return x( y, z );

這段代碼, 編譯器轉換為

bar( x &__result )

   __result.x::x( y, z );

這樣就避免了拷貝構造函數的調用, 可以直接調用構造函數來産生傳回值, 但這樣最大的問題, 你必須定義新的構造函數來以y,z構造x, 會導緻這種特殊用途的構造函數大量擴散. 這個方法不太靠譜

b.編譯器優化

編譯器如果比較聰明的話, 會自動對它進行優化, 直接用__result替換xx, 這樣就不用後面費事調用拷貝構造函數了

   // default constructor invocation

   // pseudo c++ code

   __result.x::x();

   // ... process in __result directly

this compiler optimization, sometimes referred to as the named return value (nrv) optimization.

這樣的nrv優化對于需要大量調用該函數的case, 效率提高還是很明顯的

但是這種優化對于邏輯比較複雜的函數, 也是無能為力的.

本文章摘自部落格園,原文釋出日期:2011-07-05

繼續閱讀