建構函式constructor
explicit
的引入,是為了能夠制止“單一引數的constructor”被當作一個conversion運算子
帶有預設建構函式的物件成員
若一個類中包含物件成員,且該物件有預設建構函式,此時:
- 若該類沒有建構函式
則編譯器會合成一個預設建構函式,且發生在真正呼叫時 - 若該類有建構函式,但沒有初始化物件成員
則在已有建構函式中按宣告順序生成構造物件成員的程式碼,且發生在構造最開始的時候
被合成的預設建構函式,只滿足編譯器的需要,而不是程式的需要
如初始化類物件成員是編譯器的需要,初始化其他類成員變數則是程式的需要
編譯器是如何避免合成多個預設建構函式的呢?如在不同檔案中構造同一個類
編譯器會將合成的預設建構函式,複製建構函式,解構函式,賦值運算子過載以inline
的方式完成,inline
函式有靜態連結static linkage,不會被檔案以外者看到,若函式太複雜,則會合成出一個explicit non-inline static例項
帶有預設建構函式的父類
- 若子類沒有建構函式,則會合成預設建構函式呼叫父類建構函式
- 若子類已有建構函式,則會生成呼叫父類建構函式的程式碼,且發生在初始化物件成員之前
帶有虛擬函式的類
還有兩種情況也需要合成預設建構函式
- 類中宣告瞭虛擬函式
- 類的繼承鏈中,包含虛繼承關係
class Widget{
public:
virtual void flip()=0;
};
void flip(const Widget& w){ w.flip(); }
會生成虛擬函式表vtbl
和其指標vptr
w.flip()
的虛呼叫操作會被改寫,以滿足多型
(*w.vptr[1])(&w); // &w 為當前物件的this指標
在初始化時,編譯器會給每個物件的vptr
設定初值,放置適當的vtbl
地址
此時會合成,或在已有建構函式中生成相關初始化程式碼
帶有虛基類的類
class X{ public: int i; };
class A:public virtual X{ public: int j; };
class B:public virtual X{ public: int n; };
class C:public A,public B{ public: int m; };
void foo(const A* pa){ pa->i=1024; }
編譯器無法知道X::i
的實際偏移量(經由pa
存取),因為pa
的真正型別是可改變的
編譯器必須改變“執行存取操作”的程式碼,使X::i
可以延遲至執行期決定
cfront的做法是在子類物件中安插虛基類指標
如pa->_vbcX->i=1024;
,其中_vbcX
是編譯器產生的指標,指向虛基類X
此時會合成,或在已有建構函式中生成相關程式碼,來初始化該指標
為什麼虛擬函式和虛基類只有在執行時才能確定
因為編譯器不解析賦值操作
複製建構函式copy constructor
如下場景,會呼叫類的複製建構函式
物件的顯式初始化操作
X xx=x;
物件作為引數傳遞給函式
extern void foo(X x);
foo(xx);
物件作為函式返回值
X foo_bar(){
X xx;
return xx;
}
default memberwise initialization
若一個類沒有提供顯式的複製建構函式,則在複製構造時,內部以default memberwise initialization方式完成
即複製原生型別的成員變數,但對於成員物件,會採用遞迴的方式施行memberwise initialization
(在同類物件間複製構造時)
決定一個類是否生成預設的複製建構函式,在於該類是否展現出bitwise copy semantics
(不展現才會生成)
位逐次複製bitwise copy semantics
以下宣告展現了bitwise copy semantics
class Word{
public:
Word(const char*);
~Word(){ delete[] str; }
private:
int cnt;
char* str;
};
這種情況下,不需要合成一個default copy constructor,因為上述宣告展現了default copy semantics
而物件的複製初始化操作也就不需要以一個函式呼叫收場
什麼時候一個類不展現bitwise copy semantics呢?
1.成員變數中包含成員物件,且物件的型別有複製建構函式(包括編譯器生成)
2.繼承自父類,且父類有複製建構函式(包括編譯器生成)
3.類中宣告瞭虛擬函式
4.類的繼承鏈中,包含虛繼承關係
前兩種情況,會在編譯器預設生成的複製建構函式中,呼叫相關成員物件或父類的複製建構函式
第三種情況,以子類物件初始化父類物件時,需要保證vptr
的操作安全,此時生成的父類的複製建構函式會設定vptr
的值,而不是直接從子類中複製,這也解釋了上一章程式碼
ZooAnimal za= b;
za.rotate();
呼叫的是ZooAnimal::rotate()
第四種情況
class Raccoon:public virtual ZooAnimal{
public:
Raccoon(){}
Raccoon(int val){}
private:
};
class RedPanda:public Raccoon{
public:
RedPanda(){}
RedPanda(int val){}
private:
};
若以一個Raccoon object作為另一個Raccoon object的初值,則bitwise copy綽綽有餘
若以RedPanda object作為Raccoon object的初值,編譯器則需要生成複製建構函式,並初始化virtual base class pointer/offset
下面這種情況,編譯器無法知道bitwise copy semantics是否還保持,因為無法知道Raccoon指標是指向Raccoon object還是derived class object
Raccoon *ptr;
Raccoon little_critter= *ptr;
程式轉化program transformation
顯式初始化
T t1(t0);
T t2= t0;
T t1= T(t0);
會轉化成:先宣告,再複製構造
T t1;
T t2;
T t3;
t1.T::T(t0);
t2.T::T(t0);
t3.T::T(t0);
引數初始化
將一個class object當作函式實參,或函式返回值
引數傳遞時,會以memberwise方式進行
在編譯器實現計算上,有以下兩種轉化策略
策略一
引入臨時性物件,並用複製建構函式初始化,再以bitwise方式傳遞給形參
函式形參也必須被轉化,需要以引用方式宣告
策略二
將實參物件實際複製構造在函式堆疊中
返回值初始化
cfront中採用雙階段轉化
- 宣告一個class object的引用
__result
- 在return前,使用返回值來複製構造傳入的引用
對於函式指標
X (*pf)();
pf= bar;
轉化為
void (*pf)(X&);
pf= bar;
在使用者層面最佳化
定義一個建構函式constructor,可以直接計算返回值,而不是呼叫複製建構函式
在編譯器層面最佳化
將返回值直接使用__result
代替,稱為Named Return Value(NRV)最佳化
NRV最佳化現在被認為是C++編譯器義不容辭的最佳化操作
如下程式碼
class Test{
friend Test foo(double);
public:
Test(){
memset(arr, 0, 100*sizeof(double));
}
private:
double arr[100];
};
此時,編譯器不會做NRV最佳化,因為沒有複製建構函式,如下加上inline copy constructor
inline Test(const Test& t){
memcpy(this, &t, sizeof(test));
}
是否需要複製建構函式
沒有任何理由要提供一個複製建構函式,因為編譯器自動實施了最好的行為
若一個class要大量memberwise初始化操作,則提供一個copy constructor的explicit inline函式例項是合理的(在編譯器提供NRV的前提下)
若使用更有效率的memset()
或memcpy()
作為複製建構函式的實現,則需要在class中不含任何由編譯器產生的內部成員
成員的初始化
必須使用成員初始化列表的情況
1.初始化引用reference成員
2.初始化const
成員
3.呼叫基類建構函式,且其有一組引數
4.呼叫成員的建構函式,且其有一組引數
class Word{
String name_;
int cnt_;
public:
Word(){
name_= 0;
cnt_= 0;
}
};
編譯器會生成一個臨時物件,可能的轉化如下
public:
Word(){
// 預設構造
name_.String::String();
// 臨時物件
String temp= String(0);
// memberwise複製
name_.String::operator=(temp);
temp.String::~String();
cnt_= 0;
}
若使用列表初始化
Word::Word():name_(0){
cnt_= 0;
}
編譯器可能的轉化如下
Word::Word() {
// 直接呼叫建構函式
name_.String::String(0);
cnt_= 0;
}
成員初始化列表到底做了什麼,是不是簡單的函式呼叫
可以回答,當然不是簡單的函式呼叫
編譯器會在建構函式中生成程式碼,將初始化列表中的變數按在類中的宣告順序初始化
如下程式碼
class X{
int i;
int j;
X(int val):j(val),i(j){}
};
由於宣告順序的緣故,i
會比j
先執行,會導致問題,這種情況,GNU C++編譯器g++會做出告警
建議做出如下調整
class X{
int i;
int j;
X(int val):j(val){
i= j;
}
};
能否呼叫成員函式,以初始化資料成員
X::X(int val):i(xfoo(val)),j(val){}
成員函式的使用是合法的,因為此時this
指標已經被構造
編譯器可能的生成程式碼如下
X::X(int val){
i= this->xfoo(val);
j= val;
}
能否使用子類成員函式返回值,作為父類建構函式的實參
class FooBar:public X{
int fval_;
public:
int fval(){ return fval_; }
FooBar(int val):fval_(val),X(fval()){}
};
編譯器可能的生成程式碼如下
FooBar::FooBar(int val){
X::X(this, this->fval());
fval_= val
}
可知,這不是一個好主意