程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> Effective C++讀書筆記(3)

Effective C++讀書筆記(3)

編輯:C++入門知識

條款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的專欄

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