程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++對象模型——成員初始化列表(第二章)

C++對象模型——成員初始化列表(第二章)

編輯:關於C++

2.4 成員初始化列表 (Member Initialization List)

當編寫一個類的構造函數時,有可能設定類成員的初始值,或者通過成員初始化列表初始化,或者在構造函數內初始化,除了四種情況,其實任何選擇都差不多。
本節中,首先澄清何時使用初始化列表才有意義,然後解釋初始化列表內部的真正操作是什麼,然後再看一些微妙的陷阱。
下列情況中,為了讓程序能夠被順利編譯,必須使用成員初始化列表(不能在構造函數內初始化):
1. 當初始化一個引用的成員時
2. 當初始化一個常量成員( const member)時
3. 當調用一個基類的構造函數,而它擁有一組參數時
4. 當調用一個成員類對象的構造函數,而它擁有一組參數時

在這四種情況中,程序可以被正確編譯並執行,但是效率不佳。例如:
class Word {
private:
    String _name;
    int _cnt;
public:
    // 沒有錯誤,但是太naive
    Word() {
        _name = 0;
        _cnt = 0;
    }
};
在這裡,Word構造函數會先產生一個臨時性的String對象,然後將它初始化,再以一個賦值運算符將臨時對象指定給_name,然後再銷毀臨時對象。以下是構造函數可能的內部擴張結果:
// C++偽代碼
Word::Word( /* This pointer goes here */) {
    // 調用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;
}
    它會被擴張成這樣子:
// C++偽代碼
Word::Word( /* This pointer goes here */ ) {
    // 調用String(int) constructor
    _name.String::String(0);
    _cnt = 0;
}
順便一提,陷阱最可能發生在這種形式的template code中:
template 
foo::foo(type t) {
    // 可能是也可能不是個好主意
    // 視type的真正類型而定
    _t = 0;
}
這會引導某些程序員十分積極進取地堅持所有的成員初始化操作必須在成員初始化列表中完成,甚至即使是一個行為良好的成員如_cnt
// 堅持此種代碼風格
Word::Word()
    : _cnt(0), _name(0)
{}
成員初始化列表中到底發生了什麼事情?許多人對list的語法感到迷惑,誤以為它是一組函數調用,當然不是!
編譯器會一一操作初始化列表,以適當地次序在構造函數內插入初始化操作,並且在任何顯式用戶代碼之前。例如,先前的Word構造函數被擴張為:
// C++偽代碼
Word::Word( /* this pointer goes here */ ) {
    _name.String::String(0);
    _cnt = 0;
}
它看起來很像是在構造函數中指定_cnt的值,事實上,有一些微妙的地方要注意:列表中的項目次序是由類中成員的聲明次序決定的,而不是初始化列表中的排列順序決定的。在本例的Word類中,_name被聲明在_cnt之前,所以它的初始化比_cnt早。
初始化順序和初始化列表中的項目排列順序之間的差異,會導致下面意想不到的危險:
class X {
private:
    int i;
    int j;
public:
    X (int val)
        : j(val), i(j)
    {}
};
上述程序代碼看起來好像要把j設初值為val,然後把i設初值為j。但是由於聲明順序的原因,初始化列表中的i(j)比j(val)更早執行。而j開始並沒有初始值,所以i(j)的執行結果導致i會被初始化為一個無法預測的值。
這個bug的困難度在於它很不容易被觀察出來,編譯器應該發出一個警告消息。但是目前只有一個編譯器(g++)做到這一點。(測試VS2010確實沒有給出警告消息,Lippman確實牛掰,2000年提的問題,VS10都沒解決),X x(0);然後輸出x的i,j的值,結果如下圖所示:
\
i的值不正常,j值正常.VS10中並未給出任何警告信息.
還有一個有趣的問題,初始化列表中的項目插入到構造函數中,會繼續保存聲明次序嗎?也就是說,已知:
// 一個有趣的問題
X::x(int val) : j(val) {
    i = j;
}
j的初始化順序會插入在顯式用戶賦值操作(i=j)之前還是之後呢?
如果聲明次序繼續被保存,則這段代碼會出現很大問題(因為先要將i初始化,再將j初始化).事實上,這段代碼是正確的,因為初始化列表的項目被插入在顯式用戶代碼之前.
另一個常見的問題是,是否能夠如下所示,調用一個成員函數設置成員的初始值:
// X::xfoo()被調用
X::X(int val) : i(xfoo(val)), j(val)
{}
其中xfoo()是X的一個成員函數,答案是yes.但是最好使用存在於構造函數提內的一個成員,而不要使用存在於成員初始化列表中的成員,來為另一個成員設定初始值.並不確定xfoo()對X object的依賴性有多高,如果把xfoo()放在構造函數體內,那麼對於到底哪一個member在xfoo()執行時被設置初始值這件事,就可以給出確定的答案.
成員函數的使用是合法的,因為和此object相關的this指針已經被建構妥當,而構造函數大約被擴張為:
// constructor擴張後的結果
X::X( /* this pointer, */ int val) {
    i = this->xfoo(val);
    j = val;
}
如果一個派生類成員函數被調用,其返回值被當作基類構造函數的一個參數,將會如何?
// 調用FooBar::fval()可以嗎?
class FooBar : public X {
private:
    int _fval;
public:
    int fval() { return _fval; }             // derived class member function
    FooBar(int val) : _fval(val), X(fval())    // fval()作為base class constructor的參數
    {}
};
下面是它可能的擴張結果:
// C++偽代碼
FooBar::FooBar( /* this pointer goes here */ )
{
    X::X(this, this->fval());
    _fval = val;
}
它的確不是一個好主意.

總結

編譯器對初始化列表一一處理並可能重新排序,以反映出成員的聲明次序,它會插入一些代碼到構造函數體內,並且插入在任何顯式用戶代碼之前.

 

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