C++的淺拷貝和深拷貝
我列舉一個例子來說吧:
你正在編寫C++程式中有時後用到,操作符的過載。最能體現深層拷貝與淺層拷貝的,就是‘=’的過載。
看下面一個簡單的程式:
class string
{
char *m_str;
public:
string(char *s)
{
m_str=s;
}
string(){};
string&operator=(const string s)
{
m_str=s.m_str;
return *this}
};
int main()
{
string s1("abc"),s2;
s2=s1;
cout<<s2.m_str;
}
上面的 =過載其是就是實現了淺拷貝原因。是由於物件之中含有指標資料型別.s1,s2恰好指向同一各記憶體。所以是淺拷貝。而你如果修改一下原來的程式:
string&operator(const string&s)
{
if(strlen(m_str)!=strlen(s.m_str))
m_str=new char[strlen(s.m_str)+1];
if(*this!=s)
strcmp(m_str,s.m_str);
return *this;
}
這樣你就實現了深拷貝,原因是你為被賦值物件申請了一個新的記憶體所以就是深拷貝。
淺拷貝就是物件的資料成員之間的簡單賦值,如你設計了一個沒有類而沒有提供它的複製建構函式,當用該類的一個物件去給令一個物件賦值時所執行的過程就是淺拷貝,如:
class A
{
public:
A(int _data) : data(_data){}
A(){}
private:
int data;
};
int main()
{
A a(5), b = a; // 僅僅是資料成員之間的賦值
}
這一句b = a;就是淺拷貝,執行完這句後b.data = 5;
如果物件中沒有其他的資源(如:堆,檔案,系統資源等),則深拷貝和淺拷貝沒有什麼區別,但當物件中有這些資源時,例子:
class A
{
public:
A(int _size) : size(_size){data = new int[size];} // 假如其中有一段動態分配的記憶體
A(){};
~A(){delete [] data;} // 析構時釋放資源
private:
int* data;
int size;
}
int main()
{
A a(5), b = a; // 注意這一句
}
這裡的b = a會造成未定義行為,因為類A中的複製建構函式是編譯器生成的,所以b = a執行的是一個淺拷貝過程。我說過淺拷貝是物件資料之間的簡單賦值,比如:
b.size = a.size;
b.data = a.data; // Oops!
這裡b的指標data和a的指標指向了堆上的同一塊記憶體,a和b析構時,b先把其data指向的動態分配的記憶體釋放了一次,而後a析構時又將這塊已經被釋放過的記憶體再釋放一次。
對同一塊動態記憶體執行2次以上釋放的結果是未定義的,所以這將導致記憶體洩露或程式崩潰。
所以這裡就需要深拷貝來解決這個問題,深拷貝指的就是當拷貝物件中有對其他資源(如堆、檔案、系統等)的引用時(引用可以是指標或引用)時,物件的另開闢一塊新的資源,而不再對拷貝物件中有對其他資源的引用的指標或引用進行單純的賦值。如:
class A
{
public:
A(int _size) : size(_size){data = new int[size];} // 假如其中有一段動態分配的記憶體
A(){};
A(const A& _A) : size(_A.size){data = new int[size];} // 深拷貝
~A(){delete [] data;} // 析構時釋放資源
private:
int* data;
int size;
}
int main()
{
A a(5), b = a; // 這次就沒問題了
}
總結:
深拷貝和淺拷貝的區別是在物件狀態中包含其它物件的引用的時候,當拷貝一個物件時,如果需要拷貝這個物件引用的物件,則是深拷貝,否則是淺拷貝。
進一步探討 拷貝建構函式和過載"="賦值操作符。
重點:包含動態分配成員的類 應提供拷貝建構函式,並重載"="賦值操作符。
以下討論中將用到的例子:
class CExample { public: CExample(){pBuffer=NULL; nSize=0;} ~CExample(){delete pBuffer;} void Init(int n){ pBuffer=new char[n]; nSize=n;} private: char *pBuffer; //類的物件中包含指標,指向動態分配的記憶體資源 int nSize; };
這個類的主要特點是包含指向其他資源的指標。
pBuffer指向堆中分配的一段記憶體空間。
一、拷貝建構函式
int main(int argc, char* argv[]) { CExample theObjone; theObjone.Init40); //現在需要另一個物件,需要將他初始化稱物件一的狀態 CExample theObjtwo=theObjone; ... }
語句"CExample theObjtwo=theObjone;"用theObjone初始化theObjtwo。
其完成方式是記憶體拷貝,複製所有成員的值。
完成後,theObjtwo.pBuffer==theObjone.pBuffer。
即它們將指向同樣的地方,指標雖然複製了,但所指向的空間並沒有複製,而是由兩個物件共用了。這樣不符合要求,物件之間不獨立了,併為空間的刪除帶來隱患。
所以需要採用必要的手段來避免此類情況。
回顧以下此語句的具體過程:首先建立物件theObjtwo,並呼叫其建構函式,然後成員被拷貝。
可以在建構函式中新增操作來解決指標成員的問題。
所以C++語法中除了提供預設形式的建構函式外,還規範了另一種特殊的建構函式:拷貝建構函式,上面的語句中,如果類中定義了拷貝建構函式,這物件建立時,呼叫的將是拷貝建構函式,在拷貝建構函式中,可以根據傳入的變數,複製指標所指向的資源。
拷貝建構函式的格式為:建構函式名(物件的引用)
提供了拷貝建構函式後的CExample類定義為:
class CExample { public: CExample(){pBuffer=NULL; nSize=0;} ~CExample(){delete pBuffer;} CExample(const CExample&); //拷貝建構函式 void Init(int n){ pBuffer=new char[n]; nSize=n;} private: char *pBuffer; //類的物件中包含指標,指向動態分配的記憶體資源 int nSize; }; CExample::CExample(const CExample& RightSides) //拷貝建構函式的定義 { nSize=RightSides.nSize; //複製常規成員 pBuffer=new char[nSize]; //複製指標指向的內容 memcpy(pBuffer,RightSides.pBuffer,nSize*sizeof(char)); }
這樣,定義新物件,並用已有物件初始化新物件時,CExample(const CExample& RightSides)將被呼叫,而已有物件用別名RightSides傳給建構函式,以用來作複製。
原則上,應該為所有包含動態分配成員的類都提供拷貝建構函式。
拷貝建構函式的另一種呼叫。
當物件直接作為引數傳給函式時,函式將建立物件的臨時拷貝,這個拷貝過程也將調同拷貝建構函式。
例如
BOOL testfunc(CExample obj); testfunc(theObjone); //物件直接作為引數。 BOOL testfunc(CExample obj) { //針對obj的操作實際上是針對複製後的臨時拷貝進行的 }
還有一種情況,也是與臨時物件有關的
當函式中的區域性物件被被返回給函式調者時,也將建立此區域性物件的一個臨時拷貝,拷貝建構函式也將被呼叫
CTest func() { CTest theTest; return theTest }
二、賦值符的過載
下面的程式碼與上例相似
int main(int argc, char* argv[]) { CExample theObjone; theObjone.Init(40); CExample theObjthree; theObjthree.Init(60); //現在需要一個物件賦值操作,被賦值物件的原內容被清除,並用右邊物件的內容填充。 theObjthree=theObjone; return 0; }
也用到了"="號,但與"一、"中的例子並不同,"一、"的例子中,"="在物件宣告語句中,表示初始化。更多時候,這種初始化也可用括號表示。
例如 CExample theObjone(theObjtwo);
而本例子中,"="表示賦值操作。將物件theObjone的內容複製到物件theObjthree;,這其中涉及到物件theObjthree原有內容的丟棄,新內容的複製。
但"="的預設操作只是將成員變數的值相應複製。舊的值被自然丟棄。
由於物件內包含指標,將造成不良後果:指標的值被丟棄了,但指標指向的內容並未釋放。指標的值被複制了,但指標所指內容並未複製。
因此,包含動態分配成員的類除提供拷貝建構函式外,還應該考慮過載"="賦值操作符號。
類定義變為:
class CExample { ... CExample(const CExample&); //拷貝建構函式 CExample& operator = (const CExample&); //賦值符過載 ... };
//賦值操作符過載 CExample & CExample::operator = (const CExample& RightSides) { nSize=RightSides.nSize; //複製常規成員 char *temp=new char[nSize]; //複製指標指向的內容 memcpy(temp,RightSides.pBuffer,nSize*sizeof(char)); delete []pBuffer; //刪除原指標指向內容 (將刪除操作放在後面,避免X=X特殊情況下,內容的丟失) pBuffer=temp; //建立新指向 return *this }
三、拷貝建構函式使用賦值運算子過載的程式碼。
CExample::CExample(const CExample& RightSides) { pBuffer=NULL; *this=RightSides //呼叫過載後的"=" }