【深度探索C++對象模型】(2)構造函數語意學

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或擴張所有的構造函數進行以下操作:

  1. 產生一個virtual function table (即vtbl),內放class的virtual functions地址。
  2. 在每個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 一些誤區的實際情況

  1. 並不是任何class沒有定義default constructor就一定會合成一個出來。
  2. 合成出的default constructor也並不會設定class內每一個data member的默認值。

2 Copy Constructor的構造操作

有三種情況會使用copy constructor:

  1. 顯式的對一個object做一個初始化操作.
  2. object通過傳值交給函數。
  3. 函數返回一個非引用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:

  1. 當class中含有一個聲明有copy constructor的member object時。(無論是顯式還是被合成)
  2. 當class繼承自有一個聲明有copy constructor的base object時。(無論是顯式還是被合成)
  3. 當class聲明瞭virtual functions。
  4. 當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;
}

兩種正常假設:

  1. 每調用一次foo()函數,會返回一個對象x_1的值。
  2. .應該會調用類中的拷貝構造函數。

兩個假設的正確性需要參看類X中的定義。

3.1 顯式的初始化操作(Explicit Initialization)

若有下面程序

void foo_bar(){
	X x1(x0);
	X x2 = x0;
	X x3 = X(x0);
}

上面三種初始化操作顯式的用x0初始化三個對象。但是在實際的編譯器中可能會發生如下的轉換。

  1. 重寫定義,其中的初始化操作會被剝離 。
  2. 調用相關的拷貝構造函數。

轉化後的代碼有可能如下

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返回值的初始化

對於返回值做兩階段的轉化:

  1. 加入一個類型爲class object的引用的額外參數作爲返回值。
  2. 在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)

下列四種情況必須使用成員初始化列表:

  1. 當初始化一個reference member時;
  2. 當初始化一個const member時;
  3. 當調用一個base class的constructor,而它擁有一組參數時;
  4. 當調用一個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。

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