關於淺拷貝和深拷貝這個問題遇上的次數不多,這次遇上整理一下,先說這樣一個問題,關於淺拷貝的問題,先從最簡單的說起。
假設存在一個結構體:
struct Student { string name; int age; }; int main() { struct Student stu = {"liming", 18}; struct Student stu2 = {"wanger", 20}; stu2 = stu; cout<<"age is : "<< stu2.age <<endl; cout<<"name is :"<< stu2.name<<endl; }
這樣可以看到的結果是:
age is : 18
name is :liming
說明此時的拷貝是成功的,此時的結構體可以通過“=”來直接進行賦值操作,但是接下來的問題產生了,假設存在如下的結構體:
struct stu { int i; char c; char* p; }; int main() { struct stu s1,s2; char * str = "rabbit is cute"; s1.i = 345; s1.c = 'y'; s1.p = (char*)str; s2 = s1; printf("s2 %d, %c, %s\n", s2.i, s2.c, s1.p); printf("s1 ptr: %d, s2 ptr : %d\n", s1.p, s2.p); }
產生的結果是這樣的:
s2 345, y, rabbit is cute
s1 ptr: 7934, s2 ptr : 7934
可以看到的是S2 確實得到了S1 傳遞的值,但是第二句的話卻說明這樣的一個問題,其實S2和S1的指針p都指向一個內存地址,這又說明了什么?
這說明指針的並沒有將內容復制一塊給新指針來指向,只是讓新指針指向原來的那個內存,這樣就相當於,指針在這個復制的過程中只是復制了地址,而不是內容。
原理:
在拷貝過程中,如果沒有自定義拷貝構造函數,系統會提供一個缺省的拷貝構造函數,缺省的拷貝構造函數對於基本類型的成員變量,按字節復制,對於類類型成員變量,調用其相應類型的拷貝構造函數。但是注意缺省的構造函數卻是這樣的:缺省拷貝構造函數在拷貝過程中是按字節復制的,對於指針型成員變量只復制指針本身,而不復制指針所指向的目標--淺拷貝。
這就是產生問題的原因了,淺拷貝出現了。。。
用下圖來解釋這個問題:

在進行對象復制后,事實上s1、s2里的成員指針p都指向了一塊內存空間(即內存空間共享了),在s1析構時,delete了成員指針p所指向的內存空間,而s2析構時同樣指向(此時已變成野指針)並且要釋放這片已經被s1析構函數釋放的內存空間,這就讓同樣一片內存空間出現了“double free” ,從而出錯。而淺拷貝還存在着一個問題,因為一片空間被兩個不同的子對象共享了,只要其中的一個子對象改變了其中的值,那另一個對象的值也跟着改變了。
為了實現深拷貝,往往需要自己定義拷貝構造函數,在源代碼里,我們加入自定義的拷貝構造函數如下:
在結構體中加入自己的拷貝構造函數:
struct stu { int i; char c; char* p; stu operator=(stu& stuTmp) { i = stuTmp.i; c = stuTmp.c; p = new char(strlen(stuTmp.p) + 1); strcpy(p, stuTmp.p); return *this; }; }; int main() { struct stu s1,s2; char * str = "rabbit is cute"; s1.i = 345; s1.c = 'y'; s1.p = (char*)str; s2 = s1; printf("s2 %d, %c, %s\n", s2.i, s2.c, s1.p); printf("s1 ptr: %d, s2 ptr : %d\n", s1.p, s2.p); }
測試demo
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <string> using namespace std; class stu { public: int i; char c; char* p; stu operator=(stu& stuTmp) { this->i = stuTmp.i; this->c = stuTmp.c; this->p = new char(strlen(stuTmp.p) + 1); for (int i = 0; i < strlen(stuTmp.p); i++) { this->p[i] = stuTmp.p[i]; } //strcpy(pp, stuTmp.p); return *this; }; }; int main() { struct stu s1, s2; char * str = "rabbit is cute"; s1.i = 345; s1.c = 'y'; s1.p = (char*)str; s2 = s1; printf("s2 %d, %c, %s\n", s2.i, s2.c, s1.p); printf("s1 ptr: %d, s2 ptr : %d\n", s1.p, s2.p); cin.get(); }
相當於重載operator=方法,這樣還是運行,產生的結果就是這樣的:
s2 345, y, rabbit is cute
s1 ptr: 7910, s2 ptr : 1050000
此時s1和s2中的指針p指向了不同的地址,可以打印一下此時這兩個指針的內容是否一樣,加入一下代碼:
printf("s1 ptr: %s, s2 ptr : %s\n ", s1.p, s2.p);
產生的結果是:s1 ptr: rabbit is cute, s2 ptr : rabbit is cute
此時s1和s2中的p指針地址不同,但是指向的內容一致,所以這拷貝成功。
其實類的結構和上面的結構體是類似的,其實可以將結構體看成一個類來處理,結構體也可有自己的構造、析構、重載運算符河函數,可以簡單的認為結構體是類的一種形式。
拷貝有兩種:深拷貝,淺拷貝
當出現類的等號賦值時,會調用拷貝函數 在未定義顯示拷貝構造函數的情況下,系統會調用默認的拷貝函數——即淺拷貝,它能夠完成成員的一一復制。當數據成員中沒有指針時,淺拷貝是可行的。 但當數據成員中有指針時,如果采用簡單的淺拷貝,則兩類中的兩個指針將指向同一個地址,當對象快結束時,會調用兩次析構函數,而導致指針懸掛現象。 所以,這時,必須采用深拷貝。 深拷貝與淺拷貝的區別就在於深拷貝會在堆內存中另外申請空間來儲存數據,從而也就解決了指針懸掛的問題。 簡而言之,當數據成員中有指針時,必須要用深拷貝。
建議:
我們在定義類或者結構體,這些結構的時候,最后都重寫拷貝構造函數,避免淺拷貝這類不易發現但后果嚴重的錯誤產生。