總所周知,C++對象在創建之時,會由構造函數進行一系列的初始化工作。以沒有繼承關系的單個類來看,除了構造函數本身的產生與指定,還涉及到初始化步驟,以及成員初始化方式等一些細節,本篇筆記主要對這些細節進行介紹,弄清C++對象在初始化過程中一些基本運行規則。
構造函數指定
通常,我們在設計一個類的時候,會為這個類編寫對應的default constructor、copy constructor、copy assignment operator,還有一個deconstructor。即便我們僅僅編寫一個空類,編譯器在編譯時仍舊會為其默認聲明一個default constructor、copy constructor、copy assignment operator與deconstructor,如果在代碼里面存在着它們的使用場景,那么這個時候編譯器才會創建它們。
1 class MyCppClass 2 { 3 }
一旦我們為一個類編寫了default constructor,那么編譯器也就不會為其默認生成default constructor,對於其他幾個函數也一樣。對於編譯器默認生成的constructor來說,它會以一定規則對每一個數據成員進行初始化。考慮到成員初始化的重要性,在編寫自己的constructor時就需要嚴謹認真了,特別是在類的派生與繼承情況下這點顯得尤為重要。對於copy constructor和assignment operator的運用場景,這里不得不多說一點,見如下代碼:
1 #include <iostream> 2 3 using std::cout; 4 using std::endl; 5 6 class MyCppClass 7 { 8 public: 9 MyCppClass() 10 { 11 std::cout <<"In Default Constructor!" <<std::endl; 12 } 13 14 MyCppClass(const MyCppClass& rhs) 15 { 16 std::cout <<"In Copy Constructor!" <<std::endl; 17 } 18 19 MyCppClass& operator= (const MyCppClass& rhs) 20 { 21 std::cout <<"In Copy Assignment Operator!" <<std::endl; 22 23 return *this; 24 } 25 }; 26 27 int main() 28 { 29 MyCppClass testClass1; // default constructor 30 MyCppClass testClass2(testClass1); // copy constructor 31 testClass1 = testClass2; // copy assignment operator 32 33 MyCppClass testClass3 = testClass1; // copy constructor 34 35 return 0; 36 }
執行結果:
這里需要注意的是,一般情況下我們總是以為在‘=’運算符出現的地方都是調用copy assignment operator,上面這種情況卻是個例外。也就是,當一個新對象被定義的時候,即便這個時候是使用了'='運算符,它真實調用的是初始化函數copy constructor,而不是調用copy assignment operator去進行賦值操作。
Why初始化列表
一個對象在初始化時包括了兩個步驟:首先,分配內存以保存這個對象;其次,執行構造函數。在執行構造函數的時候,如果存在有初始化列表,則先執行初始化列表,之后再執行構造函數的函數體。那么,為什么會引入初始化列表呢?
C++與C相比,在程序組織上由“以函數為基本組成單位的面向過程”變遷到“基於以類為中心的面向對象”,與此同時類也作為一種復合數據類型,而初始化列表無非就是進行一些數據的初始化工作。考慮到這里,也可以較為自然的推測初始化列表與類這種數據類型的初始化有着關聯。
在引入初始化列表之后,一個類對應數據成員的初始化就存在有兩種方式。下面是類的數據成員類型分別為內置類型、自定義類型時的一個對比。
1 // 數據成員類型為內置類型 2 class MyCppClass 3 { 4 public: 5 // 賦值操作進行成員初始化 6 MyCppClass 7 { 8 counter = 0; 9 } 10 11 // 初始化列表進行成員初始化 12 MyCppClass : counter(0) 13 { 14 } 15 16 private: 17 int counter; 18 }
當類的數據成員類型為內置類型時,上面兩種初始化方式的效果一樣。當數據成員的類型同樣也為一個類時,初始化的過程就會有不一樣的地方了,比如:
1 // 數據成員類型為自定義類型:一個類 2 class MyCppClass 3 { 4 public: 5 // 賦值操作進行成員初始化 6 MyCppClass(string name) 7 { 8 counter = 0; 9 theName = name; 10 } 11 12 // 初始化列表進行成員初始化 13 MyCppClass : counter(0), theName(name) 14 { 15 } 16 17 private: 18 int counter; 19 string theName; 20 }
在構造函數體內的theName = name這條語句,theName先會調用string的default constructor進行初始化,之后再調用copy assignment opertor進行拷貝賦值。而對於初始化列表來說,直接通過copy constructor進行初始化。明顯起見,可以通過如下的代碼進行測試。
1 2 #include <iostream> 3 #include <string> 4 5 class SubClass 6 { 7 public: 8 SubClass() 9 { 10 std::cout <<" In SubClass Default Constructor!" <<std::endl; 11 } 12 13 SubClass(const SubClass& rhs) 14 { 15 std::cout <<" In SubClass Copy Constructor!" <<std::endl; 16 } 17 18 SubClass& operator= (const SubClass& rhs) 19 { 20 std::cout <<" In SubClass Copy Assignment Operator!" <<std::endl; 21 22 return *this; 23 } 24 }; 25 26 class BaseClass 27 { 28 public: 29 BaseClass(const SubClass &rhs) 30 { 31 counter = 0; 32 theBrother = rhs; 33 std::cout <<" In BaseClass Default Constructor!" <<std::endl; 34 } 35 36 BaseClass(const SubClass &rhs, int cnt):theBrother(rhs),counter(cnt) 37 { 38 std::cout <<" In BaseClass Default Constructor!" <<std::endl; 39 } 40 41 BaseClass(const BaseClass& rhs) 42 { 43 std::cout <<" In BaseClass Copy Constructor!" <<std::endl; 44 } 45 46 BaseClass& operator= (const BaseClass& rhs) 47 { 48 std::cout <<" In BaseClass Copy Assignment Operator!" <<std::endl; 49 50 return *this; 51 } 52 private: 53 int counter; 54 SubClass theBrother; 55 }; 56 57 int main() 58 { 59 SubClass subClass; 60 61 std::cout <<"\nNo Member Initialization List: " <<std::endl; 62 BaseClass BaseClass1(SubClass); 63 64 std::cout <<"\nMember Initialization List: " <<std::endl; 65 BaseClass BaseClass2(SubClass, 1); 66 67 return 0; 68 }
執行結果:
也就是,在涉及到自定義類型初始化的時候,使用初始化列表來完成初始化在效率上會有着更佳的表現。這也是初始化列表的一大閃光點。即便對於內置類型,
在一些情況下也是需要使用初始化列表來完成初始化工作的,比如const、references成員變量。
這里有篇筆記,對初始化列表有着非常詳盡的描述。
幾個初始化名詞
在閱讀《Accelerated C++》中文版時,總是碰到“缺省初始化”、“隱式初始化”以及“數值初始化”,最初在理解這幾個名詞的時候幾費周折,總覺得為什么一個初始化操作造出了如此多的名詞,為此沒少花時間來弄清楚它們之間的關系。
為了更好的理解它們,先對C++當中的數據類型進行簡單划分。在C++里面,數據類型大致可以分為兩種:第一種是內置類型,比如float, int, double等;第二種是自定義類型,也就是我們常用的class, struct定義的類。在對這些類型的數據進行初始化時,差別就體現出來了:對於內置類型,在使用之前必須進行顯示的初始化,而對於自定義類型,初始化責任則落在了構造函數身上。
1 int x = 0; // 顯示初始化x 2 SubClass subClass; // 依賴SubClass的default constructor進行初始化
上面的名詞“缺省初始化”描述的就是當內置類型或者自定義類型的數據沒有進行顯示初始化時的一種初始化狀態,而“隱式初始化”描述的是在該狀態下面進行的具體操作方式,比如對於內置類型來說,缺省初始化狀態下進行的隱式初始化實際上是未定義的,而自定義類型的隱式初始化則依賴於其constructor。
前面提到過C++不保證內置類型的初始化,但是當內置類型在作為一個類的成員時,在某些特定的條件下該內置類型的成員會被編譯器主動進行初始化,對於這個過程也就是所謂的數值初始化。在《Accelerated C++》當中列出了如下的幾種情況:
- 對象被用來初始化一個容器元素
- 為映射表添加一個新元素,對象是這個添加動作的副作用
- 定義一個特定長度的容器,對象為容器的元素
測試如下:
1 #include <iostream> 2 #include <vector> 3 #include <map> 4 #include <string> 5 6 using std::cout; 7 using std::endl; 8 using std::vector; 9 using std::map; 10 using std::string; 11 12 class NumbericInitTestClass 13 { 14 public: 15 void PrintCounter() 16 { 17 cout <<"counter = " <<counter <<endl; 18 } 19 private: 20 int counter; 21 }; 22 23 24 int main() 25 { 26 NumbericInitTestClass tnc; 27 tnc.PrintCounter(); 28 29 map<string, int> mapTest; 30 cout <<mapTest["me"] <<endl; 31 32 vector<NumbericInitTestClass> vecNumbericTestClass(1); 33 vecNumbericTestClass[0].PrintCounter(); 34 35 return 0; 36 }
對於沒有進行初始化的內置類型,是一個未定義的值2009095316,而對於2, 3種情況來說,均被初始化為0,對於第1種情況我還沒有想到合適的場景。
回過頭想想,為了書中的一些相似的名詞,去想辦法把它們湊在一起總是顯得有些牽強附會:)
一些規則
這里附上幾條有關初始化的基本規則,它們多來源於
《Effective C++》:
1. 為內置型對象進行手工初始化,因為C++不保證初始化它們。
2. 構造函數最好使用成員初值列(member initialization list),而不要在構造函數體內使用賦值操作。初值列列出的成員變量,其排列次序應該和它們在class中聲明的次序相同。
3. C++不喜歡析構函數吐出異常。
4. 在構造函數與析構函數期間不要調用virtual函數,因為這類調用從不下降至derived class。
5. copying函數應該確保復制“對象內所有成員變量”及“所有base class成分”。
