C 深複製和淺複製
物件的複製
對於普通型別的物件來說,它們之間的複製是很簡單的,例如:
int a=88;int b=a;double f=3.12;double d(f);
而類物件與普通物件不同,類物件內部結構一般較為複雜,存在各種資料成員。下面看一個類物件複製的簡單例子。
#include <iostream>using namespace std;class Test{private: int a,b;public: Test(int x, int y) //提供的形式引數,是為了給資料成員直接初始化的 { a=x; b=y; } Test(const Test& C) //複製建構函式,提供一個同類型物件作為引數 { a=C.a; b=C.b; } void show () { cout<<a<<" "<<b<<endl; }}; int main(){ Test a(100,10); //執行建構函式Test::Test(int x, int y) Test b(a); //執行建構函式Test::Test(const Test& C) Test c=a; //也執行建構函式Test::Test(const Test& C) b.show(); c.show(); return 0;}
執行程式,螢幕輸出兩行100 10。
從以上程式碼的執行結果可以看出,系統在宣告物件b和c時,完成了由物件a的複製。
複製建構函式
就類物件而言,相同型別的類物件是通過複製建構函式來完成整個複製過程的。
上例中的Test::Test(const Test& C),就是我們自定義的複製建構函式。
可見,複製建構函式是一種特殊的建構函式,函式的名稱必須和類名稱一致,它的唯一的一個引數是本型別的一個引用變數,該引數是const型別,用來約束作為引數的物件在構造新物件中是不能被改變的。
略一歸納:類X的複製建構函式的形式為
當用一個已初始化過了的自定義類型別物件去初始化另一個新構造的物件的時候,複製建構函式就會被自動呼叫。也就是說,當類的物件需要複製時,複製建構函式將會被呼叫。以下情況都會呼叫複製建構函式:
- 一個物件以值傳遞的方式傳入函式體
- 一個物件以值傳遞的方式從函式返回
- 一個物件需要通過另外一個物件進行初始化。
如果在類中沒有顯式地宣告一個複製建構函式,那麼,編譯器將會自動生成一個預設的複製建構函式,該建構函式完成物件之間的淺複製,後面將進行說明。
自定義複製建構函式是一種良好的程式設計風格,它可以阻止編譯器形成預設的複製建構函式,提高原始碼效率。
淺複製和深複製
所謂淺複製,如同上面出現過的建構函式中處理的一樣,直接為資料成員賦值即可。在很多情況下,這是可以的。建立新的物件,要為物件的資料成員分配儲存空間,直接賦值就將值儲存在相應的空間中。
然而,這種淺複製,卻並不能通行天下,下面的程式中,淺複製帶來了問題。
#include <iostream>#include <cstring>using namespace std;class Test{private: int a; char *str;public: Test(int b, char *s) { a=b; strcpy(str,s); //肇事地點,但不是禍端 } Test(const Test& C) { a=C.a; strcpy(str,C.str); } void show () { cout<<a<<","<<str<<endl; }}; int main(){ Test a(100,"hello"); Test b(a); a.show(); b.show(); return 0;}
程式執行中,會彈出一個視窗:程式的執行意外停止了。面對這個視窗,我們應該有感覺,這和使用指標不當有關係。
我們從main函式看起。
當程式執行到第28行Test a(100,"hello");時,物件a的資料成員a獲得實際引數100的值,而資料成員str,即指標,是個隨機地址值(指標的值,非指標指向的值)!在程式中,試圖通過strcpy(str,s);將形式引數s指向的字串"hello",複製到str所指向的那個位置,而那個位置,其地址並不是經過系統分配來的,這是個危險的操作。在這裡,str這樣未經過分配的地址(指標),被稱為“野指標”。
在執行第29行Test b(a);時,同樣的事情還要發生。
str這樣的野指標是多麼的霸道!在有的系統裡,這樣的行為是被接受的,可以想到其有多危險。有些系統中,不允許這樣的事情發生的。於是,上面的程式在codeblock中除錯會出錯。這個錯來的好。
解決這樣的問題的方法,就是在建構函式中,要為指標型別的成員,分配專門的空間。以這條規則構建的複製,稱作為深複製!
上面的程式,改寫為:
#include <iostream>#include <cstring>using namespace std;class Test{private: int a; char *str;public: Test(int b, char *s) { a=b; str=new char[strlen(s)+1]; //分配str指向的空間,其長度根據s指向的字串定。為何加1?字串結束要用\0 strcpy(str,s); //前一程式的肇事地點,禍端已經由上一句摘除 } Test(const Test& C) { a=C.a; str=new char[strlen(C.str)+1]; //同上,這樣str就不是野指標了 strcpy(str,C.str); } ~Test() { delete []str; } void show () { cout<<a<<","<<str<<endl; }};int main(){ Test a(100,"hello"); Test b(a); a.show(); b.show(); return 0;}
好了,a和b物件的str成員,明確地給分配了空間,他們再不是野指標了。因為明確地分配了空間,解構函式中要釋放對應的空間。我們不能用野指標,當然,也不能物件要撤銷了,還佔著空間不放,做事不能這樣不厚道。深複製就體現在第13和第19行分配指標指向的空間,這段空間的地址,也將是指標的值(分清指標的值和指標指向的值)。
再一個深複製的例子
下面再給一個例子,類A的資料成員可以儲存len個整型資料。類中的資料成員arrayAddr是指向整型的指標,可以作為一個一元陣列的起始地址。這個類有指標資料成員,建構函式的定義中,必須要採用深複製的方法,第16行體現了這一點。另外,解構函式中完成了對分配的空間的釋放
#include<iostream>using namespace std;class A{private: int *arrayAddr;//儲存一個有len個整型元素的陣列的首地址 int len; //記錄動態陣列的長度public: A(int *a, int n); ~A(); int sum();};A::A(int *a, int n){ len=n; arrayAddr=new int[n]; //為指標資料成員分配空間,注意,沒有上面例子中加1那回事 for(int i=0; i<n; i++) //逐個地將a指向的值逐個地複製過來 { arrayAddr[i]=a[i]; }}//解構函式的類外定義,釋放指標型資料a所指向的空間A::~A(){ delete [] arrayAddr;}int A::sum() //獲得a指向的陣列中下標為i的元素的值{ int s=0; for(int i=0; i<len; i++) //逐個地將a指向的值逐個地複製過來 { s+=arrayAddr[i]; } return s;}int main(){ int b[10]= {75, 99, 90, 93, 38, 15, 5, 7, 52, 4}; A r1(b,10); cout<<"和:"<<r1.sum()<<endl; int c[15] = {18,68,10,52,3,19,12,100,56,96,95,97,1,4,93}; A r2(c,15); cout<<"和:"<<r2.sum()<<endl; return 0;}
==================== 迂者 賀利堅 CSDN部落格專欄=================|== IT學子成長指導專欄 專欄文章的分類目錄(不定期更新) ==||== C++ 課堂線上專欄 賀利堅課程教學連結(分課程年級) ==||== 我寫的書——《逆襲大學——傳給IT學子的正能量》 ==|===== 為IT菜鳥起飛鋪跑道,和學生一起享受快樂和激情的大學 ===== |