條款04:肯定對象使用前已被初始化

1. 總結

  • 不管是在初始化列表中,仍是在構造函數體內,請爲內置類型對象進行手工初始化,由於C++不保證初始化它們
  • 最好使用初始化列表進行初始化,而不要在構造函數體中使用賦值;初始化列表最好列出全部的成員變量,其排列順序應該和它們在class中的聲明順序相同
  • 爲了不"不一樣源文件內定義的non-local static對象在編譯時的初始化順序"問題,請以local static對象替換non-local static對象

2. 構造函數體 VS 初始化列表

在C++中,關於對象的初始化動做什麼時候必定發生,什麼時候不必定發生這個問題,最佳的處理辦法就是:永遠在使用對象以前先將它初始化。安全

  • 對於內置類型對象,因爲C++不保證是否初始化以及什麼時候初始化它們,所以不管是在初始化列表中,仍是在構造函數體內,你必須手工完成這項工做
  • 對於自定義類型對象,初始化工做由構造函數進行,規則也很簡單:確保每個構造函數都將對象的每個成員初始化

關於在構造函數中初始化,重要的一點是不要混淆了賦值和初始化。函數

class PhoneNumber { ... };
class ABEntry
{
private:
    std::string theName;
    std::string theAddress;
    std::list<PhoneNumber> thePhones;
    int numTimesConsulted;
public:
    ABEntry{const std::string &name, const std::string &address,
            const std::list<PhoneNumber> &phones};
};

/* 正確可行但不是最好的方法:在構造函數體內對成員變量進行賦值 */
ABEntry::ABEntry{const std::string &name, const std::string &address,
                 const std::list<PhoneNumber> &phones}
{
    theName = name;         //theName、theAddress、thePhones都是賦值,
    theAddress = address;   //而不是初始化。
    thePhones = phones;
    numTimesConsulted = 0;
}

/* 較好的方法:使用初始化列表對成員變量進行初始化 */
ABEntry::ABEntry{const std::string &name, const std::string &address,
                 const std::list<PhoneNumber> &phones}
        :theName(name),
         theAddress(address),
         thePhones(phones),
         numTimesConsulted(0)  //爲了一致性,內置類型對象初始化最好也在初始化列表中進行
{

}
  • 第一種方法基於賦值,首先調用default構造函數對theName、theAddress和thePhones進行初始化,而後進入構造函數,再分別對它們進行賦值。
  • 第二種方法基於初始化列表,在初始化列表中使用3個參數分別對theName、theAddress和thePhones進行copy構造初始化。

對大多數類型而言,比起先調用default構造函數而後再調用operator =,單隻調用一次copy構造函數是比較高效的,有時甚至高效得多。
而對於內置類型對象如numTimesConsulted,其初始化和賦值的成本是同樣的,但爲了一致性最好也經過初始化列表來初始化。線程

若是成員變量是const或reference,那麼不論是內置類型仍是自定義類型,都必定須要初值,不能被賦值,都只能經過初始化列表進行初始化(見條款5)。
爲避免須要記住什麼時候必須使用初始化列表,什麼時候不須要,最簡單的作法就是:設計

  • 老是使用初始化列表
  • 老是在初始化列表中列出全部成員變量
  • 初始化列表中成員變量的排列順序應該和它們在class中的聲明順序相同

可是,一些class有多個構造函數,並且有許多成員變量和/或base class,若是每一個構造函數都使用初始化列表,那麼就會形成大量的代碼重複。
這種狀況下,能夠合理地對"賦值和初始化開銷同樣"的成員變量改用賦值操做,並將這些賦值操做封裝到一個private init函數中,供全部構造函數調用。
這種作法在"成員變量的初始值來自於文件或數據庫讀入"時特別有用。然而,比起經由賦值操做完成的"僞初始化",經過初始化列表完成的"真正初始化"一般更加可取。code

3. 對象的初始化順序問題

C++在單個對象建立時有着十分固定的成員初始化順序,口訣就是"先父母,再客人,後本身"。對象

  • 先調用父類的構造函數
  • 再調用成員變量的構造函數,調用順序與聲明順序相同
  • 最後調用類自身的構造函數

若是已經在初始化列表中對base class和全部成員變量進行了初始化,那就只剩下一個問題——"不一樣源文件內定義的non-local static對象"的初始化問題。
先來明確下概念,函數內定義的static對象稱爲local static對象,其餘地方定義的static對象稱爲non-local static對象。
如今,咱們關心的問題涉及至少兩個源文件,每一個源文件中都至少含有一個non-local static對象,所以可能發生以下問題。string

  • 某個源文件中的non-local static對象初始化須要使用另外一個源文件中的non-local static對象
  • 但另外一個源文件內的non-local static對象可能還沒有被初始化

產生該問題的緣由是C++對不一樣源文件中的non-local static對象初始化順序沒有明肯定義,幸運的是經過一個小小的設計即可徹底消除該問題,惟一須要作的是:it

  • 將每一個non-local static對象放到本身的專屬函數中,這些函數返回一個reference指向它所含的對象
  • 而後用戶調用這些專屬函數,而不直接使用這些對象

該方法其實是用local static對象替換了non-local static對象,這也是Singleton模式的一個常見實現手法。該方法之因此管用,是由於:編譯

  • C++保證函數內的local static對象會在該第一次調用該函數時被初始化
  • 若是你從未調用過這些函數,就不會引起構造和析構成本

能夠看到,這種結構下的函數體每每十分簡單固定:第一行定義並初始化一個local static對象,第二行返回一個引用指向它。
這使得它們很是適合實現爲inline函數,尤爲是須要被頻繁調用的場合;但從另外一個角度看,內含static對象也使得它們成爲線程不安全函數。

class FileSystem { ... };

//static FileSystem tfs;  //FileSystem.cpp中定義的non-local static對象

//tfs的專屬函數,用來替換tfs對象
FileSystem &tfs()
{
    static FileSystem fs;  //定義並初始化一個local static對象fs
    return fs;             //返回一個reference指向上述對象
}
class Directory { ... };

Directory::Directory()
{
    //...
    std::size_t disks = tfs().numDisks();
    //...
}

//static Directory tempDir;  //Directory.cpp中定義的non-local static對象,tempDir的初始化依賴於FileSystem.cpp中的tfs對象先初始化完成

//tempDir的專屬函數,用來替換tempDir對象
Directory &tempDir()
{
    static Directory td;  //定義並初始化一個local static對象td
    return td;            //返回一個reference指向上述對象
}
相關文章
相關標籤/搜索