C++的一大誤區——深入解釋直接初始化與複製初始化的區別
阿新 • • 發佈:2019-01-27
不久前,在部落格上發表了一篇文章——,對於其中最後一點,多使用直接初始化,有很多讀者向我提出了疑問,並寫了一些測試程式,來說明直接初始化與複製初始化是同一件事。讓我瞭解到大家對於直接初始化與複製初始化的區別的確是不太清楚,無可否認,那篇文章的例子用得的確不太好,在這裡表示歉意!所以我覺得還是有必要跟大家詳細分享一下我對直接初始化和複製初始化的理解。
一、Primer中的說法
首先我們來看看經典是怎麼說的:
“當用於類型別物件時,初始化的複製形式和直接形式有所不同:直接初始化直接呼叫與實參匹配的建構函式,複製初始化總是呼叫複製建構函式。複製初始化首先使用指定建構函式建立一個臨時物件,然後用複製建構函式將那個臨時物件複製到正在建立的物件”
還有一段這樣說,
“通常直接初始化和複製初始化僅在低級別優化上存在差異,然而,對於不支援複製的型別,或者使用非explicit建構函式的時候,它們有本質區別:
ifstream file1("filename")://ok:direct initialization
ifstream file2 = "filename";//error:copy constructor is private
”
二、通常的誤解
從上面的說法中,我們可以知道,直接初始化不一定要呼叫複製建構函式,而複製初始化一定要呼叫複製建構函式。然而大多數人卻認為,直接初始化是構造物件時要呼叫複製建構函式,而複製初始化是構造物件時要呼叫賦值操作函式(operator=),其實這是一大誤解。因為只有物件被建立才會出現初始化,而賦值操作並不應用於物件的建立過程中,且primer也沒有這樣的說法。至於為什麼會出現這個誤解,可能是因為複製初始化的寫法中存在等號(=)吧。
為了把問題說清楚,還是從程式碼上來解釋比較容易讓人明白,請看下面的程式碼:
從輸出的結果,我們可以知道物件的構造到底呼叫了哪些函式,從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的函式語句註釋起來,如下所示:
輸出結果為:#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);而且他們都沒有呼叫賦值操作函式,所以它們都是直接初始化,然而事實是否真的如你所想的那樣呢?答案顯然不是。 三、層層推進,到底誰欺騙了我們
然而你還是非常遺憾地發現,還是沒有編譯通過。這是為什麼呢?從上面的語句和之前的執行結果來看,的確是已經沒有呼叫複製構造函數了,為什麼還是編譯錯誤呢? 經過實驗,main函式只有這樣才能通過編譯: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; }
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),編譯器就是根據這個特性來對程式碼進行優化的。然而如裡你自己定義這個複製建構函式,編譯則不會自動生成,雖然編譯不會自動生成,但是如果你自己定義的複製建構函式仍是公有的話,編譯還是會為你做同樣的優化。然而當它是私有成員時,編譯器就會有很不同的舉動,因為你明確地告訴了編譯器,你明確地拒絕了物件之間的複製操作,所以它也就不會幫你做之前所做的優化,你的程式碼的本來面目就出來了。
舉個例子來說,就像下面的語句:
ClassTest ct2 = "ab";
它本來是要這樣來構造物件的:首先呼叫建構函式ClassTest(const char *pc)函式建立一個臨時物件,然後呼叫複製建構函式,把這個臨時物件作為引數,構造物件ct2。然而編譯也發現,複製建構函式是公有的,即你明確地告訴了編譯器,你允許物件之間的複製,而且此時它發現可以通過直接呼叫過載的建構函式ClassTest(const char *pc)來直接初始化物件,而達到相同的效果,所以就把這條語句優化為ClassTest
ct2("ab")。
而如果把複製建構函式宣告為私有的,則物件之前的複製不能進行,即不能把臨時對像作為引數,呼叫複製建構函式,所以編譯就認為ClassTest ct2 = "ab"與ClassTest ct2("ab")是不等價的,也就不會幫你做這個優化,所以編譯出錯了。
注:根據上面的程式碼,有些人可能會執行出與本人測試不一樣的結果,這是為什麼呢?就像前面所說的那樣,編譯器會為程式碼做一定的優化,但是不同的編譯器所作的優化的方案卻可能有所不同,所以當你使用不同的編譯器時,由於這些優化的方案不一樣,可能會產生不同的結果,我這裡用的是g++4.7。