天天看點

C++淺拷貝和深拷貝

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的關系就是這樣的:

C++淺拷貝和深拷貝

進行調試時發現s1和s2确實指向了同一塊記憶體:

C++淺拷貝和深拷貝

淺拷貝問題:

(1)如果類中叧包含簡單資料成員,沒有指向堆的指針, 可以使用編譯器提供的預設複制構造函數。

(2)如果類中包含指向堆中資料的指針,淺複制将出現 嚴重問題

  • 淺複制直接複制兩個對象間的指針成員,導緻兩個指針 指向堆中同一坑記憶體區域。
  • 一個對象的修改将導緻另一個對象的修改。
  • 一個對象超出作用域,将導緻記憶體釋放,使得另一個對 象的指針無效,對其通路将導緻程式異常。

那麼這個問題應該怎麼去解決呢?這就引出了深拷貝。

2、深拷貝

2.1 深拷貝定義

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

深拷貝實際上是這樣的:

C++淺拷貝和深拷貝

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;
}
           

這裡的拷貝構造函數我們很容易了解,先開辟出和源對象一樣大的記憶體區域,然後将需要拷貝的資料複制到目标拷貝對象,那麼這裡的指派運算符的重載是怎麼樣做的呢?

C++淺拷貝和深拷貝

這種方法解決了我們的指針懸挂問題,通過不斷的開空間讓不同的指針指向不同的記憶體,以防止同一塊記憶體被釋放兩次的問題.

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;
}
           

我們先來分析拷貝構造是怎麼實作的:

C++淺拷貝和深拷貝

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

在拷貝構造函數裡的tmp:

C++淺拷貝和深拷貝

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

C++淺拷貝和深拷貝

可以看到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;
}
           

繼續閱讀