1、淺拷貝
淺拷貝:又稱值拷貝,将源對象的值拷貝到目标對象中去,本質上來說源對象和目标對象共用一份實體,隻是所引用的變量名不同,位址其實還是相同的。 舉個簡單的例子,你的小名叫西西,大名叫冬冬,當别人叫你西西或者冬冬的時候你都會答應,這兩個名字雖然不相同,但是都指的是你。
假設有一個String類,String s1;String s2(s1);在進行拷貝構造的時候将對象s1裡的值全部拷貝到對象s2裡。
我們現在來簡單的實作一下這個類:
#include <iostream>
#include<cstring>
using namespace std;
class STRING
{
public:
STRING( const char* s = "" ) :_str( new char[strlen(s)+1] )
{
strcpy_s( _str, strlen(s)+1, s );
}
STRING( const STRING& s )
{
_str = s._str;
}
STRING& operator=(const STRING& s)
{
if (this != &s)
{
this->_str = s._str;
}
return *this;
}
~STRING()
{
cout << "~STRING" << endl;
if (_str)
{
delete[] _str;
_str = NULL;
}
}
void show()
{
cout << _str << endl;
}
private:
char* _str;
};
int main()
{
STRING s1("hello linux");
STRING s2(s1);
s2.show();
return 0;
}
其實,這個程式是存在問題的,什麼問題呢?
我們想以下,建立s2的時候程式必然會去調用拷貝構造函數,這時候拷貝構造函數僅僅隻是完成了值拷貝,導緻兩個指針指向了同一塊區域。随着程式程式的運作結束,又去調用析構函數,先是s2去調用析構函數,釋放了它指向的記憶體區域,接着s1又去調用析構函數,這時候析構函數企圖釋放一塊已經被釋放的記憶體區域,程式将會崩潰。
s1和s2的關系就是這樣的:

進行調試時發現s1和s2确實指向了同一塊記憶體:
淺拷貝問題:
(1)如果類中叧包含簡單資料成員,沒有指向堆的指針, 可以使用編譯器提供的預設複制構造函數。
(2)如果類中包含指向堆中資料的指針,淺複制将出現 嚴重問題
- 淺複制直接複制兩個對象間的指針成員,導緻兩個指針 指向堆中同一坑記憶體區域。
- 一個對象的修改将導緻另一個對象的修改。
- 一個對象超出作用域,将導緻記憶體釋放,使得另一個對 象的指針無效,對其通路将導緻程式異常。
那麼這個問題應該怎麼去解決呢?這就引出了深拷貝。
2、深拷貝
2.1 深拷貝定義
深拷貝,拷貝的時候先開辟出和源對象大小一樣的空間,然後将源對象裡的内容拷貝到目标對象中去,這樣兩個指針就指向了不同的記憶體位置。 并且裡面的内容是一樣的,這樣不但達到了我們想要的目的,還不會出現問題,兩個指針先後去調用析構函數,分别釋放自己所指向的位置。即為每次增加一個指針,便申請一塊新的記憶體,并讓這個指針指向新的記憶體,深拷貝情況下,不會出現重複釋放同一塊記憶體的錯誤。
深拷貝實際上是這樣的:
2.2 深拷貝的拷貝構造函數和指派運算符的重載傳統實作
STRING( const STRING& s )
{
//_str = s._str;
_str = new char[strlen(s._str) + 1];
strcpy_s( _str, strlen(s._str) + 1, s._str );
}
STRING& operator=(const STRING& s)
{
if (this != &s)
{
//this->_str = s._str;
delete[] _str;
this->_str = new char[strlen(s._str) + 1];
strcpy_s(this->_str, strlen(s._str) + 1, s._str);
}
return *this;
}
這裡的拷貝構造函數我們很容易了解,先開辟出和源對象一樣大的記憶體區域,然後将需要拷貝的資料複制到目标拷貝對象,那麼這裡的指派運算符的重載是怎麼樣做的呢?
這種方法解決了我們的指針懸挂問題,通過不斷的開空間讓不同的指針指向不同的記憶體,以防止同一塊記憶體被釋放兩次的問題.
2.3 深拷貝的現代寫法
STRING( const STRING& s ):_str(NULL)
{
STRING tmp(s._str);// 調用了構造函數,完成了空間的開辟以及值的拷貝
swap(this->_str, tmp._str); //交換tmp和目标拷貝對象所指向的内容
}
STRING& operator=(const STRING& s)
{
if ( this != &s )//不讓自己給自己指派
{
STRING tmp(s._str);//調用構造函數完成空間的開辟以及指派工作
swap(this->_str, tmp._str);//交換tmp和目标拷貝對象所指向的内容
}
return *this;
}
我們先來分析拷貝構造是怎麼實作的:
拷貝構造調用完成之後,會接着去調用析構函數來銷毀局部對象tmp,按照這種思路,不難可以想到s2的值一定和拷貝構造裡的tmp的值一樣,指向同一塊記憶體區域,通過調試可以看出來:
在拷貝構造函數裡的tmp:
調用完拷貝構造後的s2:(此時tmp被析構)
可以看到s2的位址值和拷貝構造裡的tmp的位址值是一樣。
關于指派運算符的重載還可以這樣來寫:
STRING& operator=(STRING s)
{
swap(_str, s._str);
return *this;
}
#include <iostream>
#include<cstring>
using namespace std;
class STRING
{
public:
STRING( const char* s = "" ) :_str( new char[strlen(s)+1] )
{
strcpy_s( _str, strlen(s)+1, s );
}
//STRING( const STRING& s )
//{
// //_str = s._str; //淺拷貝的寫法
// cout << "拷貝構造函數" << endl;
// _str = new char[strlen(s._str) + 1];
// strcpy_s( _str, strlen(s._str) + 1, s._str );
//}
//STRING& operator=(const STRING& s)
//{
// cout << "運算符重載" << endl;
// if (this != &s)
// {
// //this->_str = s._str; //淺拷貝的寫法
// delete[] _str;
// this->_str = new char[strlen(s._str) + 1];
// strcpy_s(this->_str, strlen(s._str) + 1, s._str);
// }
// return *this;
//}
STRING( const STRING& s ):_str(NULL)
{
STRING tmp(s._str);// 調用了構造函數,完成了空間的開辟以及值的拷貝
swap(this->_str, tmp._str); //交換tmp和目标拷貝對象所指向的内容
}
STRING& operator=(const STRING& s)
{
if ( this != &s )//不讓自己給自己指派
{
STRING tmp(s._str);//調用構造函數完成空間的開辟以及指派工作
swap(this->_str, tmp._str);//交換tmp和目标拷貝對象所指向的内容
}
return *this;
}
~STRING()
{
cout << "~STRING" << endl;
if (_str)
{
delete[] _str;
_str = NULL;
}
}
void show()
{
cout << _str << endl;
}
private:
char* _str;
};
int main()
{
//STRING s1("hello linux");
//STRING s2(s1);
//STRING s2 = s1;
//s2.show();
const char* str = "hello linux!";
STRING s1(str);
STRING s2;
s2 = s1;
s1.show();
s2.show();
return 0;
}