【C++】 淺析深淺拷貝

  C++中深拷貝和淺拷貝的問題是很值得咱們注意的知識點,若是編程中不注意,可能會出現疏忽,致使bug。本文就詳細講講C++深淺拷貝的種種。ios

  咱們知道,對於通常對象:編程

    int a = 1;
    int b = 2;

  這樣的賦值,複製很簡單,但對於類對象來講並不通常,由於其內部包含各類類型的成員變量,在拷貝過程當中就會出現問題ide

例如:函數

#include <iostream>
using namespace std;

class String
{
public:

    String(char str = "")
        :_str(new char[strlen(str) + 1]) // +1是爲了不空字符串致使出錯
    {
        strcpy(_str , str);
    }
    
    // 淺拷貝
    String(const String& s)
        :_str(s._str)
    {}
    
    ~String()
    {
        if(_str)
        {
            delete[] _str;
            _str = NULL;
        }
        cout<<"~String()"<<endl;
    }
    
    void Display()
    {
        cout<<_str<<endl;
    }
    
private:
    char *_str;
};

void Test()
{
    String s1("hello");
    String s2(s1);
    String.Display();
    
}

int main()
{
    Test();
    
    return 0;
}

運行結果:學習

wKiom1bqRJ7CC1j7AABxvgeu1pI402.png

咱們發現,編譯經過了,可是崩潰了 =  =ll ,這就是淺拷貝帶來的問題。this


   事實是,在對象拷貝過程當中,若是沒有自定義拷貝構造函數,系統會提供一個缺省的拷貝構造函數,缺省的拷貝構造函數對於基本類型的成員變量,按字節複製,對於類類型成員變量,調用其相應類型的拷貝構造函數。原型以下:spa

String(const String& s)
     {}

可是,編譯器提供的缺省函數並非十全十美的。設計


      缺省拷貝構造函數在拷貝過程當中是按字節複製的,對於指針型成員變量只複製指針自己,而不復制指針所指向的目標--淺拷貝。指針


用圖形象化爲:對象


wKioL1bqSOax7GQiAAAb-81vW_4778.png

  在進行對象複製後,事實上s一、s2裏的成員指針 _str 都指向了一塊內存空間(即內存空間共享了),在s1析構時,delete了成員指針 _str 所指向的內存空間,而s2析構時一樣指向(此時已變成野指針)而且要釋放這片已經被s1析構函數釋放的內存空間,這就讓一樣一片內存空間出現了 「double free」 ,從而出錯。而淺拷貝還存在着一個問題,由於一片空間被兩個不一樣的子對象共享了,只要其中的一個子對象改變了其中的值,那另外一個對象的值也跟着改變。因此這不是咱們想要的結果,同事也不是真正意義上的複製。


爲了解決淺拷貝問題,咱們引出深拷貝,即自定義拷貝構造函數,以下:

String(const String& s)
        :_str(new char[strlen(s._str) + 1])
    {
        strcpy(_str , s._str);
    }

這樣在運行就沒問題了。


那麼,程序中還有沒有其餘地方用到拷貝構造函數呢?

答案:當函數存在對象型的參數(即拷貝構造)或對象型的返回值(賦值時的返回值)時都會用到拷貝構造函數。


而拷貝賦值的狀況基本上與拷貝複製是同樣的。只是拷貝賦值是屬於操做符重載問題。例如在主函數如有:String s3; s3 = s2; 這樣系統在執行時會調用系統提供的缺省的拷貝賦值函數,原型以下:


void operator = (const String& s) 
    {}

咱們自定義的賦值函數以下:

void operator=(const String& s)
   {
      if(_str != s._str)
      {
        strcpy(_str,s._str);
      }
      return *this;
   }


  可是這只是新手級別的寫法,考慮的問題太少。咱們知道對於普通變量來說a=b返回的是左值a的引用,因此它能夠做爲左值繼續接收其餘值(a=b)=30,這樣來說咱們操做符重載後返回的應該是類對象的引用(不然返回值將不能做爲左值來進行運算),以下:

String& operator=(const String& s)
    {
        if(_str != s._str)
        {
            strcpy(_str,s._str);
        }
        return *this;
    }

 

  而上面這種寫法其實也有問題,由於在執行語句時,_str 已經被構造已經分 配了內存空間,可是如此進行指針賦值,_str 直接轉而指向另外一片新new出來的內存空間,而丟棄了原來的內存,這樣便形成了內存泄露。應更改成:

String& operator=(const String& s)
    {
        if(_str != s._str)  
        {
            delete[] _str;
            _str = new char[strlen(s._str) + 1];
            strcpy(_str,s._str);
        }
        return *this;
    }

