# [10] 構造函數
## FAQs in section [10]:
* [10.1] 構造函數做什么?
* [10.2] `List?x;` 和 `List?x();`有區別嗎?
* [10.3] 如何才能夠使一個構造函數直接地調用另一個構造函數?
* [10.4] `Fred` 類的默認構造函數總是 `Fred::Fred()`嗎?
* [10.5] 當我建立一個 `Fred` 對象數組時,哪個構造函數將被調用?
* [10.6] 構造函數應該用“初始化列表”還是“賦值”?
* [10.7] 可以在構造函數中使用 `this` 指針嗎?
* [10.8] 什么是“命名的構造函數用法(Named Constructor Idiom)”?
* [10.9] 為何不能在構造函數的初始化列表中初始化靜態成員數據?
* [10.10] 為何有靜態數據成員的類得到了鏈接錯誤?
* [10.11] 什么是“`static` initialization order fiasco”?
* [10.12] 如何防止“`static` initialization order fiasco”?
* [10.13] 對于靜態數據成員,如何防止“`static` initialization order fiasco”?
* [10.14] 如何處理構造函數的失敗?
* [10.15] 什么是“命名參數用法(Named Parameter Idiom)”?
## 10.1 構造函數做什么?
構造函數從無到有創建對象。
構造函數就象“初始化函數”。它將一連串的隨意的內存位變成活的對象。至少它要初始化對象內部所使用的域。它還可以分配資源(內存、文件、信號、套接字等)
"ctor" 是構造函數(constructor)典型的縮寫。
## 10.2? `List?x;` 和 `List?x();`有區別嗎?
有非常大的區別!
假設`List` 是某個類的名稱。那么函數`f()` 中聲明了一個局部的 `List`對象,名稱為 `x`:
```
?void?f()
?{
???List?x;?????//?Local?object?named?x?(of?class?List)//?...
?}
```
但是函數 `g()` 中聲明了一個名稱為`x()`的函數,它返回一個 `List`:
```
?void?g()
?{
???List?x();???//?Function?named?x?(that?returns?a?List)//?...
?}
```
## 10.3 如何才能夠使一個構造函數直接地調用另一個構造函數?
不行。
注意:如果你調用了另一個構造函數,編譯器將初始化一個臨時局部對象;而不是初始化`this`對象。你可以通過一個默認參數或在一個私有成員函數 `init()` 中共享它們的公共代碼來使兩個構造函數結合起來。
## 10.4 `Fred` 類的默認構造函數總是`Fred::Fred()`嗎?
不。“默認構造函數”是能夠被無參數調用的構造函數。因此,一個不帶參數的構造函數當然是默認構造函數:
```
?class?Fred?{
?public:
???Fred();???//?默認構造函數: 能夠被無參數調用//?...
?};
```
然而,如果參數被提供了默認值,那么帶參數的默認構造函數也是可能的:
```
?class?Fred?{
?public:
???Fred(int?i=3,?int?j=5);???//?默認構造函數: 能夠被無參數調用//?...
?};
```
## 10.5 當建立一個 `Fred` 對象數組時,哪個構造函數將被調用?
`Fred` 的默認構造函數(以下討論除外)。
你無法告訴編譯器調用不同的構造函數(以下討論除外)。如果你的`Fred`類沒有默認構造函數,那么試圖創建一個`Fred`對象數組將會導致編譯時出錯。
```
?class?Fred?{
?public:
???Fred(int?i,?int?j);
???//?...?假設 Fred 類沒有默認構造函數?...
?};
?int?main()
?{
???Fred?a[10];???????????????//?錯誤:Fred 類沒有默認構造函數
???Fred*?p?=?new?Fred[10];???//?錯誤:Fred 類沒有默認構造函數
?}
```
然而,如果你正在創建一個標準的`std::vector<Fred>`,而不是 `Fred`對象數組(既然數組是有害的,那么你可能應該這么做),則在 `Fred` 類中不需要默認構造函數。因為你能夠給`std::vector`一個用來初始化元素的`Fred` 對象:
```
?#include?<vector>
?int?main()
?{
???std::vector<Fred>?a(10,?Fred(5,7));
???//?在std::vector?中的 10 個 Fred對象將使用 Fred(5,7) 來初始化//?...
?}
```
雖然應該使用`std::vector`而不是數組,但有有應該使用數組的時候,那樣的話,有“數組的顯式初始化”語法。它看上去是這樣的:
```
?class?Fred?{
?public:
???Fred(int?i,?int?j);
???//?...?假設Fred類沒有默認構造函數...
?};
?int?main()
?{
???Fred?a[10]?=?{
?????Fred(5,7),?Fred(5,7),?Fred(5,7),?Fred(5,7),?Fred(5,7),
?????Fred(5,7),?Fred(5,7),?Fred(5,7),?Fred(5,7),?Fred(5,7)
???};
???//?10 個 Fred對象將使用 Fred(5,7) 來初始化.
//?...
?}
```
當然你不必每個項都做`Fred(5,7)`—你可以放任何你想要的數字,甚至是參數或其他變量。重點是,這種語法是(a)可行的,但(b)不如`std::vector`語法漂亮。記住這個:數組是有害的—除非由于編譯原因而使用數組,否則應該用`std::vector` 取代。
## 10.6 構造函數應該用“初始化列表”還是“賦值”?
初始化列表。事實上,構造函數應該在初始化列表中初始化_所有_成員對象。
例如,構造函數用初始化列表`Fred::Fred()?:?x_(`_whatever_`)?{?}`來初始化成員對象 `x_`。這樣做最普通的好處是提高性能。如,_whatever_表達式和成員變量 `x_` 相同,_whatever_表達式的結果直接由內部的`x_`來構造——編譯器不會產生對象的兩個拷貝。即使類型不同,使用初始化列表時編譯器通常也能夠做得比使用賦值更好。
建立構造函數的另一種(錯誤的)方法是通過賦值,如:`Fred::Fred()?{?x_?=?`_whatever_`;?}`。在這種情況下,_whatever_表達式導致一個分離的,臨時的對象被建立,并且該臨時對象被傳遞給`x_`對象的賦值操作。然后該臨時對象會在 ;處被析構。這樣是效率低下的。
這好像還不是太壞,但這里還有一個在構造函數中使用賦值的效率低下之源:成員對象會被以默認構造函數完整的構造,例如,可能分配一些缺省數量的內存或打開一些缺省的文件。但如果 _whatever_表達式和/或賦值操作導致對象關閉那個文件和/或釋放那塊內存,這些工作是做無用功(舉例來說,如默認構造函數沒有分配一個足夠大的內存池或它打開了錯誤的文件)。
結論:其他條件相等的情況下,使用初始化列表的代碼會快于使用賦值的代碼。
注意:如果`x_`的類型是諸如`int`或者`char*` 或者`float`之類的內建類型,那么性能是沒有區別的。但即使在這些情況下,我個人的偏好是為了對稱,仍然使用初始化列表而不是賦值來設置這些數據成員。
## 10.7 可以在構造函數中使用 `this` 指針嗎?
某些人認為不應該在構造函數中使用`this`指針,因為這時`this`對象還沒有完全形成。然后,只要你小心,是可以在構造函數(在函數體甚至在初始化列表中)使用`this`的。
以下是始終可行的:構造函數的函數體(或構造函數所調用的函數)能可靠地訪問基類中聲明的數據成員和/或構造函數所屬類聲明的數據成員。這是因為所有這些數據成員被保證在構造函數函數體開始執行時已經被完整的建立。
以下是始終不可行的:構造函數的函數體(或構造函數所調用的函數)不能向下調用被派生類 重定義的虛函數。如果你的目的是得到派生類重定義的函數,那么你將無功而返。注意,無論你如何調用虛成員函數:顯式使用`this`指針(如,`this->method()`),隱式的使用`this`指針(如,`method()`),或甚至在`this`對象上調用其他函數來調用該虛成員函數,你都不會得到派生類的重寫函數。這是底線:即使調用者正在構建一個派生類的對象,在基類的構造函數執行期間,對象還不是一個派生類的對象。
以下是有時可行的:如果傳遞 `this` 對象的任何一個數據成員給另一個數據成員的初始化程序,你必須確保該數據成員已經被初始化。好消息是你能使用一些不依賴于你所使用的編譯器的顯著的語言規則,來確定那個數據成員是否已經(或者還沒有)被初始化。壞消息是你必須知道這些語言規則(例如,基類子對象首先被初始化(如果有多重和/或虛繼承,則查詢這個次序!),然后類中定義的數據成員根據在類中聲明的次序被初始化)。如果你不知道這些規則,則不要從`this`對象傳遞任何數據成員(不論是否顯式的使用了`this`關鍵字)給任何其他數據成員的初始化程序!如果你知道這些規則,則需要小心。
## 10.8 什么是“命名的構造函數法(Named Constructor Idiom)”?
為你的類的用戶提供的一種更直覺的和/或更安全的構造操作技巧。
問題在于構造函數總是有和類相同的名字。因此,區分類的不同的構造函數是通過參數列表。但如果有許多構造函數,它們之間的區別有時就會很敏感并且有錯誤傾向。
使用命名的構造函數法(Named Constructor Idiom),在`private:`節和`protected:`節中聲明所有類的構造函數,并提供返回一個對象的`public` `static` 方法。這些方法由此稱為“命名的構造函數(Named Constructors)”。一般,每種不同的構造對象的方法都有一個這樣的靜態方法。
例如,假設我們正在建立一個描繪X-Y平面的`Point`類。通常有兩種方法指定一個二維空間坐標:矩形坐標(X+Y),極坐標(Radius+Angle)(半徑+角度)。(不必擔心已經忘了這些;重點不在于坐標系統的析解;重點在于有幾種方法來創建一個`Point`對象。)不幸的是,這兩種坐標系統的參數是相同的:兩個 `float`。這將在重載構造函數中導致一個“重載不明確”的錯誤:
```
?class?Point?{
?public:
???Point(float?x,?float?y);?????//?矩形坐標_
???Point(float?r,?float?a);?????//?極坐標?(半徑和角度)
???//?錯誤:重載不明確:Point::Point(float,float)
?};
?int?main()
?{
???Point?p?=?Point(5.7,?1.2);???//?不明確:哪個坐標系統?
?}
```
解決這個不明確錯誤的一種方法是使用命名的構造函數法(Named Constructor Idiom):
```
?#include?<cmath>???????????????//?To?get?sin()?and?cos()
?class?Point?{
?public:
???static?Point?rectangular(float?x,?float?y);??????//?矩形坐標_
???static?Point?polar(float?radius,?float?angle);???//?極坐標
//?這些?static?方法稱為“命名的構造函數(named?constructors)”
//?...
?private:
???Point(float?x,?float?y);?????//?矩形坐標
???float?x_,?y_;
?};
?inline?Point::Point(float?x,?float?y)
?:?x_(x),?y_(y)?{?}
?inline?Point?Point::rectangular(float?x,?float?y)
?{?return?Point(x,?y);?}
?inline?Point?Point::polar(float?radius,?float?angle)
?{?return?Point(radius*cos(angle),?radius*sin(angle));?}
```
現在,`Point`的用戶有了一個清晰的和明確的語法在任何一個坐標系統中創建`Point`對象:
```
?int?main()
?{
???Point?p1?=?Point::rectangular(5.7,?1.2);???//?顯然是矩形坐標
???Point?p2?=?Point::polar(5.7,?1.2);?????????//?顯然是極坐標
?}
```
如果期望`Point`有派生類,則確保你的構造函數在`protected:`節中。
命名的構造函數法也能用于總是通過`new`來創建對象。
## 10.9 為何不能在構造函數的初始化列表中初始化靜態成員數據?
因為必須顯式定義類的靜態數據成員。
```
//Fred.h:
?class?Fred?{
?public:
???Fred();
???//?...
?private:
???int?i_;
???static?int?j_;
?};
//Fred.cpp (或 Fred.C 或其他):
?Fred::Fred()
???:?i_(10)??//?正確:能夠(而且應該)這樣初始化成員數據
???,?j_(42)??//?錯誤:不能象這樣初始化靜態成員數據
?{
???//?...
?}
?//?必須這樣定義靜態數據成員:
?int?Fred::j_?=?42;
```
## 10.10 為何有靜態數據成員的類得到了鏈接錯誤?
因為靜態數據成員必須被顯式定義在一個編輯單元中。如果不這樣做,你就可能得到`"undefined?external"`鏈接錯誤。例如:
```
//?Fred.h
?class?Fred?{
?public:
???//?...
?private:
???static?int?j_;???//?聲明靜態數據成員:Fred::j_
//?...
?};
```
鏈接器會向你抱怨(`"Fred::j_?is?not?defined"`),除非你在一個源文件中定義(而不僅僅是聲明)`Fred::j_`:
```
//?Fred.cpp
?#include?"Fred.h"
?int?Fred::j_?=?some_expression_evaluating_to_an_int;
?//?Alternatively,?if?you?wish?to?use?the?implicit?0?value?for?static?ints:
//?int?Fred::j_;
```
通常定義`Fred`類的靜態數據成員的地方是`Fred.cpp`文件(或者`Fred.C`或者你使用的其他擴展名)。
## 10.11 什么是“`static` initialization order fiasco”?
你的項目的微妙殺手。
_`static` initialization order fiasco_是對C++的一個非常微妙的并且常見的誤解。不幸的是,錯誤發生在`main()`開始之前,很難檢測到。
簡而言之,假設你有存在于不同的源文件`x.cpp` 和`y.cpp`的兩個靜態對象`x` 和 `y`。再假定`y`對象的構造函數會調用`x`對象的某些方法。
就是這些。就這么簡單。
結局是你完蛋不完蛋的機會是50%-50%。如果碰巧`x.cpp`的編輯單元先被初始化,這很好。但如果`y.cpp`的編輯單元先被初始化,然后`y`的構造函數比`x`的構造函數先運行。也就是說,`y`的構造函數會調用`x`對象的方法,而`x`對象還沒有被構造。
我聽說有些人受雇于麥當勞,享受他們的切碎肉的新工作去了。
如果你覺得不用工作,在臥室的一角玩俄羅斯方塊是令人興奮的,你可以到此為止。相反,如果你想通過用一種系統的方法防止災難,來提高自己 繼續工作而存活的機會,你可能想閱讀下一個 FAQ。
注意:static initialization order fiasco不作用于內建的/固有的類型,象`int` 或 `char*`。例如,如果創建一個`static` `float`對象,不會有靜態初始化次序的問題。靜態初始化次序真正會崩潰的時機只有在你的`static`或全局對象有構造函數時。
## 10.12 如何防止“`static` initialization order fiasco”?
使用“首次使用時構造(construct on first use)”法,意思就是簡單地將靜態對象包裹于函數內部。
例如,假設你有兩個類,`Fred` 和 `Barney`。有一個稱為`x`的全局`Fred`對象,和一個稱為`y`的全局`Barney`對象。`Barney`的構造函數調用了`x`對象的`goBowling()`方法。 `x.cpp`文件定義了`x`對象:
```
//?File?x.cpp
?#include?"Fred.hpp"
?Fred?x;
```
`y.cpp`文件定義了`y`對象:
```
//?File?y.cpp
?#include?"Barney.hpp"
?Barney?y;
```
`Barney`構造函數的全部看起來可能是象這樣的:
```
//?File?Barney.cpp
?#include?"Barney.hpp"
?Barney::Barney()
?{
???//?...
???x.goBowling();
???//?...
?}
```
正如以上所描述的,由于它們位于不同的源文件,那么 `y` 在 `x` 之前構造而發生災難的機率是50%。
這個問題有許多解決方案,但一個非常簡便的方案就是用一個返回`Fred`對象引用的全局函數`x()`,來取代全局的`Fred`對象 `x`。
```
//?File?x.cpp
?#include?"Fred.hpp"
?Fred&?x()
?{
???static?Fred*?ans?=?new?Fred();
???return?*ans;
?}
```
由于靜態局部對象只在控制流第一次越過它們的聲明時構造,因此以上的`new?Fred()`語句只會執行一次:`x()`被第一次調用時。每個后續的調用將返回同一個`Fred`對象(`ans`指向的那個)。然后你所要做的就是將 `x` 改成 `x()`:
```
//?File?Barney.cpp
?#include?"Barney.hpp"
?Barney::Barney()
?{
???//?...
???x().goBowling();
???//?...
?}
```
由于該全局的`Fred`對象在首次使用時被構造,因此被稱為_首次使用時構造法(Construct On First Use Idiom)_
這種方法的不利方面是`Fred`對象不會被析構。_C++ FAQ Book_有另一種技巧消除這個影響(但面臨了“static _de_-initialization order fiasco”的代價)。
注意:對于內建/固有類型,象`int` 或 `char*`,不必這樣做。例如,如果創建一個靜態的或全局的`float`對象,不需要將它包裹于函數之中。靜態初始化次序真正會崩潰的時機只有在你的`static`或全局對象有構造函數時。
## 10.13 對于靜態數據成員,如何防止“`static` initialization order fiasco”?
使用與描述過的相同的技巧,但這次使用靜態成員函數而不是全局函數而已。
假設類 `X` 有一個`static` `Fred`對象:
```
//?File?X.hpp
?class?X?{
?public:
???//?...
?private:
???static?Fred?x_;
?};
```
自然的,該靜態成員被分開初始化:
```
//?File?X.cpp
?#include?"X.hpp"
?Fred?X::x_;
```
自然的,`Fred`對象會在 `X` 的一個或多個方法中被使用:
```
?void?X::someMethod()
?{
???x_.goBowling();
?}
```
但現在“災難情景”就是如果某人在某處不知何故在`Fred`對象被構造前調用這個方法。例如,如果某人在靜態初始化期間創建一個靜態的 `X` 對象并調用它的`someMethod()`方法,然后你就受制于編譯器是在`someMethod()`被調用之前或之后構造 `X::x_`。(ANSI/ISO C++委員會正在設法解決這個問題,但諸多的編譯器對處理這些更改一般還沒有完成;關注此處將來的更新。)
無論何種結果,將`X::x_` 靜態數據成員改為靜態成員函數總是最簡便和安全的:
```
//?File?X.hpp
?class?X?{
?public:
???//?...
?private:
???static?Fred&?x();
?};
```
自然的,該靜態成員被分開初始化:
```
//?File?X.cpp
?#include?"X.hpp"
?Fred&?X::x()
?{
???static?Fred*?ans?=?new?Fred();
???return?*ans;
?}
```
然后,簡單地將 `x_` 改為 `x()`:
```
?void?X::someMethod()
?{
???x().goBowling();
?}
```
如果你對性能敏感并且關心每次調用`X::someMethod()`的額外的函數調用的開銷,你可以設置一個`static` `Fred&`來取代。正如你所記得的,靜態局部對象僅被初始化一次(控制流程首次越過它們的聲明處時),因此,將只調用`X::x()`一次:`X::someMethod()`首次被調用時:
```
?void?X::someMethod()
?{
???static?Fred&?x?=?X::x();
???x.goBowling();
?}
```
注意:對于內建/固有類型,象`int` 或 `char*`,不必這樣做。例如,如果創建一個靜態的或全局的`float`對象,不需要將它包裹于函數之中。靜態初始化次序真正會崩潰的時機只有在你的`static`或全局對象有構造函數時。
## 10.14 如何處理構造函數的失敗?
拋出一個異常。詳見 [17.2]。
## 10.15 什么是“命名參數法(Named Parameter Idiom)”?
發掘方法鏈的非常有用的方法。
命名參數法(Named Parameter Idiom)解決的最基本問題是C++僅支持位置相關的參數。例如,函數調用者不能說“這個值給形參`xyz`,另一個值給形參`pqr`”。在C++(和C 和Java)中只能說“這是第一個參數,這是第二個參數等”。Ada語言提出并實現的命名參數,對于帶有大量的可缺省參數的函數尤其有用。
多年來,人們構造了很多方案來彌補C 和 C++缺乏的命名參數。其中包括將參數值隱藏于一個字符串參數,然后在運行時解析這個字符串。例如,這就是`fopen()`的第二個參數的做法。另一種方案是將所有的布爾參數聯合成一個位映射,然后調用者將這堆轉換成位的常量共同產生一個實際的參數。例如,這就是`open()`的第二個參數的做法。這些方法可以工作,但下面的技術產生的調用者的代碼更明顯,更容易寫,更容易讀,而且一般來說更雅致。
這個想法,稱為命名參數法(Named Parameter Idiom),它是將函數的參數變為以新的方式創建的類的方法,這些方法通過引用返回`*this`。然后你只要將主要的函數改名為那個類中的無參數的“隨意”方法。
舉一個例子來解釋上面那段。
這個例子實現“打開一個文件”的概念。該概念邏輯上需要一個文件名的參數,和一些允許選擇的參數,文件是否被只讀或可讀寫或只寫的方式打開;如果文件不存在,是否創建它;是從末尾寫(添加"append")還是從起始處寫(覆蓋"overwrite");如果文件被創建,指定塊大小; I/O是否有緩沖區,緩沖區大小;文件是被共享還是獨占訪問;以及其他可能的選項。如果我們用常規的位置相關的參數的函數實現這個概念,那么調用者的代碼會非常難讀:有8個可選的參數,并且調用者很可能犯錯誤。因此我們使用命名參數用法來取代。
在實現它之前,假如你想接受函數的所有默認參數,看一下調用者的代碼是什么樣子:
```
File?f?=?OpenFile("foo.txt");
```
那是簡單的情況。現在看一下如果你想改變一大堆的參數:
```
?File?f?=?OpenFile("foo.txt").
????????????readonly().
????????????createIfNotExist().
????????????appendWhenWriting().
????????????blockSize(1024).
????????????unbuffered().
????????????exclusiveAccess();
```
注意這些“參數”,被公平的以隨機的順序(位置無關的)調用并且都有名字。因此,程序員不必記住參數的順序,而且這些名字是(正如所希望的)意義明顯的。
以下是如何實現:首先創建一個新的類(`OpenFile`),該類包含了所有的參數值作為 `private:` 數據成員。然后所有的方法(`readonly()`, `blockSize(unsigned)`, 等)返回`*this`(也就是返回一個`OpenFile`對象的引用,以允許方法被鏈狀調用)。最后完成一個帶有必要參數(在這里,就是文件名)的常規的,參數位置相關的`OpenFile`的構造函數。
```
?class?File;
?class?OpenFile?{
?public:
???OpenFile(const?string&?filename);
???//?為每個數據成員設置默認值
???OpenFile&?readonly();??//?將?readonly_?變為?true
???OpenFile&?createIfNotExist();
???OpenFile&?blockSize(unsigned?nbytes);
???//?...
?private:
???friend?File;
???bool?readonly_;???????//?默認為?false?[舉例]
//?...
???unsigned?blockSize_;??//?默認為 4096?[舉例]
//?...
?};
```
要做的另外一件事就是使得 `File`的構造函數帶一個`OpenFile`對象:
```
?class?File?{
?public:
???File(const?OpenFile&?params);
???//?vacuums?the?actual?params?out?of?the?OpenFile?object
//?...
?};
```
注意`OpenFile` 將 `File` 聲明為友元。
- C++ FAQ Lite
- [1] 復制許可
- [2] 在線站點分發本文檔
- [3] C++-FAQ-Book 與 C++-FAQ-Lite
- [6] 綜述
- [7] 類和對象
- [8] 引用
- [9] 內聯函數
- [10] 構造函數
- [11] 析構函數
- [12] 賦值算符
- [13] 運算符重載
- [14] 友元
- [15] 通過 &lt;iostream&gt; 和 &lt;cstdio&gt;輸入/輸出
- [16] 自由存儲(Freestore)管理
- [17] 異常和錯誤處理
- [18] const正確性
- [19] 繼承 — 基礎
- [20] 繼承 — 虛函數
- [21] 繼承 — 適當的繼承和可置換性
- [22] 繼承 — 抽象基類(ABCs)
- [23] 繼承 — 你所不知道的
- [24] 繼承 — 私有繼承和保護繼承
- [27] 編碼規范
- [28] 學習OO/C++
- [31] 引用與值的語義
- [32] 如何混合C和C++編程
- [33] 成員函數指針
- [35] 模板 ?
- [36] 序列化與反序列化
- [37] 類庫