1. 程式人生 > >C++直接初始化與拷貝初始化學習筆記

C++直接初始化與拷貝初始化學習筆記

初始化分為兩種

1. 直接初始化直接呼叫與實參匹配的建構函式,形式如“T t(u)”。
2. 拷貝初始化:複製初始化首先使用指定建構函式建立一個臨時物件,然後用複製建構函式將那個臨時物件複製到正在建立的物件”,形式如“T t=u”。


直接初始化不一定要呼叫複製建構函式
複製初始化一定要呼叫複製建構函式

注意:一般而言,在變數初始化時,儘量採用直接初始化形式,而不是拷貝初始化。

class T :
  public S {
     public:
         T() : S(1), x(2) {}   //基類初始化與成員初始化
         X x;
};
//基類與成員的初始化兜採用的是直接初始化。
T f( T t) { return t; } //傳遞函式引數與返回值 /////答案:值的傳遞與返回都是採用的拷貝初始化。 S s; T t; S& r = t; reinterpret_cast< S& > (t); dynamic_cast< T& > (r); const_cast< const T& > (t); //////答案:以上各種轉換並沒有涉及到新物件的產生,只是建立了引用而已。///////// static_cast< S > (t); ///////答案:static_cast採用直接初始化。////////// try { throw T(); } //丟擲異常
catch (T t) {} //處理異常 //////答案:異常物件的丟擲與捕獲都採用拷貝初始化。//////// f( T(s) ); //函式風格的型別轉換採用的是直接初始化 S a[3] = {1, 2, 3}; //大括號初始化語句採用的是拷貝初始化 S* p = new S(6); //new表示式採用的是直接初始化

另附上程式碼:

#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的函式語句註釋起來,如下所示:

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函式只有這樣才能通過編譯

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。