String 類的實現(1)淺拷貝存在的問題以及深拷貝實現


1.   淺拷貝 : 也稱位拷貝 , 編譯器只是直接將指針的值拷貝過來, 結果多個對象共用 同 一塊內存, 當一個對象將這塊內 存釋放掉之后, 另 一些對象不知道該塊空間已經還給了系統, 以為還有效, 所以在對這段內存進行操作的時候, 發生了違規訪問。

先上代碼

 1 class String  2 {  3 public:  4     /* 淺拷貝---下列代碼相當於系統合成的  5  String()  6  {  7  _pStr = new char;  8  *_pStr = '\0';  9  }*/
11     String(const char *pStr = "") 12  { 13         if (pStr == NULL) 14  { 15             _pStr = new char[1]; 16             *_pStr = '\0'; 17  } 18         else
19  { 20             _pStr = new char[strlen(pStr) + 1]; 21  strcpy(_pStr, pStr); 22  } 23  } 24     String(const String& s) 25  :_pStr(s._pStr) 26  {}
28     ~String() 29  { 30         if (_pStr) 31             delete[] _pStr; 32         _pStr = NULL; 33  }
35     String& operator=(const String& s) 36  { 37         if (this != &s) 38  { 39             _pStr = s._pStr; 40  } 41         return *this; 42  }
44 private: 45     char *_pStr;
47 };/*存在問題:1.同一對象析構多次,程序崩潰;2.內存泄漏*/

  int main()
  {
  String s1;
  String s2 = "hello word";
  String s3(s2);
  s1[0] = '5';
  String s4;
  s4 = s2;
  }

編譯時可以輕松通過,但是這段代碼是有問題的,運行時程序崩潰

這是String類的一個經典反例,下面來具體分析一下這段代碼存在的問題:

   當類里面有指針對象時, 拷貝構造和賦值運算符重載只進行值拷貝(淺拷貝) , 兩個對象指向同一塊內 存, 對象銷毀時該空間被釋放了 兩次, 因此程序崩潰!下面的 s1[0] = '5';  String s4;  s3 = s4; 也存在類似問題

  

存在問題:1.同一對象析構多次,程序崩潰;2.內存泄漏

   那么該如何解決出現的問題吶?這需要我們自己重新定義相關類的成員函數,下面將介紹多種深拷貝方法以解決此問題

  我們已經知道了淺拷貝存在的問題,即多次析構同一空間。這個問題是類的成員函數引起的,就是前面淺拷貝里模擬實現的編譯器自動合成的函數,確切的說,淺拷貝里的問題是由隱式拷貝構造函數和隱士賦值運算符引起的。

    拷貝構造函數用於將一個對象拷貝到新創建的對象中。也就是說,他用於初始化過程中,最常見的是將新對象顯式地初始化為現有的對象。每當程序生成了副本對象時,編譯器也將使用拷貝構造函數。默認的拷貝構造函數逐個的拷貝非靜態成員(即淺拷貝),拷貝的是成員的值。(由於按值傳遞對象將調用拷貝構造函數,因此應該按引用傳遞對象。這樣可以節省調用構造函數的時間以及存儲新對象的空間。)

    默認的賦值運算符是通過自動為類重載賦值運算符實現的。它的原型是:Class_name & Class_name ::operator=(const Class_name &);它接受並返回一個指向類對象的引用。將已有的對象賦給另一個對象時,將使用重載的賦值運算符(初始化對象時不一定會使用)。如:String s1=s2;也可能分兩步來處理這條語句:使用拷貝構造函數創建一個臨時對像,然后通過賦值將臨時對象的值復制到新對象中。即初始化總會調用拷貝構造函數,而使用“=“時也可能調用賦值運算符。

2.解決方法:深度復制(deep copy)。定義一個顯式拷貝構造函數,拷貝字符串並將副本的地址賦給str成員,而不僅僅是拷貝字符串地址。這樣每個對象都有自己的字符串,而不是引用另一個對象的字付串。則調用析構函數都將釋放不同的字符串而不是去釋放已經釋放的字符串。代碼如下:

1 String(const String& s) 2 :_pStr(new char[strlen(s._pStr)] + 1) 3  { 4 if (this != &s) 5  { 6  strcpy(_pStr, s._pStr); 7  } 8 }

   必須定義拷貝構造函數的原因在於,一些類成員是使用new初始化的、指向數據的指針,而不是數據本身。

總結:如果類中包含了使用new初始化的指針成員,應當定義一個拷貝構造函數,以拷貝指向的數據,而不是指針;淺拷貝僅淺淺的拷貝指針信息,而不會深入”挖掘“以拷貝指針引用的結構。

   默認賦值運算符存在的問題與默認拷貝構造函數相同:數據受損。試圖刪除已經刪除的數據導致的結果是不正確的,因此可能改變內存中的內容,導致程序異常終止。解決方法是提供深度拷貝的賦值運算符定義。

 1 String& operator=(const String& s)  2  {  3 if (this != &s)  4  {  5 char *temp = new char[strlen(s._pStr) + 1];  6  strcpy(temp, s._pStr);  7 delete[] _pStr;  8 _pStr = temp;  9  } 10 return *this; 11 }

注意:

•由於目標對象可能引用了以前分配的數據,所以函數應使用delete[]來釋放這些數據。

•函數應該避免將對象賦值給本身;否則,給對象重新賦值前,釋放內存操作可能刪除對象的內容,

•函數返回一個指向調用對象的引用。

按照之前討論的方法重新定義賦值運算符和拷貝構造函數,還有更簡潔的方法如下:

 1     String(const String& s)  2 :_pStr(NULL) //  3  {  4  String strtemp(s._pStr);  5  std::swap(_pStr, strtemp._pStr);  6  }  7  8 String& operator=(const String& s)  9  { 10 if (this != &s) 11  { 12  String strtemp(s); 13  std::swap(_pStr, strtemp._pStr); 14  } 15 return *this; 16  } 17 18 String& operator=(String s) 19  { 20  std::swap(_pStr, s._pStr); 21 return *this; 22 }

此外,還有一些深度拷貝的不同方法將在下一篇介紹。

 


免責聲明!

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



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