C++中直接初始化與複製初始化是很多初學者容易混淆的概念,本文就以實例形式講述二者之間的區別。供大家參考之用。具體分析如下:
一、Primer中的說法
首先我們現來看看經典是怎麼說的:
“當用於類類型對象時,初始化的複製形式和直接形式有所不同:直接初始化直接調用與實參匹配的構造函數,複製初始化總是調用複製構造函數。複製初始化首先使用指定構造函數創建一個臨時對象,然後用複製構造函數將那個臨時對象複製到正在創建的對象”
還有一段這樣說:
“通常直接初始化和複製初始化僅在低級別優化上存在差異,然而,對於不支持複製的類型,或者使用非explicit構造函數的時候,它們有本質區別:
1
2
|
ifstream file1(
"filename"
):
//ok:direct initialization
ifstream file2 =
"filename"
;
//error:copy constructor is private”
|
二、通常的誤解
從上面的說法中,我們可以知道,直接初始化不一定要調用複製構造函數,而複製初始化一定要調用複製構造函數。然而大多數人卻認爲,直接初始化是構造對象時要調用複製構造函數,而複製初始化是構造對象時要調用賦值操作函數(operator=),其實這是一大誤解。因爲只有對象被創建纔會出現初始化,而賦值操作並不應用於對象的創建過程中,且primer也沒有這樣的說法。至於爲什麼會出現這個誤解,可能是因爲複製初始化的寫法中存在等號(=)吧。
爲了把問題說清楚,還是從代碼上來解釋比較容易讓人明白,請看下面的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
#include <iostream>
#include <cstring>
using
namespace
std;
class
ClassTest
{
public
:
ClassTest()
{
c[0] =
'\0'
;
cout<<
"ClassTest()"
<<endl;
}
ClassTest& operator=(
const
ClassTest &ct)
{
strcpy
(c, ct.c);
cout<<
"ClassTest& operator=(const ClassTest &ct)"
<<endl;
return
*
this
;
}
ClassTest(
const
char
*pc)
{
strcpy
(c, pc);
cout<<
"ClassTest (const char *pc)"
<<endl;
}
// private:
ClassTest(
const
ClassTest& ct)
{
strcpy
(c, ct.c);
cout<<
"ClassTest(const ClassTest& ct)"
<<endl;
}
private
:
char
c[256];
};
int
main()
{
cout<<
"ct1: "
;
ClassTest ct1(
"ab"
);
//直接初始化
cout<<
"ct2: "
;
ClassTest ct2 =
"ab"
;
//複製初始化
cout<<
"ct3: "
;
ClassTest ct3 = ct1;
//複製初始化
cout<<
"ct4: "
;
ClassTest ct4(ct1);
//直接初始化
cout<<
"ct5: "
;
ClassTest ct5 = ClassTest();
//複製初始化
return
0;
}
|
輸出結果爲:
從輸出的結果,我們可以知道對象的構造到底調用了哪些函數,從ct1與ct2、ct3與ct4的比較中可以看出,ct1與ct2對象的構建調用的都是同一個函數——ClassTest(const char *pc),同樣道理,ct3與ct4調用的也是同一個函數——ClassTest(const ClassTest& ct),而ct5則直接調用了默認構造函數。
於是,很多人就認爲ClassTest ct1("ab");等價於ClassTest ct2 = "ab";,而ClassTest ct3 = ct1;也等價於ClassTest ct4(ct1);而且他們都沒有調用賦值操作函數,所以它們都是直接初始化,然而事實是否真的如你所想的那樣呢?答案顯然不是。
三、層層推進,到底誰欺騙了我們
很多時候,自己的眼睛往往會欺騙你自己,這裏就是一個例子,正是你的眼睛欺騙了你。爲什麼會這樣?其中的原因在談優化時的補充中也有說明,就是因爲編譯會幫你做很多你看不到,你也不知道的優化,你看到的結果,正是編譯器做了優化後的代碼的運行結果,並不是你的代碼的真正運行結果。
你也許不相信我所說的,那麼你可以把類中的複製函數函數中面註釋起來的那行取消註釋,讓複製構造函數成爲私有函數再編譯運行這個程序,看看有什麼結果發生。
很明顯,發生了編譯錯誤,從上面的運行結果,你可能會認爲是因爲ct3和ct4在構建過程中用到了複製構造函數——ClassTest(const ClassTest& ct),而現在它變成了私有函數,不能在類的外面使用,所以出現了編譯錯誤,但是你也可以把ct3和ct4的函數語句註釋起來,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
int
main()
{
cout<<
"ct1: "
;
ClassTest ct1(
"ab"
);
cout<<
"ct2: "
;
ClassTest ct2 =
"ab"
;
// cout<<"ct3: ";
// ClassTest ct3 = ct1;
// cout<<"ct4: ";
// ClassTest ct4(ct1);
cout<<
"ct5: "
;
ClassTest ct5 = ClassTest();
return
0;
}
|
然而你還是非常遺憾地發現,還是沒有編譯通過。這是爲什麼呢?從上面的語句和之前的運行結果來看,的確是已經沒有調用複製構造函數了,爲什麼還是編譯錯誤呢?
經過實驗,main函數只有這樣才能通過編譯:
1
2
3
4
5
6
|
int
main()
{
cout<<
"ct1: "
;
ClassTest ct1(
"ab"
);
return
0;
}
|
在這裏我們可以看到,原來是複製構造函數欺騙了我們。
四、揭開真相
看到這裏,你可能已經大驚失色,下面就讓我來揭開這個真相吧!
還是那一句,什麼是直接初始化,而什麼又是複製初始化呢?
簡單點來說,就是定義對象時的寫法不一樣,一個用括號,如ClassTest ct1("ab"),而一個用等號,如ClassTest ct2 = "ab"。
但是從本質來說,它們卻有本質的不同:直接初始化直接調用與實參匹配的構造函數,複製初始化總是調用複製構造函數。複製初始化首先使用指定構造函數創建一個臨時對象,然後用複製構造函數將那個臨時對象複製到正在創建的對象。所以當複製構造函數被聲明爲私有時,所有的複製初始化都不能使用。
現在我們再來看回main函數中的語句:
1、ClassTest ct1("ab");這條語句屬於直接初始化,它不需要調用複製構造函數,直接調用構造函數ClassTest(const char *pc),所以當複製構造函數變爲私有時,它還是能直接執行的。
2、ClassTest ct2 = "ab";這條語句爲複製初始化,它首先調用構造函數ClassTest(const char *pc)函數創建一個臨時對象,然後調用複製構造函數,把這個臨時對象作爲參數,構造對象ct2;所以當複製構造函數變爲私有時,該語句不能編譯通過。
3、ClassTest ct3 = ct1;這條語句爲複製初始化,因爲ct1本來已經存在,所以不需要調用相關的構造函數,而直接調用複製構造函數,把它值複製給對象ct3;所以當複製構造函數變爲私有時,該語句不能編譯通過。
4、ClassTest ct4(ct1);這條語句爲直接初始化,因爲ct1本來已經存在,直接調用複製構造函數,生成對象ct3的副本對象ct4。所以當複製構造函數變爲私有時,該語句不能編譯通過。
注:第4個對象ct4與第3個對象ct3的創建所調用的函數是一樣的,但是本人卻認爲,調用複製函數的原因卻有所不同。因爲直接初始化是根據參數來調用構造函數的,如ClassTest ct4(ct1),它是根據括號中的參數(一個本類的對象),來直接確定爲調用複製構造函數ClassTest(const ClassTest& ct),這跟函數重載時,會根據函數調用時的參數來調用相應的函數是一個道理;而對於ct3則不同,它的調用並不是像ct4時那樣,是根據參數來確定要調用複製構造函數的,它只是因爲初始化必然要調用複製構造函數而已。它理應要創建一個臨時對象,但只是這個對象卻已經存在,所以就省去了這一步,然後直接調用複製構造函數,因爲複製初始化必然要調用複製構造函數,所以ct3的創建仍是複製初始化。
5、ClassTest ct5 = ClassTest();這條語句爲複製初始化,首先調用默認構造函數產生一個臨時對象,然後調用複製構造函數,把這個臨時對象作爲參數,構造對象ct5。所以當複製構造函數變爲私有時,該語句不能編譯通過。
五、假象產生的原因
產生上面的運行結果的主要原因在於編譯器的優化,而爲什麼把複製構造函數聲明爲私有(private)就能把這個假象去掉呢?主要是因爲複製構造函數是可以由編譯默認合成的,而且是公有的(public),編譯器就是根據這個特性來對代碼進行優化的。然而如裏你自己定義這個複製構造函數,編譯則不會自動生成,雖然編譯不會自動生成,但是如果你自己定義的複製構造函數仍是公有的話,編譯還是會爲你做同樣的優化。然而當它是私有成員時,編譯器就會有很不同的舉動,因爲你明確地告訴了編譯器,你明確地拒絕了對象之間的複製操作,所以它也就不會幫你做之前所做的優化,你的代碼的本來面目就出來了。
舉個例子來說,就像下面的語句:
1
|
ClassTest ct2 =
"ab"
;
|
它本來是要這樣來構造對象的:首先調用構造函數ClassTest(const char *pc)函數創建一個臨時對象,然後調用複製構造函數,把這個臨時對象作爲參數,構造對象ct2。然而編譯也發現,複製構造函數是公有的,即你明確地告訴了編譯器,你允許對象之間的複製,而且此時它發現可以通過直接調用重載的構造函數ClassTest(const char *pc)來直接初始化對象,而達到相同的效果,所以就把這條語句優化爲ClassTest ct2("ab")。
而如果把複製構造函數聲明爲私有的,則對象之前的複製不能進行,即不能把臨時對像作爲參數,調用複製構造函數,所以編譯就認爲ClassTest ct2 = "ab"與ClassTest ct2("ab")是不等價的,也就不會幫你做這個優化,所以編譯出錯了。
注:根據上面的代碼,有些人可能會運行出與本人測試不一樣的結果,這是爲什麼呢?就像前面所說的那樣,編譯器會爲代碼做一定的優化,但是不同的編譯器所作的優化的方案卻可能有所不同,所以當你使用不同的編譯器時,由於這些優化的方案不一樣,可能會產生不同的結果,我這裏用的是g++4.7。
相信本文所述對大家深入學習C++程序設計有一定的參考借鑑作用。