《深度探索C++物件模型》讀書筆記(2)。
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;
}
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(2)。
(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地址。
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(2)。
(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)才由編譯器產生出來。
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(2)。
已知下面的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了。
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(2)。
當一個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:
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(2)。
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,而它擁有一組參數時。
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(2)。
成員初始化列表有時可帶來巨大的效能提升,不妨看看下面一組對比:
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)
{ }
...
};
系列文章:
《深度探索C++物件模型》讀書筆記(1)
《深度探索C++物件模型》讀書筆記(3)
《深度探索C++物件模型》讀書筆記(4)
《深度探索C++物件模型》讀書筆記(5)
《深度探索C++物件模型》讀書筆記(6)
《深度探索C++物件模型》讀書筆記(7)
《深度探索C++物件模型》讀書筆記 最後一記