1. 程式人生 > >為什麼C++中拷貝建構函式的引數型別必須是引用?

為什麼C++中拷貝建構函式的引數型別必須是引用?

  在C++中, 建構函式,拷貝建構函式,解構函式和賦值函式(賦值運算子過載)是最基本不過的需要掌握的知識。 但是如果我問你“拷貝建構函式的引數為什麼必須使用引用型別?”這個問題, 你會怎麼回答? 或許你會回答為了減少一次記憶體拷貝? 很慚愧的是,我的第一感覺也是這麼回答。不過還好,我思索一下以後,發現這個答案是不對的。

原因:
       如果拷貝建構函式中的引數不是一個引用,即形如CClass(const CClass c_class),那麼就相當於採用了傳值的方式(pass-by-value),而傳值的方式會呼叫該類的拷貝建構函式,從而造成無窮遞迴地呼叫拷貝建構函式。因此拷貝建構函式的引數必須是一個引用。
       需要澄清的是,傳指標其實也是傳值,如果上面的拷貝建構函式寫成CClass(const CClass* c_class),也是不行的。事實上,只有傳引用不是傳值外,其他所有的傳遞方式都是傳值。


       先從一個小例子開始:(自己測試一下自己看看這個程式的輸出是什麼?)

  1. #include<iostream>
  2. usingnamespace std;  
  3. class CExample  
  4. {  
  5. private:  
  6.     int m_nTest;  
  7. public:  
  8.     CExample(int x) : m_nTest(x)      //帶引數建構函式
  9.     {   
  10.         cout << "constructor with argument"<<endl;  
  11.     }  
  12.     // 拷貝建構函式,引數中的const不是嚴格必須的,但引用符號是必須的
  13.     CExample(const CExample & ex)     //拷貝建構函式
  14.     {  
  15.         m_nTest = ex.m_nTest;  
  16.         cout << "copy constructor"<<endl;  
  17.     }  
  18.     CExample& operator = (const CExample &ex)   //賦值函式(賦值運算子過載)
  19.     {     
  20.         cout << "assignment operator"<<endl;  
  21.         m_nTest = ex.m_nTest;  
  22.         return *this;  
  23.     }  
  24.     void myTestFunc(CExample ex)  
  25.     {  
  26.     }  
  27. };  
  28. int main(void)  
  29. {  
  30.     CExample aaa(2);  
  31.     CExample bbb(3);  
  32.     bbb = aaa;  
  33.     CExample ccc = aaa;  
  34.     bbb.myTestFunc(aaa);  
  35.     return 0;     
  36. }  
這個例子的輸出結果是:
  1. constructor with argument        // CExample aaa(2);
  2. constructor with argument        // CExample bbb(3);
  3. assignment operator              // bbb = aaa;
  4. copy constructor                 // CExample ccc = aaa;
  5. copy constructor                 //  bbb.myTestFunc(aaa);

如果你能一眼看出就是這個結果的話, 恭喜你,可以站起來扭扭屁股,不用再往下看了。

如果你的結果和輸出結果有誤差, 那拜託你謙虛的看完。

第一個輸出: constructor with argument      // CExample aaa(2);

如果你不理解的話, 找個人把你拖出去痛打一頓,然後嘴裡還喊著“我是二師兄,我是二師兄.......”

第二個輸出:constructor with argument     // CExample bbb(3);

分析同第一個

第三個輸出: assignment operator                // bbb = aaa;

第四個輸出: copy constructor                      // CExample ccc = aaa;

這兩個得放到一塊說。 肯定會有人問為什麼兩個不一致。原因是, bbb物件已經例項化了,不需要構造,此時只是將aaa賦值給bbb,只會呼叫賦值函式,就這麼簡單,還不懂的話,撞牆去! 但是ccc還沒有例項化,因此呼叫的是拷貝建構函式,構造出ccc,而不是賦值函式,還不懂的話,我撞牆去!!

第五個輸出: copy constructor                      //  bbb.myTestFunc(aaa);

實際上是aaa作為引數傳遞給bbb.myTestFunc(CExample ex), 即CExample ex = aaa;和第四個一致的, 所以還是拷貝建構函式,而不是賦值函式, 如果仍然不懂, 我的頭剛才已經流血了,不要再讓我撞了,你就自己使勁的再裝一次吧。

通過這個例子, 我們來分析一下為什麼拷貝建構函式的引數只能使用引用型別。

