1. 程式人生 > 其它 >C++ Primer學習筆記 - 第13章 拷貝控制

C++ Primer學習筆記 - 第13章 拷貝控制

目錄

本章主要內容,類定義建構函式,用來控制在建立此型別物件時做什麼。學習類如何控制該型別物件拷貝、賦值、移動或銷燬時做什麼。
主要函式:拷貝建構函式、移動建構函式、拷貝賦值運算、移動賦值運算子以及解構函式。

拷貝控制操作 --
拷貝和移動建構函式,定義了當用同類型的另一個物件初始化本物件時做什麼。
拷貝和移動賦值運算子,定義了將一個物件賦予同類型的另一個物件時做什麼。
解構函式,定義了當此型別物件銷燬時做什麼。

13.1 拷貝、賦值與銷燬

如果一個建構函式的第一個引數是自身類型別的引用,且任何額外引數都有預設值,則此建構函式是拷貝建構函式。

class Foo {
public:
  Foo(); // 預設建構函式
  Foo(const Foo&); // 拷貝建構函式,通常不應該是explicit的,即允許隱式轉化
  // ...
};

合成拷貝建構函式
如果沒有為一個類定義拷貝建構函式,編譯器會為我們定義一個。與合成預設建構函式不同,即使定義了其他建構函式,編譯器也會為我們合成一個拷貝建構函式。
對某些類來說,合成拷貝建構函式用來阻止拷貝該類型別的物件。
一般情況,合成拷貝建構函式會將其引數的成員逐個拷貝到正在建立的物件中。編譯器從給定物件中依次將每個非static成員拷貝到正在建立的物件中。
每個成員的型別決定了它如何拷貝:對類型別的成員,使用其拷貝建構函式來拷貝;內建型別的成員,直接拷貝。

問題:如果類的成員是陣列,那麼如何拷貝?
不能直接拷貝一個數組,但合成的拷貝建構函式會逐元素地拷貝一個數組型別的成員。如果陣列是類型別(如array,vector),則用元素的拷貝建構函式來進行拷貝。

例子,Sales_data類的合成拷貝建構函式等價於

class Sales_data{
  Sales_data(const Sales_data&);

private:
  std::string bookNo;
  int units_sold = 0;
  double revene = 0.0;
};

// 與Sales_data的合成的拷貝建構函式等價
Sales_data::Sales_data(const Sales_data& orig) : 
bookNo(orig.bookNo),  // 使用string的拷貝建構函式
units_sold(orig.units_sold),  // 直接值拷貝
revenue(orig.revene)  // 直接值拷貝
{ // 空函式體
}

拷貝初始化
直接初始化 vs 拷貝初始化
使用自己初始化時,實際上要求編譯器使用普通的函式匹配,來選擇與我們提供的引數最匹配的建構函式。 -- 相當於從一組過載函式中,選擇一個引數最匹配的版本(不一定是拷貝建構函式)
使用拷貝初始化時,要求編譯器將右側運算物件拷貝到正在建立的物件中,如果需要的話,還要進行型別轉換。 -- 呼叫拷貝建構函式進行構造,有時是移動建構函式

拷貝初始化應用場景:

  1. 用= 定義變數;
  2. 將一個物件作為實參傳遞給一個非引用型別的形參;
  3. 用花括號列表初始化一個數組中的元素或一個聚合類中的成員;
  4. 初始化標準庫容器,或呼叫其insert/push函式時,容器會對其元素進行拷貝初始化;

引數和返回值
函式呼叫過程中,具有非引用型別的引數要進行拷貝初始化。返回非引用型別時,返回值會被用來初始化呼叫方的結果。

拷貝初始化的限制
拷貝建構函式一般不能是explicit型別的。如果要求使用的初始化值用一個explicit的建構函式來進行型別轉換,那麼要小心使用直接初始化和拷貝初始化。

// vector接受單一大小引數的建構函式是explicit的

vector<int> v1(10); // 正確:直接初始化
vector<int> v2 = 10;  // 錯誤:接受容器大小引數的建構函式是explicit的

// 對於函式f,接收型別為vector<int>的引數
void f(vector<int>);
f(10); // 呼叫錯誤:不能用一個explicit的建構函式拷貝一個實參
f(vector<int>(10)); // 正確:從一個int直接構造一個臨時vector

假設vector建構函式不是explicit,那麼下面語句是正確的

vector<int> v2 = 10; 

// 等價於
vector<int> tmp = vecotr<int>(10);
vector<int> v2 = tmp;

編譯器可以繞過拷貝建構函式
拷貝、移動建構函式必須是存在且可訪問的(非private)

string null_book = "9-999-99999-9"; // 拷貝初始化

string null_book("9-999-99999-9"); // 編譯器繞過拷貝建構函式,直接建立物件

13.1.2 拷貝賦值運算子

類可以用 “=” 控制其物件如何賦值。類似拷貝建構函式,如果類未定義自己的拷貝賦值運算子,編譯器會合成一個。

Sales_data trans, accum;
trans = accum; // 使用Sales_data的拷貝賦值運算子

拷貝賦值運算子和用“=”定義變數(拷貝初始化),最大的區別在於:使用拷貝賦值運算子的時候,前提條件是物件必須已經存在,即初始化完畢。而如果還是在構建物件階段,就需要使用拷貝建構函式。

過載賦值運算子
過載運算子本質是函式,賦值運算是名為operator=的函式。
= 左側運算物件繫結到隱式this引數,右側運算物件作為顯式引數傳遞。賦值運算子返回一個指向左側運算物件的引用。

