也談C++深拷貝、淺拷貝和函數返回值作參數及其臨時變量的生存期

    爲什麼會要會想要談談這個話題呢,因爲最近在看書的時候發現一本書上的一個例程有關於用函數返回值賦值一個對象時,註釋說先清除臨時對象,再清除函數內作返回值的局部對象。考慮了下,有些懷疑。於是寫了幾個程序想驗證,結果註釋掉了複製構造函數的聲明作對比。然而,結果卻讓自己困惑了很久,特別是程序6。最後就作了下面的討論。當然,也證明了書上說的是錯誤的。

    這段測試程序代碼如下,打開和關閉註釋可得6個程序:

#include<iostream>
using namespace std;
int count;
class test
{
private:
 int x,y;
public:
 test(){ count=count+1;cout<<"count="<<count<<" "<<"initializing..."<<&x<<endl;};
 test(int a,int b):x(a),y(b){ count=count+1; cout<<"count="<<count<<" "<<"initializing..."<<&x<<endl;}
 test(const test &);
 void Show();
 ~test(){ count=count-1; cout<<"count="<<count<<" "<<"delete"<<x<<","<<y<<&x<<endl;}
};
test::test(const test &t)
{
 x=t.x;
 y=t.y;
 count=count+1; cout<<"count="<<count<<" "<<"copying..."<<&x<<endl;
}
void test::Show()
{
 cout<<"this is "<<x<<","<<y<<endl;
}
test func()
{
 cout<<"entering func"<<endl;
 test A(1,1);
 A.Show();
 return A;
}
void display(test a)
{
 cout<<"entering display"<<endl;
 test A(2,2);
 A.Show();
}
int main()
{
 //打開復制構造函數註釋則是程序1,註釋掉複製構造數聲明及定義是程序4
 test C;
 C=func(); 
 /*
 //打開復制構造函數註釋則是程序2,註釋掉複製構造數聲明及定義是程序5

 test C=func();
 */ 
 /*
 //打開復制構造函數註釋則是程序3,註釋掉複製構造數聲明及定義是程序6

 display(func());
    */
 cout<<"out display"<<endl;
 return 0;
}

 

程序運行結果如下。

 

                   test C;

                   C=func2();

圖1.程序1()和程序4()

 

       test C=func2();

圖2.程序2()和程序5()

 

    display(func2());

圖3.程序3()和程序6():

要理解的幾點:
1.深拷貝和淺拷貝的定義可以簡單理解成:是當拷貝對象中有對其他資源(如堆、文件、系統等)的引用指針或引用時,對象的另開闢一塊新的資源,而不再對拷貝對象中有對其他資源的引用的指針或引用進行單純的賦值。反之,淺拷貝就是對象的數據成員之間的簡單賦值。
2.在C++中。如果對象在聲明對象的同時進行的初始化操作,則進行拷貝運算。
例如:
 test("a");
 test B=A;
此時其實際進行的是B(A)這樣的淺拷貝。
如果在對象聲明之後,再進行的賦值運算,則進行賦值運算。例如:
 test A("a");
 test B;
 B=A;
此時實際調用的是類的缺省賦值函數B.operator=(A);拷貝構造函數和賦值運算,都有缺省的定義。
3.構造函數和拷貝構造函數都用於初始化。而賦值運算是在對象初始化之後才能調用。
 

下面開始討論:
在有顯示定義複製構造函數時,分別執行以下程序。

注:下面分析都不包括func2()以及局部對象的建立和結束時構造與析構動作,即對象A。

 

程序1(用函數返回值賦值):
test C;
C=func2();

 

結果:有一次構造函數(C的構造函數),一次深度複製構造函數(臨時對象的)和兩次析構函數(一次C的,一次臨時對象的)。
判斷:先深拷貝,後默認賦值運算=。先把func2()中的局部類對象深拷貝到函數返回的臨時對象;然後,把func2()返回的臨時對象通過賦值運算給賦值對象C。
執行順序:
func2()的先開始,中途執行完深拷貝給臨時變量之後,再執行賦值運算到C,再返回到func2()結束。之後在表達式結束時析構臨時對象,在main()結束時析構C。
分析:
1.少了一次臨時對象複製到C的顯示覆制構造函數的的調用,是賦值運算。
2.臨時對象的銷燬在表達式結束的最後。所以纔會有func2()中的局部對象雖然先建立,但是卻先銷燬;而臨時變量後建立,卻後銷燬。
3.C在析構時,可見內存位置沒有變化,所以也不可能是引用。而運行結果沒有C調用顯示覆制構造函數的輸出;且C已經聲明在先,不能再構造一次。所以不可能是深拷貝,而應是賦值運算。


程序2(用函數返回值初始化):
test C=func2();

 

結果:沒有構造函數,有一次深度複製構造函數和一次析構函數調用。

判斷:先深拷貝,後引用(可被看成一次深度)。
執行順序:
不於同程序1。func2()的先開始,中途執行完深拷貝之後,再返回到func2()並結束,之後執行優化的淺拷貝。然後在表達式結束時不會析構臨時對象(因爲其身份已改變),在main()結束時析構C。

 

分析:
1.誤解:沒有臨時對象產生。因爲少了一次構造函數調用,也少一次析構函數,就認爲沒有臨時對象產生。因而會被誤認爲只進行了一次深度複製。
    實際上,並非如此。之所以沒有對象C構造函數的調用,是因爲:C++的代碼優化,如果一個臨時對象作爲返回值被立即賦給另一個未構造對象,這個臨時對象本身將被構造到被賦值對象在內存中的位置(優化的淺拷貝),即引用。所以少了一次構造函數的調用。然而,實際上是減少了一次C的構造和一次臨時對象的析構。相當於沒有產生臨時對象,但事實上是產生了臨時對象。調用複製構造函數正其實真正產生的是臨時對象。
2.給變量C作拷貝,不會是深拷貝。因爲沒有調用構造函數或深度複製構造函數,所以只可能是某個已有對象的引用。


程序3(用函數返回值作參數):
display(func2());

 

結果:沒有構造函數,有一次深度複製構造函數和一次析構函數調用。

判斷:先深度,後引用(可被看成一次深度)。在參數傳遞中相當於執行的是test C=func2()。


程序4略

程序5用(函數返回值初始化):

沒有顯示定義複製構造函數時,執行以下程序。
test C=func2();

 

結果:沒有構造函數,沒有深度複製構造函數和一次析構函數調用。
判斷:先深度,後引用(可被看成一次深度)。情況同程序3


程序6(用函數返回值作參數):
沒有顯示定義複製構造函數時,執行以下程序。
display(func2());

 

結果:沒有構造函數,沒有深度複製構造函數和兩次析構函數調用。
判斷:先淺拷貝,再淺拷貝。先把局部對象淺拷貝給臨時對象,再把臨時對象淺拷貝給C。

分析:
因爲,沒有定義顯示覆制構造函數沒有定義,也沒有類對象的聲明。然而,程序輸出地址顯示,產生了兩個不同的類對象。故兩次都是淺拷貝。且臨時變量的生存期被延長到了調用它的函數結束時,最後一個被銷燬。

 

程序6的分析值得商榷!!呵呵.都是愚見,還望斧正!

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章