1.Default Constructor的構造操作
默認構造函數是在編譯器需要的時候構建出來的,被合成的默認構造函數只執行編譯器所需的動作。被合成的默認構造函數中只有base class subobjects以及member class objects會被初始化,而其他nonstatic data member(如整數、指針、數組)都不會初始化,因爲他們是滿足程序需要的。
有四種情況會使得編譯器爲未聲明constructor的classes合成一個滿足編譯器需要的implicit nontrivial default constructor。對於其他情況又沒有聲明任何constructor的,實際上並不會合成。
1.1 帶有Default Constructor的Member Class Object(一個類的成員類具有默認構造函數。)
若一個class沒有constructor但有member object,而這些member object有default constructor,那麼編譯器就會爲該class合成一個inline的default constructor,合成的constructor將會按照member class object聲明順序調用其member object。
class Foo {
public:
Foo(){ /* ...*/ };
Foo(int i){ /* ... */ };
}
class Bar{
public:
Foo foo;
char *str;
}
//Bar合成的default 構造函數很有可能是
inline Bar::Bar(){
//Bar::str的初始化是程序員的責任
foo.Foo::Foo();
}
由於合成的默認構造函數並不會初始化其他nonstatic data member(如整數、指針、數組),所以需要程序員來進行初始化操作,而編譯器可以對程序員寫的每個構造函數進行擴張,使其滿足編譯器需求。
//假如我們寫一個默認構造函數
inline Bar::Bar(){
str = 0;
}
//它會被擴張成
inline Bar::Bar(){
foo.Foo::Foo();
str = 0;
}
假如我們不要調用Member Class Object的默認構造函數,我們可以這樣:
inline Bar::Bar() : foo( 1024 ) {//這裏並不會改變擴張後的調用順序
str = 0;
}
//他會被擴張成
inline Bar::Bar(){
foo.Foo::Foo( 1024 );
str = 0;
}
1.2 帶有Default Constructor的Base Class(一個派生類的基類具有默認構造函數。)
與1.1類似,若一個沒有默認構造函數的class派生自一個有默認構造函數的base class,那麼它將按照base classes的聲明順序調用上一層base classes的default constructor,也與1.1一樣可以對每個constructor進行擴張操作。
1.2的操作將優先於1.1的操作進行。
1.3 帶有一個Virtual Function的class(帶有虛函數的類。 )
若class聲明或繼承一個virtual function,編譯器會在編譯期間合成default constructor或擴張所有的構造函數進行以下操作:
- 產生一個virtual function table (即vtbl),內放class的virtual functions地址。
- 在每個class object中合成一個額外的pointer member (即vptr),指向相關class vtbl地址。
同時虛函數的虛擬調用操作將會重新改寫爲使用vptr和vtbl的條目。
class Widget {
public:
virtual void flip() = 0; //pure virtual
};
class Bell : public Widget{};
class Whistle : public Widget{};
void flip(const Widget& widget) {
widget.flip(); //將被改寫爲
//(*widget.vptr[ 1 ] )( &widget);
}
void foo(){
Bell b;
Whistle w;
flip(b);
filp(w);
}
1.4 帶有Virtual Base Class 的 class(一個派生類,該派生類的繼承體系中含有虛基類(虛繼承)。)
必須使virtual base class在其每一個derived class object中的位置,可以在執行期準備妥當。
可以使編譯器在class object構造使其安插一個指向虛基類的指針_vbcX,然後所有僅有reference或pointer來存取virtual base class的操作都可以通過相關指針完成。
class X { public: int i; }
class A : public virtual X { public: int j; }
class B : public virtual X { public: double d; }
class C : public A, public B { public : int k; }
//無法在編譯期決定pa->X::i的位置
void foo(const A* pa) { pa->i = 1024; }
//可能會被編譯器轉變成
void foo(const A* pa) { pa->_vbcX->i = 1024; }
main{
foo(new A);
foo(new C);
}
1.5 一些誤區的實際情況
- 並不是任何class沒有定義default constructor就一定會合成一個出來。
- 合成出的default constructor也並不會設定class內每一個data member的默認值。
2 Copy Constructor的構造操作
有三種情況會使用copy constructor:
- 顯式的對一個object做一個初始化操作.
- object通過傳值交給函數。
- 函數返回一個非引用class object。
2.1 Default Memberwise Initialization(默認成員逐一初始化)
如果一個class沒有提供任何的copy construct,那麼在進行拷貝構造是,class內部是以default memberwise initialization的手法完成的。實際上就是bitwise copies(位逐次拷貝),即把class object中的所有data members按順序一個一個拷貝到另一個object身上,如果有data members是類類型,那麼就會遞歸地施行bitwise copies。
class String{
public:
//.......無copy constructor
private:
char *str;
int len;
};
class Word {
public:
Word(int i, String s) : _occurs(i), _word(s) {}
private:
int _occurs;
String _word; //String object稱爲一個data member
};
Word word1(2, "word");
Word word2 = word1;
那麼最後一行很有可能會是這樣的,這不是copy constructor,而是default memberwise initialization!
word2._occurs = word1._occurs;
//word2._word = word1._word;
//遞歸展開
word2._word.str = word1._word.str;
word2._word.len = word1._word.len;
2.2 不要 Bitwise Copy Semantics
C++ Standard說若class沒有聲明一個copy constructor就會有隱式的聲明或定義。實際上只有nontrivial的實力纔會被合成在程序中,即只有class不展示出bitwise copy semantics的時候.例如
class String{
public:
String( const char* );
String( const String& );
private:
char *str;
int len;
};
class Word {
public:
Word(int i, String s) : cnt(i), str(s) {}
private:
int cnt;
String str; //String object稱爲一個data member
};
這種情況中Word沒有展示出bitwise copy semantics,故會生成copy constructor:
inline Word::Word( const Word& wd){
str.String::String( wd.str );
cnt = wd.cnt;
}
因此,一個類的copy語意有三種情況:
- 存在copy constructor,使用copy constructor;
- 下面上面的四種情況,編譯器幫助合成copy constructor;
- bitwise copy;
有四種情況表示class不展示出bitwise copy semantics:
- 當class中含有一個聲明有copy constructor的member object時。(無論是顯式還是被合成)
- 當class繼承自有一個聲明有copy constructor的base object時。(無論是顯式還是被合成)
- 當class聲明瞭virtual functions。
- 當class派生自一個繼承鏈串,其中有virtual base classes時。
第一種和第二種情況不必多說,下面主要說第三第四種情況,當編譯器導入一個vptr到class的時候,該class就不再展現bitwise semantics了。
2.2.1 重新設定Virtual Table的指針
現在,編譯器需要合成一個copy constructor以求將vptr適當的初始化。假設有這樣的繼承關係:
class ZooAnimal {
public:
ZooAnimal();
virtual ~ZooAnimal();
virtual void draw();
virtual void animate();
};
class Bear : public ZooAnimal {
public:
Bear();
virtual ~Bear();
void draw(); //virtual function
void animate();
virtual void dance();
};
當一個class object以其class的另一個class爲初始值時,種情況都可以直接靠“bitwise copy semantics”來完成(除了pointer member)。、例如一個ZooAnimal class object以另一個ZooAnimal class object爲初值或者Bear class object以另一個Bear class object爲初值:
Bear yogi;
Bear winnie = yogi;
相同 class 的 objects 的 vtbl 相同。在constructor中,yogi的vptr被設定指向Bear class的virtual table。
當一個base class object以其derived class的object爲初值時,合成的顯式構造函數會設定object的vptr指向base class的virtual table,而不是之前從右手邊的object拷貝。
void draw( const ZooAnimal& zoey ) { zoey.draw(); }
void foo(){
ZooAnimal franny = yogi;//發生切割,franny的vptr指向ZooAnimate的vtbl而非Bear的
draw(yogi); //調用Bear::draw()
draw(franny); //調用ZooAnimate::draw()
2.2.2 處理Virtual Base Class Subobject
virtual base class需要特別處理,編譯期必須讓處於derived class object中的virtual base class subobject的位置在執行期就準備妥當。
假如有這樣的繼承關係:
class Raccoon : public virtual ZooAnimal {
public:
Raccoon() {}
Raccoon(int val) {}
};
class RedPanda : public Raccoon {
public:
RedPanda() {}
RedPanda(int val) {}
};
如果一個class object以其derived classes的某個object爲初值(例如一個Raccoon object作爲另一個Raccoon object的初值),“bitwise copy”就足夠了。
Raccoon rocky;
Raccoon little_critter = rocky;
但是如果以derived object作爲base object的初值,如以RedPanda object作爲Raccoon object的初值,編譯器必須判斷**“能否正常執行存取ZooAnimal的subobject的動作”(進行切割),這種情況下編譯器必須合成一個copy constructor,安插一些代碼以設定virtual base class pointer/offset的初值**,對每個members執行必要的memberwise初始化以及執行其他的內存相關工作。
RedPanda little_red;
Raccoon little_critter = little_red;
對指針而言,“bitwise copy”可能夠用,也可能不夠用,因爲編譯器無法知道一個base class指針是否指向一個真正的base class object,或是指向一個derived class object。
3.程序轉化語意學
#include <iostream>
using namespace std;
//加載頭文件
#include "X.h"
X foo(){
X x_1;
//對對象x_1進行處理的相關操作。
return x_1;
}
兩種正常假設:
- 每調用一次foo()函數,會返回一個對象x_1的值。
- .應該會調用類中的拷貝構造函數。
兩個假設的正確性需要參看類X中的定義。
3.1 顯式的初始化操作(Explicit Initialization)
若有下面程序
void foo_bar(){
X x1(x0);
X x2 = x0;
X x3 = X(x0);
}
上面三種初始化操作顯式的用x0初始化三個對象。但是在實際的編譯器中可能會發生如下的轉換。
- 重寫定義,其中的初始化操作會被剝離 。
- 調用相關的拷貝構造函數。
轉化後的代碼有可能如下
void foo_bar(){
X x1;
X x2;
X x3;
x1.X::X(x0); //調用拷貝構造函數。
x2.X::X(x0);
x3.X::X(x0);
//可能在類X中會有類似的聲明:
//X::X(const X&);
}
3.2 參數的初始化
當將一個class object作爲參數以傳值方式給一個函數或作爲一個非引用返回值,將會導入臨時對象策略,調用copy constructor將它初始化,然後將臨時對象交給對象(或返回),同時根據需要將參數或返回值改爲引用。假如有代碼
void foo( X x0 );
X xx;
foo(xx);
可能會轉換如下:
//第一步
void foo( X& x0 );
X xx;
//第二步
X __temp0;
__temp0.X::X( xx );
foo( __temp0 );
//第三步
__temp0.X::~X();
3.3返回值的初始化
對於返回值做兩階段的轉化:
- 加入一個類型爲class object的引用的額外參數作爲返回值。
- 在return前安插一個copy constructor。將要返回的結果拷貝給引用的額外參數。(故要求一定要有拷貝構造函數才啓動)
假如有代碼:
X bar(){
X xx;
return xx;
}
//1
X xx = bar();
//2
bar().mem_func();
//3
X (*pf)();
pf = bar();
很有可能改寫如下:
void bar(X& __result){
X xx;
xx.X::X();
__result.X::X(xx);
return;
}
//1
X xx;
bar( xx );
//2
X __temp0;
(bar( __temp0 ), __temp0 ).mem_func();
//3
X (*pf)( X& );
pf = bar();
3.4 在編譯器層次做優化(NVR優化)
對返回值進一步優化,並不是在最後將要返回的結果拷貝給引用的額外參數,而是直接使用額外參數作爲要返回的對象進行操作。
X bar(){
X xx;
//對xx操作
return xx;
}
很有可能改寫如下:
void bar(X& __result){
__result.X::X();
//直接處理__result
return;
}
3.5 是否需要copy constructor?
假如class需要大量的memberwise(深拷貝)初始化操作,例如以傳值(by value)的方式傳回object,那麼提供一個copy constructor的explicit inline函數實例就非常合理了。
3.6成員們的初始化隊伍(Member Initialization List)
下列四種情況必須使用成員初始化列表:
- 當初始化一個reference member時;
- 當初始化一個const member時;
- 當調用一個base class的constructor,而它擁有一組參數時;
- 當調用一個member class的constructor,而它擁有一組參數時;
實際上在四種情況下不用Initialization List仍然可以正確編譯執行,但是效率低。
class Word {
String _name;
int _cnt;
public:
Word() {
_name = 0;
_cnt = 0;
}
};
在以上程序中Word constructor會產生臨時String對象,初始化後再給_name,最後再摧毀臨時對象:
Word::Word {
_name.String::String();
String temp = String(0);
_name.String::operator=(temp);
temp.String::~String();
_cnt = 0;
}
正確高效的方法如下是
Word::Word() : _name(0), _cnt(0) {}
擴張後的結果爲
Word::Word() {
_name::String::String(0);
_cnt = 0;
}
成員初始化列表中的初始化順序是按照聲明順序來的,與initialization list順序無關,並且initialization list的項目要先於explicit user code。