C++ 中的深拷貝與淺拷貝


  淺拷貝:又稱值拷貝,將源對象的值拷貝到目標對象中去,本質上來說源對象和目標對象共用一份實體,只是所引用的變量名不同,地址其實還是相同的。舉個簡單的例子,你的小名叫西西,大名叫冬冬,當別人叫你西西或者冬冬的時候你都會答應,這兩個名字雖然不相同,但是都指的是你。

  假設有一個String類,String s1;String s2(s1);在進行拷貝構造的時候將對象s1里的值全部拷貝到對象s2里。

  我們現在來簡單的實現一下這個類

 1 #include <iostream>
 2 #include<cstring>
 3 
 4 using namespace std;
 5 
 6 class STRING 
 7 {
 8 public:
 9     STRING( const char* s = "" ) :_str( new char[strlen(s)+1] )
10 
11     {
12         strcpy_s( _str, strlen(s)+1, s );
13     }
14     STRING( const STRING& s )
15     {
16         _str = s._str;
17     }
18     STRING& operator=(const STRING& s)
19     {
20         if (this != &s)
21         {
22             this->_str = s._str;
23         }
24         return *this;
25     }
26 
27     ~STRING()
28     {
29         cout << "~STRING" << endl;
30         if (_str)
31         {
32             delete[] _str;
33             _str = NULL;
34         }
35     }
36 
37     void show()
38     {
39         cout << _str << endl;
40     }
41 private:
42     char* _str;
43 };
44 
45 int main()
46 {
47     STRING s1("hello linux");
48     STRING s2(s1);
49     s2.show();
50 
51     return 0;
52 }

其實這個程序是存在問題的,什么問題呢?我們想一下,創建s2的時候程序必然會去調用拷貝構造函數,這時候拷貝構造僅僅只是完成了值拷貝,導致兩個指針指向了同一塊內存區域。隨着程序的運行結束,又去調用析構函數,先是s2去調用析構函數,釋放了它指向的內存區域,接着s1又去調用析構函數,這時候析構函數企圖釋放一塊已經被釋放的內存區域,程序將會崩潰。s1和s2的關系就是這樣的:

進行調試程序發現s1和s2確實指向了同一塊區域:

所以程序會崩潰是應該的,那么這個問題應該怎么去解決呢?這就引出了深拷貝。

深拷貝,拷貝的時候先開辟出和源對象大小一樣的空間,然后將源對象里的內容拷貝到目標對象中去,這樣兩個指針就指向了不同的內存位置。並且里面的內容是一樣的,這樣不但達到了我們想要的目的,還不會出現問題,兩個指針先后去調用析構函數,分別釋放自己所指向的位置。即為每次增加一個指針,便申請一塊新的內存,並讓這個指針指向新的內存,深拷貝情況下,不會出現重復釋放同一塊內存的錯誤。

深拷貝實際上是這樣的:

深拷貝的拷貝構造函數和賦值運算符的重載傳統實現:

 1 STRING( const STRING& s )
 2     {
 3         //_str = s._str;
 4         _str = new char[strlen(s._str) + 1];
 5         strcpy_s( _str, strlen(s._str) + 1, s._str );
 6     }
 7     STRING& operator=(const STRING& s)
 8     {
 9         if (this != &s)
10         {
11             //this->_str = s._str;
12             delete[] _str;
13             this->_str = new char[strlen(s._str) + 1];
14             strcpy_s(this->_str, strlen(s._str) + 1, s._str);
15         }
16         return *this;
17     }

這里的拷貝構造函數我們很容易理解,先開辟出和源對象一樣大的內存區域,然后將需要拷貝的數據復制到目標拷貝對象,

那么這里的賦值運算符的重載是怎么樣做的呢?

這種方法解決了我們的指針懸掛問題,通過不斷的開空間讓不同的指針指向不同的內存,以防止同一塊內存被釋放兩次的問題,還有一種深拷貝的現代寫法:

 1 STRING( const STRING& s ):_str(NULL)
 2     {
 3         STRING tmp(s._str);// 調用了構造函數,完成了空間的開辟以及值的拷貝
 4         swap(this->_str, tmp._str); //交換tmp和目標拷貝對象所指向的內容
 5     }
 6 
 7     STRING& operator=(const STRING& s)
 8     {
 9         if ( this != &s )//不讓自己給自己賦值
10         {
11             STRING tmp(s._str);//調用構造函數完成空間的開辟以及賦值工作
12             swap(this->_str, tmp._str);//交換tmp和目標拷貝對象所指向的內容
13         }
14         return *this;
15     }

先來分析一下拷貝構造是怎么實現的:

拷貝構造調用完成之后,會接着去調用析構函數來銷毀局部對象tmp,按照這種思路,不難可以想到s2的值一定和拷貝構造里的tmp的值一樣,指向同一塊內存區域,通過調試可以看出來:

在拷貝構造函數里的tmp:

調用完拷貝構造后的s2:(此時tmp被析構)

可以看到s2的地址值和拷貝構造里的tmp的地址值是一樣

關於賦值運算符的重載還可以這樣來寫:

STRING& operator=(STRING s)
{
  swap(_str, s._str);
  return *this;
}

 1 #include <iostream>
 2 #include<cstring>
 3 
 4 using namespace std;
 5 
 6 class STRING 
 7 {
 8 public:
 9     STRING( const char* s = "" ) :_str( new char[strlen(s)+1] )
10 
11     {
12         strcpy_s( _str, strlen(s)+1, s );
13     }
14     //STRING( const STRING& s )
15     //{
16     //    //_str = s._str; //淺拷貝的寫法
17     //    cout << "拷貝構造函數" << endl;
18     //    _str = new char[strlen(s._str) + 1];
19     //    strcpy_s( _str, strlen(s._str) + 1, s._str );
20     //}
21     //STRING& operator=(const STRING& s)
22     //{
23     //    cout << "運算符重載" << endl;
24     //    if (this != &s)
25     //    {
26     //        //this->_str = s._str; //淺拷貝的寫法
27     //        delete[] _str;
28     //        this->_str = new char[strlen(s._str) + 1];
29     //        strcpy_s(this->_str, strlen(s._str) + 1, s._str);
30     //    }
31     //    return *this;
32     //}
33 
34     STRING( const STRING& s ):_str(NULL)
35     {
36         STRING tmp(s._str);// 調用了構造函數,完成了空間的開辟以及值的拷貝
37         swap(this->_str, tmp._str); //交換tmp和目標拷貝對象所指向的內容
38     }
39 
40     STRING& operator=(const STRING& s)
41     {
42         if ( this != &s )//不讓自己給自己賦值
43         {
44             STRING tmp(s._str);//調用構造函數完成空間的開辟以及賦值工作
45             swap(this->_str, tmp._str);//交換tmp和目標拷貝對象所指向的內容
46         }
47         return *this;
48     }
49 
50     ~STRING()
51     {
52         cout << "~STRING" << endl;
53         if (_str)
54         {
55             delete[] _str;
56             _str = NULL;
57         }
58     }
59 
60     void show()
61     {
62         cout << _str << endl;
63     }
64 private:
65     char* _str;
66 };
67 
68 int main()
69 {
70     //STRING s1("hello linux");
71     //STRING s2(s1);
72     //STRING s2 = s1;
73     //s2.show();
74     const char* str = "hello linux!";
75     STRING  s1(str);
76     STRING s2;
77     s2 = s1;
78     s1.show();
79     s2.show();
80 
81     return 0;
82 }

參考與

淺析C++中的深淺拷貝 - qq_39344902的博客 - CSDN博客
https://blog.csdn.net/qq_39344902/article/details/79798297


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM