[C++ Primer Note12] 拷貝,賦值與銷燬
當定義一個類時,我們顯式地或隱式地制定在此型別的物件拷貝,移動,賦值和銷燬時做什麼。一個類通過五種特殊的成員函式來控制這些操作,包括:拷貝建構函式 ,拷貝賦值運算子 ,移動建構函式 ,移動賦值運算子 和解構函式 。我們稱這些操作為拷貝控制操作(copy control)
- 如果一個建構函式的第一個引數是自身類型別的引用 ,且任何額外引數都有預設值,則此建構函式是拷貝建構函式 。通常這個引數都是const 的,並且該函式不應該是explicit 的。
- 如果我們沒有為一個類定義拷貝建構函式,編譯器會為我們定義一個。即使我們定義了其他建構函式,編譯器也會為我們合成一個拷貝建構函式。
- 一般情況下,合成的拷貝建構函式會將其引數的成員逐個拷貝到正在建立的物件中。對類型別的成員,會使用其拷貝建構函式來拷貝,內建型別的成員則直接拷貝。雖然我們不能拷貝一個數組,但合成拷貝建構函式會逐個元素地拷貝一個數組型別的成員。
- 當使用直接初始化時,我們實際上是要求編譯器使用普通的函式匹配。當使用拷貝初始化 時,我們要求編譯器將右側運算物件拷貝到正在建立的物件中。
- 拷貝初始化不僅在我們使用= 定義變數時會發生,在下列情況下也會發生:
- 將一個物件作為實參傳遞給一個非引用型別的形參
- 從一個返回型別為非引用型別的函式返回一個物件
- 用花括號列表初始化一個數組中的元素或一個聚合類中的成員
- 與類控制其物件如何初始化一樣,類也可以控制其物件如何賦值:
Sales_data trans,accum; trans = accum;//使用拷貝賦值運算子
如果類未定義自己的拷貝賦值運算子,編譯器會為它合成一個。
- 過載運算子本質上是函式,其名字由operator 關鍵字後接要定義的運算子的符號組成。因此,賦值運算子 就是一個名為operator= 的函式。類似於其它函式,運算子函式也有返回值和引數列表。
- 過載運算子的引數表示運算子的運算物件。某些運算子,包括賦值運算子,必須定義為成員函式。如果一個運算子是成員函式,其左側運算物件就繫結到隱式的this 引數。對於一個二元運算子,其右側運算物件作為顯式引數傳遞。
- 賦值運算子通常返回一個指向其左側運算物件的引用 :
1. 如果一個建構函式的第一個引數是**自身類型別的引用**,且任何額外引數都有預設值,則此建構函式是**拷貝建構函式**。通常這個引數都是**const**的,並且該函式不應該是**explicit**的。 2. 如果我們沒有為一個類定義拷貝建構函式,編譯器會為我們定義一個。即使我們定義了其他建構函式,編譯器也會為我們合成一個拷貝建構函式。 3. 一般情況下,合成的拷貝建構函式會將其引數的成員逐個拷貝到正在建立的物件中。對類型別的成員,會使用其拷貝建構函式來拷貝,內建型別的成員則直接拷貝。雖然我們不能拷貝一個數組,但合成拷貝建構函式會逐個元素地拷貝一個數組型別的成員。 3. 當使用直接初始化時,我們實際上是要求編譯器使用普通的函式匹配。當使用**拷貝初始化**時,我們要求編譯器將右側運算物件拷貝到正在建立的物件中。 4. 拷貝初始化不僅在我們使用**=**定義變數時會發生,在下列情況下也會發生: *將一個物件作為實參傳遞給一個非引用型別的形參 *從一個返回型別為非引用型別的函式返回一個物件 *用花括號列表初始化一個數組中的元素或一個聚合類中的成員 5. 與類控制其物件如何初始化一樣,類也可以控制其物件如何賦值:
Sales_data trans,accum;
trans = accum;//使用拷貝賦值運算子
如果類未定義自己的拷貝賦值運算子,編譯器會為它合成一個。 6. 過載運算子本質上是函式,其名字由**operator**關鍵字後接要定義的運算子的符號組成。因此,**賦值運算子**就是一個名為**operator=**的函式。類似於其它函式,運算子函式也有返回值和引數列表。 7. 過載運算子的引數表示運算子的運算物件。某些運算子,包括賦值運算子,必須定義為成員函式。如果一個運算子是成員函式,其左側運算物件就繫結到隱式的**this**引數。對於一個二元運算子,其右側運算物件作為顯式引數傳遞。 8. 賦值運算子通常返回一個指向其左側運算物件的**引用**:
Sales_data& Sales_data::operator=(const Sales_data &rhd){
bookNo=rhs.bookNo;
units_sold=rhs.units_sold;
revenue=rhs.revenue;
return *this;
}
如果一個類未定義自己的拷貝賦值運算子,編譯器會為它合成一個。 13. 解構函式執行與建構函式相反的操作,釋放物件使用的資源,並銷燬物件的非static資料成員。解構函式是類的一個成員函式,名字由波浪號接類名構成。它沒有返回值,也**不接受引數**,因此也不支援過載。 14. 在一個解構函式中,首先執行函式體,然後銷燬成員。成員按初始化順序的**逆序**銷燬。析構部分是隱式的,成員銷燬時發生什麼**完全依賴於成員的型別**。 15. 無論何時一個物件被銷燬,就會自動呼叫其解構函式: *變數在離開作用域時被銷燬 *當一個物件被銷燬時,其成員被銷燬 *容器被銷燬時,其元素被銷燬 *對於動態分配的物件,當對指標應用delete時被銷燬 *對於臨時物件,當建立它的完整表示式結束時被銷燬 16. 解構函式體自身**並不直接銷燬成員**,成員是在解構函式體之後隱含的析構階段中被銷燬的。 17. 隱式銷燬一個內建指標型別的成員不會delete它所指向的物件。 18. 如前所述,已經有三個基本操作可以控制類的拷貝操作,在新標準下,一個類還可以定義一個**移動建構函式**和一個**移動賦值運算子**,C++並不要求我們定義所有這些操作。 19. 當我們決定一個類是否要定義自己版本的拷貝控制成員時,一個基本原則是首先確定這個類是否需要一個**解構函式**。通常,對解構函式的需求比對拷貝建構函式或賦值運算子的需求更明顯。如果一個類需要一個解構函式,我們幾乎可以肯定它也需要一個拷貝建構函式和一個拷貝賦值運算子。因為合成解構函式不會delete一個指標資料成員,所以有時候需要定義一個解構函式,但同時如果採用合成拷貝建構函式和合成拷貝運算子時就會出現很多問題,因為合成的方法僅僅拷貝指標的值,而不是拷貝物件。 20. 如果一個類需要一個拷貝建構函式,幾乎可以肯定它也需要一個拷貝賦值運算子,反之亦然。但無論需要拷貝建構函式還是拷貝賦值運算子不意味著也需要解構函式。 21. Sales_data& Sales_data::operator=(const Sales_data &rhd){ bookNo=rhs.bookNo; units_sold=rhs.units_sold; revenue=rhs.revenue; return *this; }
如果一個類未定義自己的拷貝賦值運算子,編譯器會為它合成一個。
- 解構函式執行與建構函式相反的操作,釋放物件使用的資源,並銷燬物件的非static資料成員。解構函式是類的一個成員函式,名字由波浪號接類名構成。它沒有返回值,也不接受引數 ,因此也不支援過載。
- 在一個解構函式中,首先執行函式體,然後銷燬成員。成員按初始化順序的逆序 銷燬。析構部分是隱式的,成員銷燬時發生什麼完全依賴於成員的型別 。
- 無論何時一個物件被銷燬,就會自動呼叫其解構函式:
- 變數在離開作用域時被銷燬
- 當一個物件被銷燬時,其成員被銷燬
- 容器被銷燬時,其元素被銷燬
- 對於動態分配的物件,當對指標應用delete時被銷燬
- 對於臨時物件,當建立它的完整表示式結束時被銷燬
- 解構函式體自身並不直接銷燬成員 ,成員是在解構函式體之後隱含的析構階段中被銷燬的。
- 隱式銷燬一個內建指標型別的成員不會delete它所指向的物件。
- 如前所述,已經有三個基本操作可以控制類的拷貝操作,在新標準下,一個類還可以定義一個移動建構函式 和一個移動賦值運算子 ,C++並不要求我們定義所有這些操作。
- 當我們決定一個類是否要定義自己版本的拷貝控制成員時,一個基本原則是首先確定這個類是否需要一個解構函式 。通常,對解構函式的需求比對拷貝建構函式或賦值運算子的需求更明顯。如果一個類需要一個解構函式,我們幾乎可以肯定它也需要一個拷貝建構函式和一個拷貝賦值運算子。因為合成解構函式不會delete一個指標資料成員,所以有時候需要定義一個解構函式,但同時如果採用合成拷貝建構函式和合成拷貝運算子時就會出現很多問題,因為合成的方法僅僅拷貝指標的值,而不是拷貝物件。
- 如果一個類需要一個拷貝建構函式,幾乎可以肯定它也需要一個拷貝賦值運算子,反之亦然。但無論需要拷貝建構函式還是拷貝賦值運算子不意味著也需要解構函式。
- 我們可以通過將拷貝控制成員定義為=default 來顯式地要求編譯器生成合成的版本,當我們在類內宣告時,將隱式地宣告為內聯的。如果不希望如此,應該只對成員的類外定義使用=default。
- 對某些類來說,拷貝建構函式和拷貝賦值運算子沒有意義。在此情況下,定義類時必須採用某種機制阻止拷貝或賦值 。例如,iostream類阻止了拷貝,避免多個物件寫入或讀取相同的IO緩衝。為了阻止拷貝,看起來應該不定義拷貝控制成員,但是這種策略恰恰是無效的,如前文所述編譯器會生成合成的版本如果我們沒有定義它們。
- 在新標準下,我們可以通過將拷貝建構函式和拷貝賦值運算子定義為刪除的函式(deleted function) 來阻止拷貝 。刪除的函式是這樣一種函式:我們雖然聲明瞭它們,但不能以任何方式使用它們。在函式的引數列表後面加上=delete 來指出我們希望將它定義為刪除的:
struct Nocopy{ Nocopy() = default; Nocopy(const Nocopy &) =delete;//阻止拷貝 Nocopy& operator= (const Nocopy &) =delete;//阻止賦值 ~Nocopy() = default; }
- =delete必須出現在函式第一次宣告的時候,這與=default不同。我們可以對任何函式指定=delete
- 我們不能 刪除解構函式,對於解構函式已刪除的型別,不能定義該型別的變數或釋放指向該型別動態分配物件的指標,但是可以動態分配這種型別的物件。
- 如果一個類有資料成員不能預設構造,拷貝,複製或銷燬,則對應的成員函式將被定義為刪除的。特別需要注意引用型別成員和const成員,具體規則此處不贅述。
- 在新標準釋出之前,類是通過將拷貝建構函式和拷貝賦值運算子宣告為private 來阻止拷貝的,但現在已經不採用這樣的方法。
- 當編寫賦值運算子時,如果將一個物件賦予它自身,賦值運算子必須正確工作;同時大多數賦值運算子組合了解構函式和拷貝建構函式的工作 。
- 新標準的一個最主要的特性是可以移動而非拷貝物件的能力。很多情況都會發生物件拷貝,在其中某些情況中,物件拷貝後就立即被銷燬了。在這些情況下,移動 而非拷貝物件會大幅度提升效能。
- 為了支援移動操作,新標準引入了一種新的引用型別——右值引用(rvalue reference) ,所謂右值引用就是必須繫結到右值的引用。我們通過&& 而不是& 來獲得右值引用。右值引用有一個重要的性質——只能繫結到一個將要銷燬的物件 。因此,我們可以自由地將一個右值引用的資源“移動”到另一個物件。
- 我們可以將一個右值引用繫結到要求轉換的表示式,字面常量或是返回右值的表示式上,但不能將一個右值引用直接繫結到一個左值上:
int i=42; int &r=i; int &&rr=i;//錯誤 int &r2=i*42;//錯誤:i*42是一個右值 const int &r3=i*42;// 正確:我們可以將一個const引用繫結到一個右值上 int &&rr2=i*42;//正確:將rr2繫結到乘法結果右值上
返回非引用型別的函式,連同算數,關係,位以及後置遞增/遞減運算子,都生成右值。我們可以將一個const的左值引用或將一個右值引用繫結到這類表示式上。
- 右值引用只能繫結到臨時物件 ,所引用的物件將要被銷燬,該物件沒有其它使用者。
- 雖然不能將一個右值引用直接繫結到一個左值上,但我們可以顯式地將一個左值轉換為對應的右值引用型別。我們還可以通過呼叫一個名為move 的新標準庫函式來獲得繫結到左值上的右值引用,此函式定義在標頭檔案utility 中:
int &&rr3=std::move(rr1);
- 為了讓我們自己的型別支援移動操作,需要為其定義移動建構函式和移動賦值運算子。這兩個成員類似對應的拷貝操作,但它們從給定物件“竊取”資源而不是拷貝資源。
- 類似拷貝建構函式,移動建構函式 的第一個引數是該類型別的一個引用,但這個引用引數必須是一個右值引用 。除了完成資源移動,移動建構函式還必須確保移後源物件處於這樣一個狀態——銷燬它是無害的 。特別是,一旦資源完成移動,源物件必須不再指向被移動的資源——這些資源的所有權已經歸屬新建立的物件。
StrVec::StrVec(StrVec &&s) noexcept//移動操作不應丟擲任何異常 :elements(s.elements),first_free(s.first_free),cap(s.cap){ s.elements=s.first_free=s.cap=nullptr; }
移動建構函式不分配任何新記憶體,它接管給定的StrVec中的記憶體。最終,移後源物件會被銷燬,意味著在其上執行解構函式。
- 由於移動操作通常不分配任何資源,所以移動操作通常不會丟擲任何異常 ,noexcept 是我們承諾一個函式不丟擲異常的一種方法,我們必須在類標頭檔案的宣告中和定義中都指定noexcept。
- 移動賦值運算子執行與西溝函式和移動建構函式相同的工作,引數同樣應該是右值引用 型別。
- 在移動操作之後,移後源物件必須保持有效,可析構的狀態,但是使用者不能對其值進行任何假設 。
- 只有當一個類沒有定義任何自己版本的拷貝控制成員,且類的每個非static資料成員都可以移動時 ,編譯器才會為它合成移動建構函式或移動賦值運算子。編譯器可以移動內建型別的成員,如果一個成員是類型別,且該類由對應的移動操作,編譯器也能移動這個成員。
- 如果類定義了一個移動建構函式或一個移動賦值運算子,則該類的合成拷貝建構函式和拷貝賦值運算子會被定義為刪除的,必須自己定義。
- 如果一個類既有移動建構函式,也有拷貝建構函式,編譯器使用普通的函式匹配規則來確定使用哪個建構函式,賦值操作情況類似。
- 除了建構函式和賦值運算子之外,如果一個成員函式同時提供拷貝和移動版本,它也能從中收益,這種允許移動的成員函式使用與拷貝/移動建構函式和賦值運算子相同的引數模式——一個版本接受一個指向const的左值引用,第二個版本接受一個指向非const 的右值引用。
- 通常,我們在一個物件上呼叫成員函式,而不管該物件是一個左值還是右值 :
string s1="a value",s2="another"; auto n =(s1+s2).find('a);
新標準庫類仍然允許向右值賦值,但是,我們可能希望在自己的類中阻止這種用法。在此情況下,我們希望強制左側運算物件(this指向的物件)是一個左值。
我們指出this的左值/右值屬性的方式與定義const成員函式相同,即,在引數列表後面放置一個引用限定符 :
Foo &Foo::operator=(const Foo &rhs) &{ ... }
對於& 限定的函式,我們只能用於左值,對於&& 限定的函式,只能用於右值。
一個函式可以同時用const和引用限定,在此情況下,引用限定符必須跟隨在const後面 。
- 引用限定符也可以區分過載版本,如果一個成員函式有引用限定符,則具有相同引數列表的所有版本都必須有引用限定符。