條款04:確定對象被使用前已先被初始化
Make sure that objects are initializedbefore they're used.
關於"將對象初始化"這事,C++ 似乎反復無常。在某些語境下內置類型和類的成員變量保證被初始化,但在其他語境中卻不保證。
讀取未初始化的值會導致不明確的行為。它可能讓你的程序終止運行,可能污染了正在進行讀取動作的那個對象,可能導致不可測知的程序行為,以及許多令人不愉快的調試過程。
最佳處理辦法就是:永遠在使用對象之前先將它初始化。無論是對於內置類型、指針還是讀取輸入流,你必須手工完成此事。
l 為內置型對象進行手工初始化,因為C++不保證初始化它們。
內置類型以外的任何其他東西,初始化則由構造函數完成,確保每一個構造函數都將對象的每一個成員初始化。
這個規則很容易奉行,重要的是別混淆了賦值和初始化。考慮一個用來表現通訊簿的class,其構造函數如下:
1. class PhoneNumber { ... };
2. class ABEntry { //ABEntry = "Address Book Entry"
3. public:
4. ABEntry(const std::string& name, const std::string& address,
5. const std::list<PhoneNumber>& phones);
6. private:
7. std::string theName;
8. std::string theAddress;
9. std::list<PhoneNumber> thePhones;
10. int numTimesConsulted;
11. };
12. ABEntry::ABEntry(const std::string& name, const std::string& address,
13. const std::list<PhoneNumber>& phones)
14. {
15. theName = name; //這些都是賦值(assignments),
16. theAddress = address;//而非初始化(initializations)。
17. thePhones = phones;
18. numTimesConsulted = 0;
19. }
這會導致ABEntry對象帶有你期望(你指定)的值,但不是最佳做法。C++ 規定,對象的成員變量的初始化動作發生在進入構造函數本體之前。在ABEntry構造函數內,theName, theAddress和thePhones都不是被初始化,而是被賦值。初始化的發生時間更早,發生於這些成員的default構造函數被自動調用之時(比進入ABEntry構造函數本體的時間更早)。
使用所謂的member initialization list(成員初值列)替換賦值動作會更好:
1. ABEntry::ABEntry(const std::string& name, const std::string& address,
2. const std::list<PhoneNumber>& phones)
3. :theName(name),
4. theAddress(address), //現在,這些都是初始化(initializations)
5. thePhones(phones),
6. numTimesConsulted(0)
7. { } //現在,構造函數本體不必有任何動作
這個構造函數和上一個的最終結果相同,但通常效率較高。對大多數類型而言,比起先調用default構造函數然後再調用copy assignment操作符,單只調用一次copy構造函數是比較高效的,有時甚至高效得多。對於內置型對象如numTimesConsulted,其初始化和賦值的成本相同,但為了一致性最好也通過成員初值列來初始化。同樣道理,甚至當你想要default構造一個成員變量,你都可以使用成員初值列。假設ABEntry有一個無參數構造函數,我們可將它實現如下:
1. ABEntry::ABEntry( )
2. :theName(), //調用theName的default構造函數;
3. theAddress(), //為theAddress做類似動作;
4. thePhones(), //為thePhones做類似動作;
5. numTimesConsulted(0)//記得將numTimesConsulted顯式初始化為0
6. { }
請立下一個規則,規定總是在初值列中列出所有成員變量,並總是使用成員初值列。
C++ 有著十分固定的"成員初始化次序",base classes早於其derived classes,而class的成員變量總是以其聲明次序被初始化。回頭看看ABEntry,其theName成員永遠最先被初始化,然後是theAddress,再來是thePhones,最後是numTimesConsulted,即使它們在成員初值列中以不同的次序出現。為避免某些可能存在的晦澀錯誤(兩個成員變量的初始化帶有次序性,如初始化array時需要指定大小,因此代表大小的那個成員變量必須先有初值),當你在成員初值列中條列各個成員時,最好總是以其聲明次序為次序。
l 構造函數最好使用成員初值列(memberinitialization list),而不要在構造函數本體內使用賦值操作(assignment)。初值列列出的成員變量,其排列次序應該和它們在class中的聲明次序相同。
不同編譯單元內定義之non-local static對象的初始化次序
static對象:函數內的static對象稱為localstatic對象,其他static對象稱為non-localstatic對象。程序結束時static對象會被自動銷毀,也就是它們的析構函數會在main()結束時被自動調用。
編譯單元(translation unit):產出單一目標文件(single object file)的那些源碼,基本上它是單一源碼文件加上其所含入的頭文件(#include files)。
真正的問題是:如果某編譯單元內的某個non-localstatic對象的初始化動作使用了另一編譯單元內的某個non-local static對象,它所用到的這個對象可能尚未被初始化,因為C++ 對"定義於不同編譯單元內的non-local static對象"的初始化次序並無明確定義。
假設你有一個FileSystem class,它讓互聯網上的文件看起來好像位於本機(local)。由於這個class使世界看起來像個單一文件系統,你可能會產出一個特殊對象,位於global或namespace作用域內,象征單一文件系統:
1. class FileSystem { //來自你的程序庫
2. public:
3. ...
4. std::size_t numDisks() const;//眾多成員函數之一
5. ...
6. };
7. extern FileSystem tfs; //預備給客戶使用的對象,tfs代表"the file system"
現在假設某些客戶建立了一個class用以處理文件系統內的目錄(directories)。很自然他們的class會用上theFileSystem對象:
1. class Directory { //由程序庫客戶建立
2. public:
3. Directory( params );
4. ...
5. };
6. Directory::Directory( params )
7. {
8. ...
9. std::size_t disks = tfs.numDisks();//使用tfs對象
10. ...
11. }
進一步假設,這些客戶決定創建一個Directory對象,用來放置臨時文件:
1. Directory tempDir( params ); //為臨時文件而做出的目錄
除非tfs在tempDir之前先被初始化,否則tempDir的構造函數會用到尚未初始化的tfs。但tfs和tempDir是不同的人在不同的時間於不同的源碼文件建立起來的,它們是定義於不同編譯單元內的non-local static對象。
C++ 對"定義於不同的編譯單元內的non-localstatic對象"的初始化相對次序並無明確定義。這是有原因的:決定它們的初始化次序相當困難,非常困難,根本無解。
一個小小的設計便可完全消除這個問題:將每個non-localstatic對象搬到自己的專屬函數內,並將該對象在此函數內被聲明為static,這些函數返回一個reference指向它所含的對象。然後用戶調用這些函數,而不直接指涉這些對象。換句話說,non-local static對象被local static對象替換了。這是Singleton模式的一個常見實現手法。
C++ 保證,函數內的local static對象會在該函數被調用期間首次遇上該對象之定義式時被初始化。如果你從未調用non-local static對象的"仿真函數",就絕不會引發構造和析構成本!
以此技術施行於tfs和tempDir身上,結果如下:
1. class FileSystem { ... }; //同前
2. FileSystem& tfs() //這個函數用來替換tfs對象;它在
3. { //FileSystem class中可能是個static。
4. static FileSystem fs; //定義並初始化一個local static對象,
5. return fs; //返回一個reference指向上述對象。
6. }
7. class Directory { ... }; //同前
8. Directory::Directory( params )//同前,但原本的reference to tfs
9. { //現在改為tfs()
10. ...
11. std::size_t disks = tfs().numDisks( );
12. ...
13. }
14. Directory& tempDir() //這個函數用來替換tempDir對象;
15. { //它在Directory class中可能是個static。
16. static Directory td; //定義並初始化local static對象,
17. return td; //返回一個reference指向上述對象。
18. }
這麼修改之後,這個系統程序的客戶唯一不同的是他們現在使用tfs()和tempDir()而不再是tfs和tempDir,也就是說他們使用函數返回的"指向static對象"的references,而不再使用static對象自身。這些函數內含static對象的事實使它們在多線程系統中帶有不確定性。
l 為免除"跨編譯單元之初始化次序"問題,請以local static對象替換non-local static對象。
摘自 pandawuwyj的專欄