看第四個輸出: copy constructor                      // CExample ccc = aaa;

構造ccc,實質上是ccc.CExample(aaa); 我們假如拷貝建構函式引數不是引用型別的話, 那麼將使得 ccc.CExample(aaa)變成aaa傳值給ccc.CExample(CExample ex),即CExample ex = aaa,因為 ex 沒有被初始化, 所以 CExample ex = aaa 繼續呼叫拷貝建構函式,接下來的是構造ex,也就是 ex.CExample(aaa),必然又會有aaa傳給CExample(CExample ex), 即 CExample ex = aaa;那麼又會觸發拷貝建構函式,就這下永遠的遞迴下去。

所以繞了那麼大的彎子,就是想說明拷貝建構函式的引數使用引用型別不是為了減少一次記憶體拷貝, 而是避免拷貝建構函式無限制的遞迴下去。

附帶說明,在下面幾種情況下會呼叫拷貝建構函式:
a、   顯式或隱式地用同類型的一個物件來初始化另外一個物件。如上例中,用物件c初始化d;
b、  作為實參(argument)傳遞給一個函式。如CClass(const CClass c_class)中,就會呼叫CClass的拷貝建構函式;
c、  在函式體內返回一個物件時,也會呼叫返回值型別的拷貝建構函式;
d、  初始化序列容器中的元素時。比如 vector<string> svec(5),string的預設建構函式和拷貝建構函式都會被呼叫;
e、  用列表的方式初始化陣列元素時。string a[] = {string(“hello”), string(“world”)}; 會呼叫string的拷貝建構函式。
如果在沒有顯式宣告建構函式的情況下,編譯器都會為一個類合成一個預設的建構函式。如果在一個類中聲明瞭一個建構函式,那麼就會阻止編譯器為該類合成預設的建構函式。和建構函式不同的是,即便定義了其他建構函式(但沒有定義拷貝建構函式),編譯器總是會為我們合成一個拷貝建構函式。
另外函式的返回值是不是引用也有很大的區別,返回的不是引用的時候,只是一個簡單的物件,此時需要呼叫拷貝建構函式,否則,如果是引用的話就不需要呼叫拷貝建構函式。
  1. #include<iostream>
  2. usingnamespace std;  
  3. class A  
  4. {  
  5. private:  
  6.     int m_nTest;  
  7. public:  
  8.     A()  
  9.     {  
  10.     }  
  11.     A(const A& other)    //建構函式過載
  12.     {  
  13.         m_nTest = other.m_nTest;  
  14.         cout << "copy constructor"<<endl;    
  15.     }  
  16.     A & operator =(const A& other)  
  17.     {  
  18.         if(this != &other)  
  19.         {  
  20.             m_nTest = other.m_nTest;  
  21.             cout<<"Copy Assign"<<endl;  
  22.         }  
  23.         return *this;  
  24.     }  
  25. };  
  26. A fun(A &x)  
  27. {  
  28.     return x;     //返回的不是引用的時候,需要呼叫拷貝建構函式
  29. }  
  30. int main(void)  
  31. {  
  32.     A test;  
  33.     fun(test);  
  34.     system("pause");  
  35.     return 0;  
  36. }  

分享一道筆試題目,編譯執行下圖中的C++程式碼,結果是什麼?(A)編譯錯誤;(B)編譯成功,執行時程式崩潰;(C)編譯執行正常,輸出10。請選擇正確答案並分析原因。

  1. class A  
  2. {  
  3. private:  
  4.     int value;  
  5. public:  
  6.     A(int n)  
  7.     {  
  8.         value = n;  
  9.     }  
  10.     A(A other)  
  11.     {  
  12.         value = other.value;  
  13.     }  
  14.     void Print()  
  15.     {  
  16.         cout<<value<<endl;  
  17.     }  
  18. };  
  19. int main(void)  
  20. {  
  21.     A a = 10;  
  22.     A b = a;  
  23.     b.Print();  
  24.     return 0;  
  25. }  
答案:編譯錯誤。在複製建構函式中傳入的引數是A的一個例項。由於是傳值,把形參拷貝到實參會呼叫複製建構函式。因此如果允許複製建構函式傳值,那麼會形成永無休止的遞歸併造成棧溢位。因此C++的標準不允許複製建構函式傳值引數,而必須是傳引用或者常量引用。在Visual Studio和GCC中,都將編譯出錯。