C++中深拷貝和淺拷貝的問題是很值得我們注意的知識點,如果編程中不注意,可能會出現疏忽,導致bug。本文就詳細講講C++深淺拷貝的種種。
我們知道,對於一般對象:
int a = 1; int b = 2;
這樣的賦值,複製很簡單,但對於類對象來說並不一般,因爲其內部包含各種類型的成員變量,在拷貝過程中就會出現問題
例如:
#include <iostream> using namespace std; class String { public: String(char str = "") :_str(new char[strlen(str) + 1]) // +1是爲了避免空字符串導致出錯 { strcpy(_str , str); } // 淺拷貝 String(const String& s) :_str(s._str) {} ~String() { if(_str) { delete[] _str; _str = NULL; } cout<<"~String()"<<endl; } void Display() { cout<<_str<<endl; } private: char *_str; }; void Test() { String s1("hello"); String s2(s1); String.Display(); } int main() { Test(); return 0; }
運行結果:
我們發現,編譯通過了,但是崩潰了 = =ll ,這就是淺拷貝帶來的問題。
事實是,在對象拷貝過程中,如果沒有自定義拷貝構造函數,系統會提供一個缺省的拷貝構造函數,缺省的拷貝構造函數對於基本類型的成員變量,按字節複製,對於類類型成員變量,調用其相應類型的拷貝構造函數。原型如下:
String(const String& s) {}
但是,編譯器提供的缺省函數並不是十全十美的。
缺省拷貝構造函數在拷貝過程中是按字節複製的,對於指針型成員變量只複製指針本身,而不復制指針所指向的目標--淺拷貝。
用圖形象化爲:
在進行對象複製後,事實上s1、s2裏的成員指針 _str 都指向了一塊內存空間(即內存空間共享了),在s1析構時,delete了成員指針 _str 所指向的內存空間,而s2析構時同樣指向(此時已變成野指針)並且要釋放這片已經被s1析構函數釋放的內存空間,這就讓同樣一片內存空間出現了 “double free” ,從而出錯。而淺拷貝還存在着一個問題,因爲一片空間被兩個不同的子對象共享了,只要其中的一個子對象改變了其中的值,那另一個對象的值也跟着改變。所以這不是我們想要的結果,同事也不是真正意義上的複製。
爲了解決淺拷貝問題,我們引出深拷貝,即自定義拷貝構造函數,如下:
String(const String& s) :_str(new char[strlen(s._str) + 1]) { strcpy(_str , s._str); }
這樣在運行就沒問題了。
那麼,程序中還有沒有其他地方用到拷貝構造函數呢?
答案:當函數存在對象型的參數(即拷貝構造)或對象型的返回值(賦值時的返回值)時都會用到拷貝構造函數。
而拷貝賦值的情況基本上與拷貝複製是一樣的。只是拷貝賦值是屬於操作符重載問題。例如在主函數若有:String s3; s3 = s2; 這樣系統在執行時會調用系統提供的缺省的拷貝賦值函數,原型如下:
void operator = (const String& s) {}
我們自定義的賦值函數如下:
void operator=(const String& s) { if(_str != s._str) { strcpy(_str,s._str); } return *this; }
但是這只是新手級別的寫法,考慮的問題太少。我們知道對於普通變量來講a=b返回的是左值a的引用,所以它可以作爲左值繼續接收其他值(a=b)=30,這樣來講我們操作符重載後返回的應該是類對象的引用(否則返回值將不能作爲左值來進行運算),如下:
String& operator=(const String& s) { if(_str != s._str) { strcpy(_str,s._str); } return *this; }
而上面這種寫法其實也有問題,因爲在執行語句時,_str 已經被構造已經分 配了內存空間,但是如此進行指針賦值,_str 直接轉而指向另一片新new出來的內存空間,而丟棄了原來的內存,這樣便造成了內存泄露。應更改爲:
String& operator=(const String& s) { if(_str != s._str) { delete[] _str; _str = new char[strlen(s._str) + 1]; strcpy(_str,s._str); } return *this; }
同時,也考慮到了自己給自己賦值的情況。
可是這樣寫就完善了嗎?是否要再仔細思索一下,還存在問題嗎?!其實我可以告訴你,這樣的寫法也頂多算個初級工程師的寫法。前面說過,爲了保證內存不泄露,我們前面 delete[] _str,然後我們在把new出來的空間給了_str,但是這樣的問題是,你有考慮過萬一 new 失敗了呢?!內存分配失敗,m_psz沒有指向新的內存空間,但是它卻已經把舊的空間給扔掉了,所以顯然這樣的寫法依舊存在着問題。一般高級工程師的寫法會是這樣的:
String& operator=(const String& s) { if(_str != s._str) { char *tmp = new char[strlen(s._str) + 1]; strcpy(tmp , s._str); delete[] _str; _str = tmp; } return *this; }
這樣寫就比較全面了。
但是!!!還有元老級別的大師寫的更加簡便的拷貝構造和賦值函數,我們一睹爲快:
<元老級拷貝構造函數>
String(const String& s) :_str(NULL) { String tmp = s._str; swap(_str , tmp._str); }
<元老級賦值函數>
// 1. String& operator=(const String& s) { if(_str != s._str) { String tmp = s._str; swap(_str , tmp._str); } return *this; } // 2. String& operator=(String& s)//在此會拷貝構造一個臨時的對象s { if(_str != s._str) { swap(_str ,s._str);// 交換this->_str和臨時生成的對象數據成員s._str,離開作用域會自動析構釋放 } return *this; }
看出端倪了麼?
事實上,這是藉助了以上自定義的拷貝構造函數。定義了局部對象 tmp,在拷貝構造中已經爲 tmp 的成員指針分配了一塊內存,所以只需要將 tmp._str 與this->_str交換指針即可,簡化了程序的設計,因爲 tmp 是局部對象,離開作用域會調用析構函數釋放交換給 tmp._str 的內存,避免了內存泄露。
這是非常值得我們學習和借鑑的。
這是本人對C++深淺拷貝的理解,若有紕漏,歡迎留言指正 ^_^
附註整體代碼:
#include <iostream> using namespace std; class String { public: String(char *str = "") :_str(new char[strlen(str) + 1]) { strcpy(_str , str); } // 淺拷貝 String(const String& s) :_str(s._str) {} //賦值運算符重載 //有問題,會造成內存泄露。。。 String& operator=(const String& s) { if(_str != s._str) { strcpy(_str,s._str); } return *this; } // 深拷貝 <傳統寫法> String(const String& s) :_str(new char[strlen(s._str) + 1]) { strcpy(_str , s._str); } //賦值運算符重載 //一. 這種寫法有問題,萬一new失敗了。。 String& operator=(const String& s) { if(_str != s._str) { delete[] _str; _str = new char[strlen(s._str) + 1]; strcpy(_str,s._str); } return *this; } //二. 對上面的方法改進,先new後delete,如果new失敗也不會影響到_str原來的內容 String& operator=(const String& s) { if(_str != s._str) { char *tmp = new char[strlen(s._str) + 1]; strcpy(tmp , s._str); delete[] _str; _str = tmp; } } // 深拷貝 <現代寫法> String(const String& s) :_str(NULL) { String tmp = s._str; swap(_str , tmp._str); } //賦值運算符的現代寫法一: String& operator=(const String& s) { if(_str != s._str) { String tmp = s._str; swap(_str , tmp._str); } return *this; } //賦值運算符的現代寫法二: String& operator=(String& s) //在此會拷貝構造一個臨時的對象s { if(_str != s._str) { swap(_str ,s._str);//交換this->_str和臨時生成的對象數據成員s._str,離開作用域會自動析構釋放 } return *this; } ~String() { if(_str) { delete[] _str; _str = NULL; } cout<<"~String()"<<endl; } void Display() { cout<<_str<<endl; } private: char *_str; }; void Test() { String s1; String s2("hello"); String s3(s2); String s4 = s3; s1.Display(); s2.Display(); s3.Display(); s4.Display(); } int main() { Test(); return 0; }