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

  • 2019 年 10 月 3 日
  • 筆記

普通類型對象之間的複製很簡單,而類對象與普通對象不同,類對象內部結構一般較為複雜,存在各種成員變量,這篇文章將幫你理清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;  };

對比一下

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

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