1. 程式人生 > >C++類拷貝控制 深拷貝 淺拷貝

C++類拷貝控制 深拷貝 淺拷貝

普通型別物件之間的複製很簡單,而類物件與普通物件不同,類物件內部結構一般較為複雜,存在各種成員變數,這篇文章將幫你理清C++類物件的拷貝方式

拷貝建構函式,拷貝賦值運算子

首先我們簡單瞭解下預設的拷貝建構函式和拷貝賦值運算子

拷貝建構函式

第一個引數是自身類型別引用,其他引數都有預設值的建構函式就是拷貝建構函式

class Sales_data {
public:
    Sales_data();           //預設建構函式
    Sales_data(const Foo&); //預設拷貝建構函式
    //...
};

拷貝建構函式用來初始化非引用類型別引數,所以拷貝建構函式自己的引數必須是引用型別(如果不是引用:為了呼叫拷貝建構函式,必須拷貝它的實參,為了拷貝實參,又需要呼叫拷貝建構函式,無限迴圈)

合成拷貝建構函式(預設)

和預設建構函式一樣,編譯器會幫你定義一個預設拷貝建構函式(如果你沒有手動定義的話),不同的是,如果你定義了其他建構函式,編譯器還是會給你合成一個拷貝建構函式

舉個例子:Sales_data的合成拷貝建構函式等價於

class Sales_data {
public:
    Sales_data();
    Sales_data(const Sales_data&);
private:
    std::string bookNo;
    int units_sold = 0;
    double revenue = 0.0;
};

Sales_data::Sales_data(const Sales_data& origin) :
    bookNo(origin.bookNo),          //使用string的拷貝建構函式
    units_sold(origin.units_sold),  //拷貝
    revenue(origin.revenue) {       //拷貝
                                    //空函式體
    }

直接初始化,拷貝初始化

通過以下幾行程式碼不難理解

string dots(10,'.');                //直接初始化
string s(dots);                     //直接初始化
string s2 = dots;                   //拷貝初始化
string null_book = "9-999-9999-9"   //拷貝初始化
string nines = strings(100,'9');    //拷貝初始化

使用直接初始化時,我們是在要求編譯器使用普通的函式匹配,來選擇與我們提供的引數最匹配的建構函式

使用拷貝初始化時,我們要求編譯器將右側運算子物件拷貝到正在建立的物件中(需要的話還進行型別轉換

拷貝賦值運算子

賦值運算子本質也是函式,它由operator關鍵字後面接要定義的運算子的符號組成,賦值運算子就是一個名為operator=的函式,和其他函式一樣,它也有一個返回型別和一個引數列表

引數表示運算子的運算物件,某些運算子(包括賦值運算子)必須定義為成員函式,如果一個運算子是成員函式,則其左側運算物件就能繫結到隱式的this引數上,對於一個二元運算子(例如賦值運算子),右側運算物件就會作為顯示引數傳遞

拷貝賦值運算子接受一個與其所在類相同型別的引數

class Sales_data {
public:
    Sales_data& operator=(const Sales_data&);
};

為了與內建型別的賦值保持一直,賦值運算子通常返回一個指向其左側運算物件的引用

合成拷貝賦值運算子(預設)

和拷貝建構函式一樣,如果一個類未定義自己的拷貝賦值運算子,編譯器會生成一個合成拷貝賦值運算子,類似拷貝建構函式,對於某些類,合成拷貝賦值運算子用來禁止該型別物件的賦值

拷貝賦值運算子會將右側運算物件的每個非static成員賦予左側運算物件的對應成員,對於陣列型別的成員,逐個賦值陣列元素合成拷貝賦值運算子返回一個指向其左側運算物件的引用

Sales_data& Sales_data::operator=(const Sales_data& rhs) {
    bookNo = rhs.bookNo;
    units_sold = rhs.units_sold;
    revenue = rhs.revenue;
    return *this;
}

淺拷貝

回頭看看我們最初的Sales_data類

class Sales_data {
public:
    Sales_data();
    Sales_data(const Sales_data&);
private:
    std::string bookNo;
    int units_sold = 0;
    double revenue = 0.0;
};

以下這樣的初始化看似沒有什麼問題

int main()
{
    Sales_data data1;
    Sales_data data2 = data1;
}

下面給出一個和Sales_data不太一樣的Array類

class Array
{
public:
    Array()                 //建構函式
    {
        m_iCount = 5;
        m_pArr = new int[m_iCount];
    }
    Array(const Array& rhs) //拷貝建構函式(相當於預設拷貝建構函式)
    {
        m_iCount = rhs.m_iCount;
        m_pArr = rhs.m_pArr;
    }
private:
    int m_iCount;
    int* m_pArr;
};

(這裡的拷貝建構函式其實相當於編譯器合成的預設拷貝建構函式)

我們用同樣的方式初始化的時候:

int main()
{
    Array array1;
    Array array2 = array1;
}

預設拷貝建構函式可以完成物件的資料成員簡單的複製,但是由於我們這裡有一個指標型別的成員變數m_pArr,直接使用預設拷貝就會出現一個問題,兩個物件的m_pArr指標指向了同一塊區域

當物件arr2通過物件arr1初始化,物件arr1已經申請了記憶體,那麼物件arr2就會指向物件arr1所申請的記憶體,如果物件arr1釋放掉記憶體,那麼物件A中的指標就是野指標了,這就是淺拷貝

深拷貝

為了避免這樣的記憶體洩露,擁有指標成員的物件進行拷貝的時候,需要自己定義拷貝建構函式,使拷貝後的物件指標成員擁有自己的記憶體地址

class Array {
public:
    Array() {
        m_iCount = 5;
        m_pArr = new int[m_iCount];
    }
    Array(const Array& rhs) {
        m_iCount = rhs.m_iCount;
        m_pArr = new int[m_iCount];
        for (int i = 0; i < m_iCount; i++)
        {
            m_pArr[i] = rhs.m_pArr[i];
        }
    }
private:
    int m_iCount;
    int* m_pArr;
};

對比一下

  • 淺拷貝也叫位拷貝,拷貝的是地址
  • 深拷貝也叫值拷貝,拷貝的是內容

深拷貝和淺拷貝可以簡單理解為:如果一個類擁有資源,當這個類的物件發生複製過程的時候,資源重新分配,這個過程就是深拷貝,反之,沒有重新分配資源,就是淺拷