C++---寫時拷貝解決深淺拷貝問題

Jammm發表於2018-08-03

對於普通的型別來說,拷貝沒什麼大不了的。

int a = 0;
int b = a;

不會出現任何問題。

而類物件與普通物件不同,類物件內部結構一般較為複雜,存在各種成員變數。

淺拷貝

首先來說說我們常遇到的淺拷貝的情況。

#include <stdio.h> 
class student
{
public:
    student()      // 建構函式,p指向堆中分配的一空間
    {
        _name = new char(100);
        printf("預設建構函式\n");
    }
    ~student()     // 解構函式,釋放動態分配的空間
    {
        if (_name != NULL)
        {
            delete _name;
            _name = NULL;
            printf("解構函式\n");
        }
    }
private:
    char * _name;     // 一指標成員
};

int main()
{
    student a;
    student b(a);   // 複製物件
    return 0;
}

這段程式碼乍看之下沒什麼毛病,通過類的預設建構函式將 a 複製給 b ,但是一旦執行就會程式崩潰
經過我的刻苦學習與鑽研,終於發現其中的問題所在。
由於我的類沒有拷貝建構函式,所以student b(a)會呼叫,編譯器自動生成的一個預設拷貝建構函式,該建構函式完成物件之間的位拷貝。位拷貝又稱淺拷貝
淺拷貝

  • 淺拷貝只是拷貝了指標,並沒有建立新的空間,使得兩個指標指向同一個地址,這樣在物件塊結束,呼叫函式析構的時,會造成同一份資源析構2次,即delete同一塊記憶體2次,造成程式崩潰。
  • 淺拷貝使得 a 和 b 指向同一塊記憶體,任何一方的變動都會影響到另一方。
  • 由於 a 和 b 指向的是同一塊記憶體空間,當 a 釋放了後,b 指向的記憶體空間不復存在,所以會出現記憶體洩露的情況。

如何避免淺拷貝害人呢?
養成自定義拷貝建構函式的習慣,當顯式定義了拷貝建構函式後,編譯器就會呼叫拷貝建構函式了,為了不出現程式崩潰,請使用自定義拷貝建構函式,當然我們自己如果把程式碼寫成了淺拷貝的形式,那也不是不可能的事。

深拷貝

// 使用自定製拷貝建構函式,完成深拷貝!!!
class A
{
public:
    A()      // 建構函式,p指向堆中分配的一空間
    {
        m_pdata = new char(100);
        printf("預設建構函式\n");
    }

    A(const A& r)    // 拷貝建構函式
    {
        m_pdata = new char(100);    // 為新物件重新動態分配空間
        memcpy(m_pdata, r.m_pdata, strlen(r.m_pdata));
        printf("copy建構函式\n");
    }

    ~A()     // 解構函式,釋放動態分配的空間
    {
        if (m_pdata != NULL)
        {
            delete m_pdata;
            printf("解構函式\n");
        }
    }

private:
    char *m_pdata;     // 一指標成員
};

int main()
{
    A a;
    A b(a);   // 複製物件
    return 0;
}

在拷貝建構函式中,為 b 物件 new 了一個新的空間,這樣 a 和 b 指向的是不同的空間,只是內容一致,但是互不影響。
重複的去開闢空間和釋放空間效率是很低的,聰明的地球人決定使用寫時拷貝。

寫時拷貝

寫時拷貝:引入一個計數器,每片不同內容的空間上都再由一個計數器組成,在構造第一個類指向時,計數器初始化為1,之後每次有新的類也指向同一片空間時,計數器加 1 ;在析構時判斷該片空間對應計數器是否為1,為1則執行清理工作,大於1則計數器減 1 。如果有需要進行增刪等操作時,再拷貝空間完成,有利於提高效率。

class String
{
public:
    String(const char* str = "")
        :_str(new char[strlen(str) + 1 + 4])//+1表示字串後面要放一個'\0',+4表示多開闢一個空間存放引用計數
    {
        _str += 4;//_str指向資料存放區
        strcpy(_str, str);
        _GetCount() = 1;
    }
    String(const String& s)
        :_str(s._str)
    {
        _GetCount()++;
    }
    String& operator=(String& s)
    {
        if (this != &s)
        {
            if (--_GetCount() == 0)
            {
                delete[](_str - 4);
            }
            ++s._GetCount();
            _str = s._str;
        }
        return *this;
    }
    ~String()
    {
        if (--_GetCount() == 0)
        {
            delete[](_str - 4); // 注意:由於計數器存放在了_str首地址-4的地址上,所以在析構時一定要注意全部釋放,避免記憶體洩漏。
        }
    }
public:
    int& _GetCount()
    {
        return *((int*)_str - 1);
    }
private:
    char* _str;

};

相關文章