class Foo {
public:
  Foo& operator=(const Foo&); // 賦值運算子
  ...
};

合成拷貝賦值運算子
如果一個類未定義直接的拷貝賦值運算子,編譯器會為它生成一個合成拷貝賦值運算子。會將右側運算子物件的每個非static成員賦予左側運算物件的對應成員。對於陣列成員逐個賦值陣列元素。

// 等價於合成拷貝賦值運算子
Sales_data&
Sales_data::operator=(const Sales_data& rhs) {
  bookNo = rhs.bookNo; // 呼叫string::operator=
  units_sold = rhs.units_sold; // 使用內建的int
  revenue = rhs.revenue; // 使用內建的double賦值
  return *this; // 返回此物件引用
}

// 這樣定義了合成拷貝賦值運算子後,可以對Sales_data物件進行拷貝賦值
Sales_data d1, d2;
d1 = d2; // 拷貝賦值運算

// 注意下面是呼叫拷貝建構函式
Sales_data d3(d1);
Sales_data d4 = d2;

13.1.3 解構函式

解構函式執行與建構函式相反操作:建構函式初始化物件的非static資料成員,解構函式是否物件使用的資源,銷燬物件的非static資料成員。
解構函式沒有返回值,也不接受引數。解構函式不能被過載,而且對應一個類僅有一個。

class Foo {
public:
  ~Foo(); // 解構函式
  // ..
}

解構函式完成什麼工作?
解構函式釋放物件在生存期分配的所有資源。
建構函式初始化成員,是按它們在類中出現的順序進行初始化。解構函式中,先執行函式體,然後按初始化順序的逆序銷燬成員。

注意:內建指標型別的成員不會delete所指向的物件
智慧指標與普通指標不同,智慧指標是類型別,具有解構函式,在析構階段自動銷燬。

什麼時候呼叫解構函式
無論何時一個物件被銷燬,會自動呼叫其解構函式,具體體現在:

  • 變數值離開其作用域時被銷燬;
  • 當一個物件被銷燬時,其成員被銷燬;
  • 容器(標準庫容器或陣列)被銷燬時,其元素被銷燬;
  • 對於動態分配的物件,當對指向它的指標應用delete運算子時被銷燬;
  • 對應臨時物件,當建立它的完整表示式結束時被銷燬;

合成解構函式
當一個類未定義自己的解構函式時,編譯器會為它定義一個合成解構函式。
需要注意的是:

  • 解構函式本身並不直接銷燬成員,成員是在解構函式體之後隱含的析構階段中被銷燬的;
  • 合成解構函式函式不會delete一個指標資料成員,需要定義一個解構函式來釋放函式分配的記憶體;

13.1.4 三/五法則

三個基本操作控制類的拷貝:拷貝建構函式、拷貝賦值運算子、解構函式;
兩個新增的操作:移動建構函式、移動賦值運算子。

需要解構函式的類也需要拷貝和賦值操作
因為自定義解構函式往往用來釋放合成解構函式無法釋放的記憶體,比如指標資料成員指向的記憶體。此時,也需要自定義拷貝和賦值操作,因為合成的拷貝和賦值操作,通常無法處理指標指向的記憶體。

需要拷貝操作的類也需要賦值操作,反之亦然
某些類需要完成的工作,只需要拷貝或賦值,不需要析構。比如,類為每個物件分配一個獨有的、唯一的序號。該類需要自定義一個拷貝建構函式為每個新建物件生成該序號。賦值操作也同樣需要自定義。

13.1.5 使用=default

將拷貝控制成員定義為=default顯示地要求編譯器生成合成的版本 -- 合成預設建構函式,合成拷貝建構函式,合成賦值運算子,合成解構函式。
注意:只能對具有合成版本的成員函式使用 =default (預設建構函式,拷貝控制成員)

// 類內用=default修飾宣告,合成的函式將隱式宣告為內聯的;類外,就不是內聯的。

class Sales_data {
public:
  Sales_data() = default;
  Sales_data(const Sales_data&) = default;
  Sales_data& operator=(const Sales_data &);
  ~Sales_data() = default;
  // 其他成員的定義,略
}

Sales_data& operator=(const Sales_data &) = default;

13.1.6 阻止拷貝

大多數類應該定義預設建構函式、拷貝建構函式和拷貝賦值運算子。少數類可能會需要阻止拷貝,如iostream類阻止拷貝,避免多個物件寫入或讀取相同的IO緩衝。

定義刪除的函式
可以通過將拷貝建構函式和拷貝賦值運算子,定義為刪除的函式來阻止拷貝。
刪除的函式:雖然聲明瞭該函式,但不能以任何方式使用它們。

struct NoCopy {
  NoCopy() = default; // 使用合成的預設建構函式
  NoCopy(const NoCopy&) = delete; // 阻止拷貝
  NoCopy& operator=(const NoCopy&) = delete; // 阻止賦值
  ~NoCopy() = default; // 使用合成的解構函式
}

解構函式不能是刪除的成員
不能刪除解構函式,如果刪除解構函式,就無法銷燬此型別物件。

合成的拷貝控制成員可能是刪除的
如果一個類有一個數據成員不能預設構造、拷貝、複製或銷燬,則對應成員函式將被定義為刪除的。
一個成員有刪除的,或不可訪問的解構函式,會導致合成的預設和拷貝建構函式被定義為刪除的。