2.4 成員們的初始化佇列(Member Initialization List)
當你寫下一個constructor時,就有機會設定class members的初值。為了讓程式能夠被順利編譯,有4種情況你必須使用member initialization list:
- 當初始化一個reference member時;
- 當初始化一個const member時;
- 當呼叫一個base class的constructor,而它有用一組引數時;
- 當呼叫一個member class 的constructor,而它有用一組引數時。
當著4種環境,你使用constructor函式體內初始化,程式可以被正確編譯並執行,但是效率不高。例如:
class Word { String _name; int _cnt; public: //沒有錯誤,效率不佳 Word() { _name = 0; _cnt = 0; } };
在這裡,Word constructor會先產生一個臨時的String Object,然後將它初始化,之後以一個assignment運算子將臨時object指定給_name,隨後再摧毀臨時性的object。下面是constructor可能的內部擴張:
Word::Word() { //呼叫String的default constructor _name.String::String(); //產生臨時物件 String temp = String(0); //“memberwise”地拷貝_name _name.String::operator=(temp); //摧毀臨時物件 temp.String::~String(); _cnt = 0; }
如果使用member initialization list初始化,效率會明顯提高:
//較佳的效率
Word::Word():_name(0)
{
_cnt = 0;
}
member initialization list中的專案順序是由class的members宣告順序決定的,不是由initialization list中的排列順序決定的。有些程式設計師堅持所有的member初始化操作必須在member initialization list中完成,甚至是內建型別的member。
初始化順序和initialization list中的專案排列順序之間的外觀錯亂,會導致下面意想不到的危險:
class X
{
int i;
int j;
public:
X(int val):
j(val),i(j)
{}
//...
};
上述程式碼看起像是把j設定為val,再把i的值設定為j。問題在於,宣告順序的緣故,initialization list中的i(j)比起j(val)執行更早。所以i(j)的值未定義的。
這個“臭蟲”的困難度在於不容易被觀察出來。要不就如下面的放在constructor中:
class X
{
int i;
int j;
public:
X(int val):
j(val)
{
i = j;
}
//...
};
當constructor擴張時候,因為initialization list的專案被編譯器安插到explicit user code之前,所以上述程式碼能正確賦值。
另一個常見問題是,是否你能夠像下面一樣,呼叫一個member function以設定一個member初值:
//X::xfoo()被呼叫,這樣好嗎?
X::X(int val)
:i(xfoo(val)),j(val)
{}
其中xfoo()是X的一個member function。忠告是請使用“存在於constructor體內的一個member”,不要使用“存在於member initialization list中的member”,來為另一個member 設定初值。你並不知道xfoo()對X object 依賴度多高,如果你把xfoo()放在constructor裡面,那麼對於“到底是哪一個member 在xfoo()執行時被設定初值”這件事,可以確保不會發生模凌兩可的情況。
Member function的使用是合法的(當然我們必須不考慮它使用的的member function是否被初始化),這是因為和此object相關的this指標已經被建構妥當,而constructor大約被擴充為:
X::X(/*this pointer*,/int val)
{
i = this->xfoo(val);
j = val;
}
最後,如果一個derived class member function被呼叫,其返回值被當作base class constructor的一個引數,將會如何:
//呼叫FooBar::fval()可以嗎?
class FooBar: public X
{
int _fval;
public:
int fval(){return _fval;};
FooBar(int val)
:_fval(val),
X(fval()) //fval()作為base class constructor引數
};
它可能的擴張結果是:
FooBar::FooBar(/*this pointer goes here*/)
{
X::X(this,this->fval());
_fval = val;
}
它的確不是一個好主意。
簡答的講,編譯器會對initialization list 一一處理並可能重新排序,以反應出member 的宣告次序。它會安插一些程式碼到constructor體內,並置於任何explicit user code之前。