C++中深拷貝和淺拷貝的問題是很值得咱們注意的知識點,若是編程中不注意,可能會出現疏忽,致使bug。本文就詳細講講C++深淺拷貝的種種。ios
咱們知道,對於通常對象:編程
int a = 1; int b = 2;
這樣的賦值,複製很簡單,但對於類對象來講並不通常,由於其內部包含各類類型的成員變量,在拷貝過程當中就會出現問題ide
例如:函數
#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 ,這就是淺拷貝帶來的問題。this
事實是,在對象拷貝過程當中,若是沒有自定義拷貝構造函數,系統會提供一個缺省的拷貝構造函數,缺省的拷貝構造函數對於基本類型的成員變量,按字節複製,對於類類型成員變量,調用其相應類型的拷貝構造函數。原型以下:spa
String(const String& s) {}
可是,編譯器提供的缺省函數並非十全十美的。設計
缺省拷貝構造函數在拷貝過程當中是按字節複製的,對於指針型成員變量只複製指針自己,而不復制指針所指向的目標--淺拷貝。指針
用圖形象化爲:對象
在進行對象複製後,事實上s一、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; }