Item 4: 確保 objects(對象)在使用前被初始化
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
C++ 看上去在對象的值的初始化方面變化莫測。例如,如果你這樣做,
int x;
在某些情形下,x 會被初始化(為 0),但是在其它情形下,也可能沒有。如果你這樣做,
class Point {
int x, y;
};
...
Point p;
p 的 data members(資料成員)有時會被初始化(為 0),但有時沒有。如果你從一個不存在 uninitialized objects(未初始化對象)的語言來到 C++,請注意這個問題,因為它非常重要。
讀取一個 uninitialized values(未初始化值)會引起 undefined behavior(未定義行為)。在一些平台上,讀一個 uninitialized value(未初始化值)會引起程式中止,更可能的情況是得到一個你所讀的那個位置上的 semi-random bits(半隨機二進位位),最終導致不可預測的程式行為和惱人的調試。
現在,有一些描述關於什麼時候能保證 object initialization(對象初始化)會發生什麼時候不能保證的規則。不幸的是,這些規則很複雜——我覺得它複雜得無法記住。通常,如果你使用 C++ 的 C 部分(參見 Item 1),而且 initialization(初始化)可能會花費一些已耗用時間,它就不能保證發生。如果你使用 C++ 的 non-C 部分,事情會有些變化。這就是為什麼一個 array(數組)(來自 C++ 的 C 部分)不能確保它的元素被初始化,但是一個 vector(來自 C++ 的 STL 部分)就能夠確保。
處理這種事情的表面不確定狀態的最好方法就是總是在使用之前初始化你的對象。對於 built-in types(內建類型)的 non-member objects(非成員對象),需要你手動做這件事。例如:
int x = 0; // manual initialization of an int
const char * text = "A C-style string"; // manual initialization of a
// pointer (see also Item 3)
double d; // "initialization" by reading from
std::cin >> d; // an input stream
除此之外的幾乎全部情況,initialization(初始化)的重任就落到了 constructors(建構函式)的身上。這裡的規則很簡單:確保 all constructors(所有的建構函式)都初始化了 object(對象)中的每一樣東西。
這個規則很容易遵守,但重要的是不要把 assignment(賦值)和 initialization(初始化)搞混。考慮下面這個表現一個通訊錄條目的 class(類)的 constructor(建構函式):
class PhoneNumber { ... };
class ABEntry { // ABEntry = "Address Book Entry"
public:
ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int num TimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
{
theName = name; // these are all assignments,
theAddress = address; // not initializations
thePhones = phones;
numTimesConsulted = 0;
}
這樣做雖然使得 ABEntry objects(對象)具有了你所期待的值,但還不是最好的做法。C++ 的規則規定一個 object(對象)的 data members(資料成員)在進入 constructor(建構函式)的函數體之前被初始化。在 ABEntry 的 constructor(建構函式)內,theName,theAddress 和 thePhones 不是 being initialized(被初始化),而是 being assigned(被賦值)。initialization(初始化)發生得更早——在進入 ABEntry 的 constructor(建構函式)的函數體之前,它們的 default constructors(預設的建構函式)已經被自動調用。但不包括 numTimesConsulted,因為它是一個 built-in type(內建類型)。不能保證它在被賦值之前被初始化。
一個更好的寫 ABEntry constructor(建構函式)的方法是用 member initialization list(成員初始化列表)來代替 assignments(賦值):
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
: theName(name),
theAddress(address), // these are now all initializations
thePhones(phones),
numTimesConsulted(0)
{} // the ctor body is now empty
這個 constructor(建構函式)的最終結果和前面那個相同,但是通常它有更高的效率。assignment-based(基於賦值)的版本會首先調用 default constructors(預設建構函式)初始化 theName,theAddress 和 thePhones,然而很快又在 default-constructed(預設構造)的值之上賦予新值。那些 default constructions(預設建構函式)所做的工作被浪費了。而 member initialization list(成員初始化列表)的方法避免了這個問題,因為 initialization list(初始化列表)中的 arguments(參數)就可以作為各種 data members(資料成員)的 constructor(建構函式)所使用的 arguments(參數)。在這種情況下,theName 從 name 中 copy-constructed(拷貝構造),theAddress 從 address 中 copy-constructed(拷貝構造),thePhones 從 phones 中 copy-constructed(拷貝構造)。對於大多數類型來說,只調用一次 copy constructor(拷貝建構函式)的效率比先調用一次 default constructor(預設建構函式)再調用一次 copy assignment operator(拷貝賦值運算子)的效率要高(有時會高很多)。
對於 numTimesConsulted 這樣的 built-in type(內建類型)的 objects(對象),initialization(初始化)和 assignment(賦值)沒有什麼不同,但為了統一性,最好是經由 member initialization(成員初始化)來 initialize(初始化)每一件東西。類似地,當你只想 default-construct(預設構造)一個 data member(資料成員)時也可以使用 member initialization list(成員初始化列表),只是不必指定 initialization argument(初始化參數)而已。例如,如果 ABEntry 有一個不取得 parameters(參數)的 constructor(建構函式),它可以像這樣實現:
ABEntry::ABEntry()
:theName(), // call theName's default ctor;
theAddress(), // do the same for theAddress;
thePhones(), // and for thePhones;
numTimesConsulted(0) // but explicitly initialize
{} // numTimesConsulted to zero
因為對於那些在 member initialization list(成員初始化列表)中的,沒有 initializers(初始化器)的,user-defined types(使用者自訂類型)的 data members(資料成員),編譯器會自動調用其 default constructors(預設建構函式),所以一些程式員會認為上面的方法有些過分。這也不難理解,但是一個方針是:在 initialization list(初始化列表)中總是列出每一個 data member(資料成員),這就可以避免一旦發生疏漏就必須回憶起可能是哪一個 data members(資料成員)沒有被初始化。例如,因為 numTimesConsulted 是一個 built-in type(內建類型),如果將它從 member initialization list(成員初始化列表)中刪除,就為 undefined behavior(未定義行為)開啟了方便之門。
有時,即使是 built-in types(內建類型),initialization list(初始化列表)也必須使用。比如,const 或 references(引用)data members(資料成員)是必須 be initialized(被初始化)的,它們不能 be assigned(被賦值)(參見 Item 5)。為了避免記憶什麼時候 data members(資料成員)必須在 member initialization list(成員初始化列表)中初始化,而什麼時候又是可選的,最簡單的方法就是總是使用 initialization list(初始化列表)。它有時是必須的,而且它通常都比 assignments(賦值)更有效率。
很多 classes(類)有多個 constructors(建構函式),而每一個 constructor(建構函式)都有自己的 member initialization list(成員初始化列表)。如果有很多 data members(資料成員)和/或 base classes(基類),成倍增加的 initialization lists(初始化列表)的存在引起令人鬱悶的重複(在列表中)和厭煩(在程式員中)。在這種情況下,不能不講道理地從列表中刪除那些 assignment(賦值)和 true initialization(真正的初始化)一樣工作的 data members(資料成員)項目,而是將 assignments(賦值)移到一個單獨的(當然是 private(私人)的)函數中,以供所有 constructors(建構函式)調用。這個方法對於那些 true initial values(真正的初始值)是從檔案中讀入或從資料庫中檢索出來的 data members(資料成員)特別有協助。然而,通常情況下,true member initialization(真正的成員初始化)(經由一個 initialization list(初始化列表))比經由 assignment(賦值)來進行的 pseudo-initialization(假初始化)更可取。
C++ 並非變幻莫測的方面是一個 object(對象)的資料被初始化的順序。這個順序總是相同的:base classes(基類)在 derived classes(衍生類別)之前被初始化(參見 Item 12),在一個 class(類)內部,data members(資料成員)按照它們被聲明的順序被初始化。例如,在 ABEntry 中,theName 總是首先被初始化,theAddress 是第二個,thePhones 第三,numTimesConsulted 最後。即使它們在 member initialization list(成員初始化列表)中以一種不同的順序排列(這不幸合法),這依然是成立的。為了避免讀者混淆,以及一些模糊不清的行為引起錯誤的可能性,initialization list(初始化列表)中的 members(成員)的排列順序應該總是與它們在 class(類)中被聲明的順序保持一致。
一旦處理了 built-in types(內建類型)的 non-member objects(非成員對象)的顯式初始化,而且確保你的 constructors(建構函式)使用 member initialization list(成員初始化列表)初始化了它的 base classes(基類)和 data members(資料成員),那就只剩下一件事情需要費心了。那就是——深呼吸先——定義在不同 translation units(轉換單元)中的 non-local static objects(非局部靜態對象)的 initialization(初始化)的順序。
讓我們一片一片地把這個片語拆開。
一個 static object(靜態對象)的生存期是從它建立開始直到程式結束。stack and heap-based objects(基於堆棧的對象)就被排除在外了。包括 global objects(全域對象),objects defined at namespace scope(定義在命名空間範圍內的對象),objects declared static inside classes(在類內部聲明為靜態對象),objects declared static inside functions(在函數內部聲明為靜態對象)和 objects declared static at file scope(在檔案範圍內被聲明為靜態對象)。static objects inside functions(在函數內部的靜態對象)以 local static objects(局部靜態對象)(因為它局部於函數)為人所知,其它各種 static objects(靜態對象)以 non-local static objects(非局部靜態對象)為人所知。程式結束時 static objects(靜態對象)會自動銷毀,也就是當 main 停止執行時會自動調用它們的 destructors(解構函式)。
一個 translation unit(轉換單元)是可以形成一個單獨的 object file(目標檔案)的 source code(原始碼)。基本上是一個單獨的 source file(源檔案),再加上它全部的 #include 檔案。
我們關心的問題是這樣的:包括至少兩個分別編譯的 source files(源檔案),每一個中都至少包含一個 non-local static object(非局部靜態對象)(也就是說,global(全域)的,at namespace scope(命名空間範圍)的,static in a class(類內)的或 at file scope(檔案範圍)的 object(對象))。實際的問題是這樣的:如果其中一個 translation unit(轉換單元)內的一個 non-local static object(非局部靜態對象)的 initialization(初始化)用到另一個 translation unit(轉換單元)內的non-local static object(非局部靜態對象),它所用到的 object(對象)可能沒有被初始化,因為 the relative order of initialization of non-local static objects defined in different translation units is undefined(定義在不同轉換單元內的非局部靜態對象的初始化的相對順序是沒有定義的)。
一個例子可以協助我們。假設你有一個 FileSystem class(類),可以使 Internet 上的檔案看起來就像在本地。因為你的 class(類)使得世界看起來好像只有一個單獨的 file system(檔案系統),你可以在 global(全域)或 namespace(命名空間)範圍內建立一個專門的 object(對象)來代表這個單獨的 file system(檔案系統):
class FileSystem { // from your library
public:
...
std::size_t numDisks() const; // one of many member functions
...
};
extern FileSystem tfs; // object for clients to use;
// "tfs" = "the file system"
一個 FileSystem object(對象)絕對是舉足輕重的,所以在 tfs object(對象)被建立之前就使用將會損失慘重。(此段和下段原文有誤,根據作者網站勘誤更改——譯者注。)
現在假設一些客戶為一個 file system(檔案系統)中的目錄建立了一個 class(類),他們的 class(類)使用了 tfs object(對象):
class Directory { // created by library client
public:
Directory( params );
...
};
Directory::Directory( params )
{
...
std::size_t disks = tfs.numDisks(); // use the tfs object
...
}
更進一步,假設這個客戶決定為臨時檔案建立一個單獨的 Directory object(對象):
Directory tempDir( params ); // directory for temporary files
現在 initialization order(初始化順序)的重要性變得明顯了:除非 tfs 在 tempDir 之前初始化,否則,tempDir 的 constructor(建構函式)就會在 tfs 被初始化之前試圖使用它。但是,tfs 和 tempDir 是被不同的人於不同的時間在不同的 source files(源檔案)中建立的——它們是定義在不同 translation units(轉換單元)中的 non-local static objects(非局部靜態對象)。你怎麼能確保 tfs 一定會在 tempDir 之前被初始化呢?
你不能。重申一遍,the relative order of initialization of non-local static objects defined in different translation units is undefined(定義在不同轉換單元內的非局部靜態對象的初始化的相對順序是沒有定義的)。這是有原因的。決定 non-local static objects(非局部靜態對象)的“恰當的”初始化順序是困難的,非常困難,以至於無法完成。在最常見的形式下——多個 translation units(轉換單元)和 non-local static objects(非局部靜態對象)通過 implicit template instantiations(隱式模板執行個體化)產生(這本身可能也是經由 implicit template instantiations(隱式模板執行個體化)引起的)——不僅不可能確定一個正確的 initialization(初始化)順序,甚至不值得去尋找可能確定正確順序的特殊情況。
幸運的是,一個小小的設計改變從根本上解決了這個問題。全部要做的就是將每一個 non-local static object(非局部靜態對象)移到它自己的函數中,在那裡它被聲明為 static(靜態)。這些函數返回它所包含的 objects(對象)的引用。客戶可以調用這些函數來代替直接涉及那些 objects(對象)。換一種說法,就是用 local static objects(局部靜態對象)取代 non-local static objects(非局部靜態對象)。(aficionados of design patterns(設計模式迷們)會認出這是 Singleton 模式的通用實現)。(實際上,這隻不過是一個 Singleton 實現的一部分。我在這個 Item 中忽略的 Singleton 的核心部分是為了預防建立一個特定類型的多個 objects。)(括弧中的文字原文無,參照作者網站勘誤添加——譯者注。)
這個方法建立在 C++ 保證 local static objects(局部靜態對象)的初始化發生在因為調用那個函數而第一次遇到那個 object(對象)的 definition(定義)時候。所以,如果你用調用返回 references to local static objects(局部靜態對象的引用)的函數的方法取代直接存取 non-local static objects(非局部靜態對象)的方法,你將確保你取回的 references(引用)引向 initialized objects(已初始化的對象)。作為一份額外收穫,如果你從不調用這樣一個仿效 non-local static object(非局部靜態對象)的函數,你就不會付出建立和銷毀這個 object(對象)的代價,而一個 true non-local static objects(真正的非局部靜態對象)則不會有這樣的效果。
以下就是這項技術在 tfs 和 tempDir 上的應用:
class FileSystem { ... }; // as before
FileSystem& tfs() // this replaces the tfs object; it could be
{ // static in the FileSystem class
static FileSystem fs; // define and initialize a local static object
return fs; // return a reference to it
}
class Directory { ... }; // as before
Directory::Directory( params ) // as before, except references to tfs are
{ // now to tfs()
...
std::size_t disks = tfs().numDisks();
...
}
Directory& tempDir() // this replaces the tempDir object; it
{ // could be static in the Directory class
static Directory td; // define/initialize local static object
return td; // return reference to it
}
這個改良系統的客戶依然可以按照他們已經習慣的方法編程,只是他們現在應該用 tfs() 和 tempDir() 來代替 tfs 和 tempDir。也就是說,他們應該使用返回 references to objects(對象引用)的函數來代替使用 objects themselves(對象自身)。
按照以下步驟來寫 reference-returning functions(返回引用的函數)總是很簡單:在第 1 行定義並初始化一個 local static object(局部靜態對象),在第 2 行返回它。這樣的簡單使它們成為 inlining(內聯化)的完美的候選者,特別是在它們被頻繁調用的時候(參見 Item 30)。在另一方面,這些函數包含 static object(靜態對象)的事實使它們在 multithreaded systems(多線程系統)中會出現問題。更進一步,任何種類的 non-const static object(非常量靜態對象)—— local(局部)的或 non-local(非局部)的——在 multiple threads(多線程)存在的場合都會發生麻煩。解決這個麻煩的方法之一是在程式的 single-threaded(單線程)的啟動部分手動調用所有的 reference-returning functions(返回引用的函數)。以此來避免 initialization-related(與初始化相關)的混亂環境。
當然,用 reference-returning functions(返回引用的函數)來防止 initialization order problems(初始化順序問題)的想法首先依賴於你的 objects(對象)有一個合理的 initialization order(初始化順序)。如果你有一個系統,其中 object A 必須在 object B 之前初始化,但是 A 的初始化又依賴於 B 已經被初始化,你將遇到問題,坦白地講,你遇到大麻煩了。然而,如果你避開了這種病態的境遇,這裡描述的方法會很好地為你服務,至少在 single-threaded applications(單線程應用)中是這樣。
避免在初始化之前使用 objects(對象),你只需要做三件事。首先,手動初始化 built-in types(內建類型)的 non-member objects(非成員對象)。第二,使用 member initialization lists(成員初始化列表)初始化一個 object(對象)的所有部分。最後,在設計中繞過搞亂定義在分離的 translation units(轉換單元)中的 non-local static objects(非局部靜態對象)initialization order(初始化順序)的不確定性。
Things to Remember
- 手動初始化 built-in type(內建類型)的 objects(對象),因為 C++ 只在某些時候才會自己初始化它們。
- 在 constructor(建構函式)中,用 member initialization list(成員初始化列表)代替函數體中的 assignment(賦值)。initialization list(初始化列表)中 data members(資料成員)的排列順序要與它們在 class(類)中被聲明的順序相同。
- 通過用 local static objects(局部靜態對象)代替 non-local static objects(非局部靜態對象)來避免跨 translation units(轉換單元)的 initialization order problems(初始化順序問題)。