淺拷貝和深拷貝(用string類分析)
阿新 • • 發佈:2018-12-02
區分什麼是初始化和賦值。
對於一個類來講,如果用一個已經存在的物件去構造一個新物件,這個過程就是初始化的過程。如果用一個已經存在的物件去給另一個已經存在的物件賦值,這就是賦值的過程。
在初始化和賦值的過程中,假設類涉及到堆記憶體,如果採用編譯器系統預設給定的拷貝建構函式和賦值運算子的過載函式進行物件之間的初始化過程和賦值過程,輕則發生淺拷貝,重則記憶體洩漏,這樣的程式都是有問題的。下面分析為什麼在使用預設的拷貝建構函式時會出現淺拷貝問題。
class Cstring{ private: char* _data; public: Cstring(const char* str){ if(str!=NULL){ _data = new char[strlen(str)+1]; strcpy(_data,str); } else{ //不處理為NULL的原因是,降低類中其他函式的邏輯複雜度,統一處理 //不需要判斷_data是否NULL,分開處理 _data = new char[1]; *_data = '\0'; } } ~Cstring(){ delete[] _data; } };
測試程式 :
int main(){
Cstring str("hello");
Cstring str1 = str;
return 0;
}
因為呼叫預設的拷貝建構函式,只是簡單的記憶體值拷貝,這樣會使得物件str1中的_data和物件str中的_data指向同一塊在堆上的記憶體空間,str1相對於str後構造,所以str1會先進行析構,釋放掉了這塊堆記憶體。使得str中的_data變為野指標,此str物件進行析構。將會釋放野指標指向heap上的一塊堆記憶體,這是非常不安全的。
解決方案:進行深拷貝,每個物件有自己的堆記憶體
Cstring::Cstring(const Cstring& src){ this->_data = new char[strlen(src._data)+1]; strcpy(_data,src.data); }
如上圖,這樣就實現了在拷貝建構函式中深拷貝。
下面再討論一下使用預設賦值運算子過載的拷貝建構函式在使用時出現的記憶體洩漏以及淺拷貝問題。
測試程式:
int main(){
Cstring str("hello");
Cstrint str1("world");
str = str1;//可以理解為str.=(str1)這樣的呼叫方式
}
編譯器系統預設的賦值運算子的過載函式是簡單的賦值。
解決方案:自己實現賦值運算子過載函式,兩個目的:1.避免出現記憶體洩漏的問題。2.進行深拷貝
Cstring& Cstring(const Cstring& src){ //防止發生在自賦值 if(this == &src){ return *this; } delete[] this->_data;//釋放當前物件在堆上的空間,避免出現記憶體洩漏 _data = new char[strlen(src._data)+1];//申請空間,避免出現淺拷貝 strcpy(_data,src._data);//進行記憶體拷貝 return *this; } //經過上述的處理,記憶體洩漏和淺拷貝的問題都得以解決
總結:在實現類的建構函式中,往往會涉及到堆記憶體的開闢。如果對拷貝建構函式和賦值運算子的過載函式不進行重寫,會發生淺拷貝以及記憶體洩漏等問題導致程式出錯。