一、拷貝構造函數
當類沒有定義拷貝構造函數的時候,編譯器會默認提供一個,這個拷貝函數是淺拷貝。
如果該類中含有指針,可能會發生內存泄漏,見下面的例子:
class Test { public: int *p; Test(){ p=new int; }; ~Test(){ delete p; }; }; void main() { Test t1; Test t2(t1); Test t3 = t1; }
t1、t2、t3的成員變量p指向的是同一塊內存,程序結束后會出現重復釋放的問題。
為了解決這個問題,可以自定義拷貝構造函數:
class Test { public: int *p; Test(const Test &t) { p = new int (*(t.p)); } Test(){ p=new int; }; ~Test(){ delete p; }; };
二、右值引用
除了上述的解決方法,還可以使用C++11的【右值引用】新特性來解決,而且可以提高程序的性能,減少內存開銷。
為了引出左值引用的概念,先來復習左值和右值
1.左值和右值
int a = 3 + 4 ;
上面的式子中,變量 a 就是左值,右邊的表達式會生成一個臨時變量存放 (3+4) 的值,這個變量稱之為右值。
有兩種方式可以判斷:
(1)只能放在等號(=)右側的即為右值,可以放在左側的為左值
int a = 10 ; 10 = a ; //錯誤
(2)左值可以取地址,而右值不允許:
int a = 3 + 4 ; int * b = & a ; //ok b = & (3+4) ; //錯誤
2.右值引用
使用方法如下,b就是對右值 (3+4) 的引用。
int && b = 3 + 4 ;
先看下下面的左值引用:
int a = 0 ; int &b = 4 ; //錯誤! int &b = a ; //左值引用
如上例所示,左值引用只能對左值進行別名引用,無法引用右值
於是C++11增加了右值引用,使用 && 表示(和邏輯運算中的”且“一致)。
int a = 0 ; int b = 1 ; int && c = a+c ; //右值引用 int && c = 3 ; //右值引用 int && c = 3 +4 ; //右值引用 int && c = a ; //錯誤!
注意不能直接右值引用左值,C++提供了一個函數std::move()函數,可以將左值變成右值:
string str1 = "aa" ; string && str2 = std::move( str1 ); //ok
3.右值引用的應用場景
(1)案例:
還是回到之前的例子:
class Test { public: int *p; Test(const Test &t) { p = new int (*(t.p)); cout<<"copy construct"<<endl; } Test(){ p=new int; cout<<"construct"<<endl; }; ~Test(){ delete p; cout<<"destruct"<<endl; }; }; Test getTest() { return Test(); } void main() { { Test t = getTest(); } }
使用vs2012運行,結果為:
construct //執行 Test() destruct //銷毀 t
但需要注意的是,這是vs編譯器對拷貝構造函數優化后的結果。禁止優化,結果為:
construct //執行 Test() copy construct //執行 return Test() destruct //銷毀 Test() 產生的匿名對象 copy construct //執行 t = getTest() destruct //銷毀 getTest() 返回的臨時對象 destruct //銷毀 t
可以看到,進行了兩次的深拷貝,對於對內存要求不高、本例這種占內存比較小的類Test而言(申請的堆空間小),可以接受。
但如果臨時對象中的指針成員申請了大量的堆空間,那將嚴重影響程序的執行效率。
C++11為了解決這一問題(深拷貝占用大量空間),引入移動構造函數。
(2)移動構造函數
所謂的移動,就是將其他的內存資源,“移為己有”,這些資源通常是臨時對象,比如上文所敘的右值。
修改如下(增加一個移動構造函數):
class Test { public: int *p; Test(Test &&t) //移動構造函數 { p = t.p; t.p = nullptr;//將臨時對象的指針賦值為空 cout<<"copy construct"<<endl; } Test(const Test &t) //拷貝構造函數 { p = new int (*(t.p)); cout<<"move construct"<<endl; } Test(){ p=new int; cout<<"construct"<<endl; }; ~Test(){ delete p; cout<<"disconstruct"<<endl; }; }; Test getTest() { return Test(); } void main() { { Test t = getTest(); } }
禁止vs優化,結果為:
construct //執行 Test() move construct //執行 return Test() destruct //銷毀 Test() 產生的匿名對象 move construct //執行 t = getTest() destruct //銷毀 getTest() 返回的臨時對象 destruct //銷毀 t
可以看到,定義了移動構造函數后,臨時對象的創建使用移動構造函數創建,如下,沒有在堆上創建對象,減少了開銷。
Test(Test &&t) //移動構造函數 { p = t.p; t.p = nullptr;//將臨時對象的指針賦值為空 cout<<"copy construct"<<endl; }
那么問題來了,什么時候調用移動構造函數,什么時候調用拷貝構造函數呢?將在后面的文章中分析。