1. 程式人生 > >【c++學習筆記】深入解析淺拷貝與深拷貝

【c++學習筆記】深入解析淺拷貝與深拷貝

測試環境:vs2013

什麼是淺拷貝

  • 也稱位拷貝,編譯器只是將物件中的值拷貝過來,如果物件中管理資源,最後就會導致多個物件共享同一份資源,當一個物件銷燬時就會將該資源釋放掉,而此時另一些物件不知道該資源已經被釋放,以為還有效,所以當繼續對資源進行操作時,就會發生髮生了訪問違規。

先看下面的程式碼有問題嗎

class String
{
public:
    String(const char* ptr = "")//建構函式,預設放'\0'
        :_ptr(new char[strlen(ptr) + 1])//strlen計算出的長度不加\0
    {
        strcpy
(_ptr, ptr); } ~String() { if (_ptr) { cout << "~String()" << this << endl; delete[] _ptr; } } private: char* _ptr; }; int main() { String s1("hello"); String s2(s1); return 0; }

執行結果如下:
這裡寫圖片描述

  • 可以發現這裡程式會崩,為什麼會造成這樣的原因呢,那就必須搞清楚上面的程式碼都做了什麼
    這裡寫圖片描述

    這就是淺拷貝,多個物件共享同一份資源,造成的問題也顯而易見,一份資源被釋放了多次。那麼,怎麼解決呢?

引用計數

  • 當多個物件共享一塊資源時,要保證該資源只釋放一次,只需記錄有多少個物件在使用該資源即可,每減少(增加)一個物件使用,給該計數減一(加一),當最後一個物件不使用時,該物件負責將資源釋放掉即可;觀察下面程式碼:
class String
{
public:
    String()
    {}

