程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 《深度探索C++對象模型》讀書筆記(2)

《深度探索C++對象模型》讀書筆記(2)

編輯:關於C++

default constructor僅在編譯器需要它時,才會被合成出來。

通常來說,由編譯器合成出來的default constructor是沒啥用的(trivial),但有以下幾種例外:

(1)帶有“Default Constructor”的Member Class Object

如果一個class沒有任何 constructor,但它內含一個member object,而後者有default constructor,那麼編譯器會在 constructor真正需要被調用時未此class合成一個“nontrivial”的default constructor. 為了避免合成出多個default constructor,解決方法是把合成的default constructor、copy constructor、destructor、assignment copy operator都以inline方式完成。一個inline函數有靜態鏈 接(static linkage),不會被檔案以外者看到。如果函數太復雜,不適合做成inline,就會合成出一 個explicit non-inline static實體。

根據准則“如果class A內含一個或一個以上的 member class objects,那麼class A的每一個constructor必須調用每一個member classes的default constructor”,即便對於用戶明確定義的default constructor,編譯器也會對其進行擴張,在 explicit user code之前按“member objects在class中的聲明次序”安插各個member所關聯 的default constructor.

class Dcpey   { public:Dopey(); ... };
class Sneezy  { public:Sneezy(int); Sneezy(); ... };
class Bashful { public:Bashful(); ... };

class Snow_White {
public:
Dopey dopey;
Sneezy sneezy;
Bashful bashful;
// ...
private:
int mumble;
};

Snow_White::Snow_White() : sneezy(1024)
{
mumble = 2048;
}
// 編譯器 擴張後的default constructor
Snow_White::Snow_White() : sneezy(1024)
{
// 插 入member class object
// 調用其constructor
dopey.Dopey::Dopey();
sneezy.Sneezy::Sneezy();
bashful.Bashful::Bashful();

// explicit user code
mumble = 2048;
}

(2)“帶有Default Constructor”的 Base Class

如果一個沒有任何constructors的class派生自一個“帶有default constructor”的base class,那麼這個derived class的default constructor會被視為 nontrivial,並因此需要被合成出來。

需要注意的是,編譯器會將這些base class constructor 安插在member object之前。

(3)“帶有一個Virtual Function”的Class

另有兩種情況,也需要合成出default constructor:

(a)class聲明(或繼承)一個virtual function

(b)class派生自一個繼承串鏈,其中有一個或更多的virtual base classes

以下面這個程序片段為例:

class Widge {
public:
virtual void flip() = 0;
// ...
};

void flip(const Widge& widge) { widge.flip(); }

// 假設Bell和Whistle都派生自Widge
void foo()
{
Bell b;
Whistle w;

flip(b);
flip(w);
}

下面兩個擴張操作會在編譯期間發生:

1.一個virtual function table會被編譯器產生出來,內放class的virtual functions地址;

2.在每一個class object中,一個額外的pointer member(也就是vptr)會被編譯器合成出來, 內含相關的class vtbl的地址。

此外,widge.flip()的虛擬引發操作(virtual invocation) 會被重新改寫,以使用widge的vptr和vtbl中的flip()條目。

// widge.flip()的虛擬引 發操作的轉變
(*widge.vptr[1])(&widge)

為了讓這個機制發揮功效,編譯器 必須為每一個Widge(或其派生類)之object的vptr設置初值,放置適當的virtual table地址。

(4)“帶有一個virtual Base Class”的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; }

main()
{
foo(new A);
foo(new C);
// ...
}

編 譯器需要在derived class object中為每一個virtual base classes安插一個指針,使得所有“經 由reference或pointer來存取一個virtual base class”的操作可以通過相關指針完成。

// 可能的編譯器轉變操作
// _vbcX表示編譯器所產生的指針,指向virtual base class X
void foo(const A* pa) ...{ pa->_vbcX->i = 1024; }

小結:在合成的default constructor中,只有base class subobjects和member class objects會被初始化,所 有其它的nonstatic data member都不會被初始化。

有三種情況會以一個object的內容作為另一 個class object的初值,即object賦值、object參數傳遞、object作為函數返回值。

如果class 沒有提供一個explicit copy constructor,其內部是以所謂的default memberwise initialization手 法完成的,也就是把每一個內建的或派生的data member(例如一個指針或一個數組)的值,從某個 object拷貝一份到另一個object身上,不過它並不會拷貝其中的member class object,而是以遞歸的方 式施行memberwise initialization. copy constructor僅在必要的時候(class不展現bitwise copy semantics)才由編譯器產生出來。

已知下面的class Word聲明:

// 以下聲明展 現了bitwise copy semantics
class Word {
public:
Word( const char* );
~Word(){ delete []str; }
// ...
private:
int cnt;
char *str;
};

對於上述這種情況,並不需要合成出一個default copy constructor,因為上述聲 明展現了“default copy semantics”。

然而,如果class Word是這樣聲明:

// 以下聲明未展現出bitwise copy semantics
class Word {
public:
Word( const String& );
~Word();
// ...
private:
int cnt;
String& str;
};

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