同時,也考慮到了本身給本身賦值的狀況。


  但是這樣寫就完善了嗎?是否要再仔細思索一下,還存在問題嗎?!其實我能夠告訴你,這樣的寫法也頂多算個初級工程師的寫法。前面說過,爲了保證內存不泄露,咱們前面 delete[]  _str,而後咱們在把new出來的空間給了_str,可是這樣的問題是,你有考慮過萬一 new 失敗了呢?!內存分配失敗,m_psz沒有指向新的內存空間,可是它卻已經把舊的空間給扔掉了,因此顯然這樣的寫法依舊存在着問題。通常高級工程師的寫法會是這樣的:

String& operator=(const String& s)
    {
        if(_str != s._str)
        {
            char *tmp = new char[strlen(s._str) + 1];
            strcpy(tmp , s._str);
            delete[] _str;
            _str = tmp;
        }
        return *this;
    }

這樣寫就比較全面了。


可是!!!還有元老級別的大師寫的更加簡便的拷貝構造和賦值函數,咱們一睹爲快:

<元老級拷貝構造函數>

    String(const String& s)
        :_str(NULL)
    {
        String tmp = s._str;
        swap(_str , tmp._str);
    }

<元老級賦值函數>

// 1.
   String& operator=(const String& s)
   {
     if(_str != s._str)
     {
       String tmp = s._str;
       swap(_str , tmp._str);
     }
     return *this;
   }
// 2.
    String& operator=(String& s)//在此會拷貝構造一個臨時的對象s
   {
     if(_str != s._str)
     {
       swap(_str ,s._str);// 交換this->_str和臨時生成的對象數據成員s._str,離開做用域會自動析構釋放
     }
     return *this;
   }

看出端倪了麼?


  事實上,這是藉助了以上自定義的拷貝構造函數。定義了局部對象 tmp,在拷貝構造中已經爲 tmp 的成員指針分配了一塊內存,因此只須要將 tmp._str 與this->_str交換指針便可,簡化了程序的設計,由於 tmp 是局部對象,離開做用域會調用析構函數釋放交換給 tmp._str 的內存,避免了內存泄露。

這是很是值得咱們學習和借鑑的。


這是本人對C++深淺拷貝的理解,如有紕漏,歡迎留言指正 ^_^


附註總體代碼:

#include <iostream>
using namespace std;

class String
{
public:

    String(char *str = "")
        :_str(new char[strlen(str) + 1])
    {
        strcpy(_str , str);
    }
    
    // 淺拷貝
    String(const String& s)
        :_str(s._str)
    {}


    //賦值運算符重載
    //有問題,會形成內存泄露。。。
    String& operator=(const String& s)
    {
        if(_str != s._str)
        {
            strcpy(_str,s._str);
        }
        return *this;
    }

    // 深拷貝  <傳統寫法>
    String(const String& s)
        :_str(new char[strlen(s._str) + 1])
    {
        strcpy(_str , s._str);
    }
    
    //賦值運算符重載
    //一.  這種寫法有問題,萬一new失敗了。。
    String& operator=(const String& s)
    {
        if(_str != s._str)
        {
            delete[] _str;
            _str = new char[strlen(s._str) + 1];
            strcpy(_str,s._str);
        }
        return *this;
    }

    //二.  對上面的方法改進,先new後delete,若是new失敗也不會影響到_str原來的內容
    String& operator=(const String& s)
    {
        if(_str != s._str)
        {
            char *tmp = new char[strlen(s._str) + 1];
            strcpy(tmp , s._str);
            delete[] _str;
            _str = tmp;
        }
    }

    // 深拷貝  <現代寫法>
    String(const String& s)
        :_str(NULL)
    {
        String tmp = s._str;
        swap(_str , tmp._str);
    }
    
    //賦值運算符的現代寫法一:
    String& operator=(const String& s)
    {
        if(_str != s._str)
        {
            String tmp = s._str;
            swap(_str , tmp._str);
        }
        return *this;
    }
    //賦值運算符的現代寫法二:
    String& operator=(String& s) //在此會拷貝構造一個臨時的對象s
    {
        if(_str != s._str)
        {
            swap(_str ,s._str);//交換this->_str和臨時生成的對象數據成員s._str,離開做用域會自動析構釋放
        }
        return *this;
    }

    ~String()
    {
        if(_str)
        {
            delete[] _str;
            _str = NULL;
        }
        cout<<"~String()"<<endl;
    }

    void Display()
    {
        cout<<_str<<endl;
    }

private:
    char *_str;
};

void Test()
{
    String s1;
    String s2("hello");
    String s3(s2);
    String s4 = s3;

    s1.Display();
    s2.Display();
    s3.Display();
    s4.Display();

}

int main()
{
    Test();

    return 0;
}
相關文章
相關標籤/搜索