    String(const char* str )
    {
        if (str == NULL)
        {
            _str
= new char[4+1];//多申請一個int用來儲存計數器 _str += 4; _str = '\0'; } else { _str = new char[strlen(str) + 1 + 4]; _str += 4; strcpy(_str, str); } GetCount(_str) = 1;//將計數器初始值設定為1 } String(const String& s)//拷貝構造 :_str(s._str) { GetCount(_str)++; } //s1=s2 String& operator=(const String& s)//賦值操作符過載 { if (_str != s._str) { //1.當s1的引用計數為1時 //①釋放空間 //②改變s1指標的指向 //③s2的引用計數加1 Release(); //當s1的引用計數大於1時 //①s1的引用計數減1 //同上②③ _str = s._str; ++GetCount(_str);//引用計數+1 } return *this; } ~String()//解構函式 { Release(); } private: int& GetCount(char* str) { return *(int*)(str - 4);//因為這塊記憶體型別為char } void Release() { if (_str != NULL && (--GetCount(_str)) == 0) { delete [](_str - 4);//一定要釋放儲存計數器的空間 } } private: char* _str; }; int main() { String s1("hello"); String s2(s1); String s3("world"); String s4(s3); String s5(s3); s1 = s3; }

上面的程式碼是在構造的時候多申請4位元組的空間用來儲存計數器,從而實現引用計數。那麼具體是怎麼做的呢?
這裡寫圖片描述
但是這樣還是有問題,看下面的程式碼

class String
{
public:
    String()
    {}

    String(const char* str)
    {
        if (str == NULL)
        {
            _str = new char[4 + 1];//多申請一個int用來儲存計數器
            _str += 4;
            _str = '\0';
        }
        else
        {
            _str = new char[strlen(str) + 1 + 4];
            _str += 4;
            strcpy(_str, str);
        }

        GetCount(_str) = 1;//將計數器初始值設定為1
    }

    String(const String& s)//拷貝構造
        :_str(s._str)
    {
        GetCount(_str)++;
    }

    //s1=s2
    String& operator=(const String& s)//賦值操作符過載
    {
        if (_str != s._str)
        {
            Release();
            _str = s._str;
            ++GetCount(_str);//引用計數+1
        }

        return *this;
    }

    char& operator[](size_t index)//可以採用下標的方式訪問String類
    {
        return _str[index];
    }


    ~String()//解構函式
    {
        Release();
    }

private:
    int& GetCount(char* str)
    {
        return *(int*)(str - 4);//因為這塊記憶體型別為char
    }

    void Release()
    {
        if (_str != NULL && (--GetCount(_str)) == 0)
        {
            delete[](_str - 4);//一定要釋放儲存計數器的空間
        }
    }

private:
    char* _str;
};


int main()
{
    String s1("hello");
    String s2(s1);
    String s3(s1);
    s3[1] = 'a';

    return 0;
}

這裡寫圖片描述

  • 當幾個共用同一塊空間的物件中的任一物件修改字串中的值,則會導致所有共用這塊空間的物件中的內容被破壞掉。像上面的程式碼中,我們只想改變s3中的值,但是和他公用空間的另外兩個物件s2,s1中的值也改變了,這就引出了寫時拷貝

寫時拷貝

  • 有多個物件共享同一個空間時,當對其中一個物件只讀時,不會有什麼影響,但是如果想要改變(寫入)某一個物件中的值時,這時就要為這個物件重新分配空間。程式碼實現如下:
class String
{
public:
    String()
    {}

    String(const char* str)
    {
        if (str == NULL)
        {
            _str = new char[4 + 1];//多申請一個int用來儲存計數器
            _str += 4;
            _str = '\0';
        }
        else
        {
            _str = new char[strlen(str) + 1 + 4];
            _str += 4;
            strcpy(_str, str);
        }

        GetCount() = 1;//將計數器初始值設定為1
    }

    String(const String& s)//拷貝構造
        :_str(s._str)
    {
        GetCount()++;
    }

    //s1=s2
    String& operator=(const String& s)//賦值操作符過載
    {
        if (_str != s._str)
        {
            Release();

            _str = s._str;
            ++GetCount();//引用計數+1
        }

        return *this;
    }

    char& operator[](size_t index)//可以採用下標的方式訪問String類
    {
        if (GetCount() > 1)
        {
            --GetCount();
            char* pTmp = new char[strlen(_str) + 1 + 4];
            pTmp += 4;
            strcpy(pTmp, _str);
            _str = pTmp;
            GetCount() = 1;//將新空間值賦1
        }

        return _str[index];
    }

    const char& operator[](size_t index)const//[]操作符必須成對過載
    {
        return _str[index];
    }

    ~String()//解構函式
    {
        Release();
    }

private:
    int& GetCount()
    {
        return *(int*)(_str - 4);//因為這塊記憶體型別為char
    }

    void Release()
    {
        if (_str != NULL && (--GetCount()) == 0)
        {
            delete[](_str - 4);//一定要釋放儲存計數器的空間
        }
    }

private:
    char* _str;
};


int main()
{
    String s1("hello");
    String s2(s1);
    String s3(s1);
    s3[1] = 'a';

    return 0;
}

深拷貝

  • 給要拷貝構造的物件重新分配空間
class String
{
public:

    String(const char* str = "")
        :_str(new char[strlen(str)+1])
    {
        strcpy(_str, str);
    }

    String(const String& s)//深拷貝
        : _str(new char[strlen(s._str) + 1])
    {
        strcpy(_str, s._str);
    }
    //賦值操作符過載
    //方法一
    //String& operator=(const String& s)
    //{
    //  if (this != &s)
    //  {
    //      delete[] _str;
    //      _str = new char[strlen(s._str) + 1];
    //      strcpy(_str, s._str);
    //  }
    //  return *this;//為了支援鏈式訪問
    //}

    //方法二(優)
    String& operator=(const String& s)
    {
        if (this != &s)//自己不能拷貝自己
        {
            char* tmp = new char[strlen(s._str) + 1];
            strcpy(tmp, s._str);
            delete[] _str;
            _str = tmp;
        }
        return *this;//為了支援鏈式訪問
    }

    ~String()
    {
        if (_str != NULL)
        {
            delete[]_str;
        }
    }

private:
    char* _str;
};
int main()
{
    String s1("hello");
    String s2(s1);

    String s3("world");
    s1 = s3;
}
  • 一般情況下,上面對賦值操作符過載的兩種寫法都可以,但是相對而言,第二種更優一點。
    對於第一種,先釋放了舊空間,但是如果下面用new開闢新空間時有可能失敗——>拋異常,而這時你是將s2賦值給s3,不僅沒有賦值成功(空間開闢失敗),而且也破壞了原有的s3物件。對於第二種,先開闢新空間,將新空間的地址賦給一個臨時變數,就算這時空間開闢失敗,也不會影響原本s3物件。