// 一個被合成出來的copy constructor
inline Word::Word(const Word& wd)
{
str.String::String (wd.str);
cnt = wd.cnt;
}

一個class不展現出“bitwise copy semantics”的四種情況:(1)當class內含一個member object而後者的class聲明有一個copy constructor時(無論是被明確聲明或被合成而得)

(2)當class繼承自一個base class而後者 存在有一個copy constructor時

對於前兩種情況,編譯器必須將member或base class的 “copy constructor調用操作”安插到被合成的copy constructor中。

(3)當class 聲明了一個或多個virtual functions時

由於編譯器要對每個新產生的class object的vptr設置 初值,因此,當編譯器導入一個vptr到class之中時,該class就不再展現bitwise semantics了。

當一個base class object以其derived class的object內容做初始化操作時,其vptr復制操作必 須保證安全,而如果依舊采用bitwise copy的話,base class object的vptr會被設定指向derived class的virtual table,而這將導致災難。

(4)當class派生自一個繼承串鏈,其中有一個或多 個virtual base classes時當一個class object以其derived classes的某個object作為初值時,為了完 成正確的virtual base class pointer/offset的初值設定,編譯器必須合成一個copy constructor,安 插一些碼以設定virtual base class pointer/offset的初值,對每一個member執行必要的memberwise初 始化操作,以及執行其他的內存相關操作。在這種情況下,簡單的bitwise copy所做的就遠遠不夠了。

***優化***

(1)在使用者層面做優化定義一個“計算用”的constructor:

X bar(const T &y,const T &z)
{
X xx;
// ... 以y和z來處 理xx
return xx;
}

上述constructor要求xx被“memberwise”地 拷貝到編譯器所產生地_result之中。故可定義如下的constructor,可以直接計算xx的值:

X bar(const T &y,const T &z)
{
return X(y,z);
}

(2)在編譯器層面做優化比較以下三處初始化操作:

X xx0(1024);
X xx1 = X(1024);
X xx2 = (X)1024;

其中,xx0是被單一的constructor操作設 定初值:

xx0.X::X(1024);

而xx1和xx2則是調用兩個constructor,產生一個暫時性 的object並設以初值1024,接著將暫時性的object以拷貝建構的方式作為explicit object的初值,最後 還針對該暫時性object調用class X的destructor:

X _temp0;
_temp0.X::X (1024);
xx1.X::X(_temp0);
_temp0.X::~X();

由此可以看出,編譯器所做的 優化可導致機器碼產生時有明顯的效率提升,缺點則是你不能安全地規劃你的copy constructor的副作 用,必須視其執行而定。

那麼,究竟要不要copy constructor?copy constructor的應用,迫使 編譯器多多少少對你的程序代碼做部分優化。尤其是當一個函數以傳值(by value)的方式傳回一個 class object,而該class有一個copy constructor時,這將導致深奧的程序轉化。

舉個簡單的 例子,不管使用memcpy()或memset(),都只有在“classes不含任何由編譯器產生的內部 members”時才能有效運行。而如下的class由於聲明了一個virtual function,編譯器為其產生了 vptr,此時若使用上述函數將導致vptr的初值被改寫。

class Shape ...{
public:
// 這會改變內部的vptr
Shape() { memset(this,0,sizeof(Shape)); };
virtual ~Shape();
// ...
};

// 擴張後的constructor
Shape::Shape()
{
// vptr必須在使用者的代碼執行之前先設定妥當
_vptr_Shape = _vtbl_Shape;

// memset會將vptr清為0
memset(this,0,sizeof(Shape));
}

***成員的初始化隊伍***

下列情況中,為了讓你的程序能夠被順利編譯,你必須 使用member initialization list:

(1)當初始化一個reference member時;

(2)當 初始化一個const member時;

(3)當調用一個base class的constructor,而它擁有一組參數時 ;

(4)當調用一個member class的constructor,而它擁有一組參數時。

成員初始化列 表有時可帶來巨大的性能提升,不妨看看下面一組對比:

class Word {
String _name;
int _cnt;
public:
Word() {
_name = 0;
_cnt = 0;
}
};

// 其constructor可能的內部擴張結果
Word::Word()
{
// 調用 String的default constructor
_name.String::String();

// 產生暫時性對象
String temp = String(0);

// “memberwise”地拷貝_name
_name.String::operator=(temp);

// 摧毀暫時性對象
temp.String::~String ();

_cnt = 0;
}

若使用成員初始化列表:

// 較好的方 式
Word::Word : _name(0)
{
_cnt = 0;
}

Word::Word()
{
// 調用String(int) constructor
_name.String::String(0);
_cnt = 0;
}

需要注意的是,成員初始化列表中的項目是依據class中的member聲明次序,一一安插到 explicit user code之前的。(因此,對於表達式兩邊均出現member的情形要特別小心)

下面就 給出錯誤實例:

class X {
int i;
int j;
public:
// i比j先被 賦值,而此時j尚未有初值
X(int val) : j(val),i(j)
{ }
...
};

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved