這五種操作:構造(包括移動),賦值(包括移動),析構其實就是定義了對一個對象進行構造,賦值,析構時的行為。理解這些行為並不復雜,復雜的是理解在繼承下這些行為的表現。需要注意的是他們並不會被繼承(傳統意義上的繼承)。
拷貝構造函數
形式:
class Foo{
public:
Foo(); //默認構造函數
Foo(const Foo&); //拷貝構造函數
};
如果一個構造函數的第一個參數是自身類類型的引用,且任何額外參數都有默認值,則此構造函數是拷貝構造函數。引用是必須的(否則會出現無限循環),const是通常的。不應是explicit(傳參,返回都是隱式)。
如果我們沒有為一個類定義一個拷貝構造函數,編譯器會為我們定義一個。合成版本會逐個按位拷貝非static成員,如果是類類型,機會調用類的拷貝構造函數。
拷貝賦值運算符
形式:
class Foo(){
public:
Foo& operator=(const Foo& ); //賦值雲運算符
};
這本質上是運算符重載,重載運算符本質上是函數,其名字由operator關鍵字后接表示要定義的運算符的符號。因此,賦值運算符就是一個名為operator=的函數。類似於其他任何函數,運算符函數也有一個返回類型和一個參數列表。
重載運算符的參數表示運算符的運算對象。賦值運算符,必須定義為成員函數。如果一個運算符是一個成員函數,其左側運算對象就綁定到隱式的this參數。右側參數作為參數傳遞。為了與內置類型賦值保持一致,賦值運算符通常返回一個指向其左側運算對象的引用。
與處理拷貝構造函數一樣,如果一個類未定義自己的拷貝賦值運算符,編譯器會為它生成一個合成拷貝運算符。就是將右側運算對象的每個非static成員賦值於左側運算對象的對應成員,這一功作主要是通過成員類型的拷貝賦值運算符來完成的。
析構函數
形式:
class Foo{
public:
~Foo(); //析構函數
};
由於析構函數不接受參數,因此它不能被重載。對於一個給定的類,只會有唯一一個析構函數。
如同構造函數有一個初始化部分和一個函數體,析構函數也有一個函數體和一個析構部分。在一個構造函數中,成員初始化是在函數體執行之前完成的,且按照他們在類中出現的順序進行初始化。在一個析構函數中,首先執行函數體,然后銷毀成員。成員按照初始化順序的逆序銷毀。
在對象最后一次使用之后,析構函數的函數體可執行類設計者希望執行的任何收尾工作。通常析構函數釋放對象在生存期分配的所有資源。
在一個析構函數中,不存在類似構造函數中初始化列表的東西來控制成員如何銷毀,析構部分是隱式的。成員銷毀時發生什么完全依賴於成員的類型。銷毀類類型的成員需要執行成員自己的析構函數。內置類型沒有析構函數,因此銷毀內置類型成員什么也不需要做。
隱式銷毀一個內置指針類型的成員不會delete它所指向的對象。
與普通指針不同,智能指針是類類型,所以具有析構函數。因此,與普通指針不同,智能指針成員在析構階段會被自動銷毀。
何時會調用析構函數?
無論何時一個對象被銷毀,就會自動調用其析構函數。
變量在離開作用域時被銷毀。
當一個對象被銷毀時,其成員被銷毀。
容器被銷毀時,其元素被銷毀
對於動態分配的對象,當其指向它的指針應用delete運算符時被銷毀。
對於臨時對象,當創建它的完整表達式結束時被銷毀。
析構函數自動運行,我們的程序可以按需求分配資源,無需擔心何時釋放這些資源。
當一個類未定義自己的析構函數時,編譯器會為它定義一個合成的析構函數。合成的析構函數體為空。如下:
class Sales_data{
public:
~Sales_data(){}
};
在析構函數執行完畢后,成員會被自動銷毀。特別的,string的析構函數會被調用,它將釋放bookNo成員所用的內存。
認識到析構函數體本身並不直接銷毀成員是非常重要的。成員是在析構函數體之后隱含的析構階段中被銷毀的。在整個對象銷毀過程中,析構函數體是作為成員銷毀步驟之外的另一部分而進行的。
移動構造函數
形式:
class StrVec{
public:
StrVec(StrVec&& s) noexcept:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
s.elements=s.first_free=s.cap=nullptr;
}
};
類似拷貝構造函數,移動構造函數的第一個參數是該類類型的一個引用,不同於構造函數的是,這個引用參數在移動構造函數中是一個右值引用。與拷貝構造函數一樣,任何額外的參數都必須有默認實參。
除了完成資源移動,移動構造函數還必須確保移動后源對象處於這樣一種狀態,銷毀它是無害的.特別是,一旦資源完成移動,源對象必須不在指向被移動的資源,這些資源的所有權已經歸屬新創建的對象。
由於移動操作“竊取”資源,它並不分配資源。因此,移動操作不會拋出任何異常。用noexcept告知標准庫我們的移動構造函數不會拋出異常,因此標准庫減少了未處理拋出異常這種可能性而做的額外的工作。在一個構造函數中,noexcept出現在參數列表和初始化列表開始的冒號之間。聲明與定義都必須指定noexcept。不拋出異常的移動構造函數和移動賦值運算符必須標記為noexcept。
移動賦值運算符
形式:
class StrVec{
public:
StrVec& operator=(StrVec&& rhs)noexcept{
if(this!=(&rhs)){
free() //釋放已有元素
elements=rhs.elements; //從rhs接管資源
first_free=rhs.first_free;
cap=rhs.cap;
rhs.elements=rhs.first_free=rhs.cap=nullptr;
}
return *this;
}
};
我們檢查this指針與rhs的地址是否相同。如果相同,右側和左側運算對象指向相同的對象,我們不需要做任何事情。否則,我們釋放左側運算對象所使用的內存,並接管給定對象的內存。與移動構造函數一樣,我們將rhs中的指針職位nullptr。
我們費盡心機的檢查自賦值情況可能有些奇怪。畢竟,移動賦值運算符需要右側運算對象的一右值。我們進行檢查的原因是次此右值可能是move調用的返回結果。與其他任何賦值運算符一樣,關鍵點是我們不能在使用右側運算對象的資源之前就釋放左側運算對象的資源(可能是相同資源)。
從一個對象移動數據並不會銷毀此對象,但有時在移動操作完成后,源對象會被銷毀。因此 ,當我們編寫一個移動操作時,必須確保移動后源對象進入一個可析構狀態。我們的StrVec的移動操作滿足這一要求,就是通過將以后源對象的指針成員設為nullptr實現的。
除了將移動后源對象置為析構安全的狀態后,移動操作還必須保證對象仍然是有效的。也就是說可以安全的為其賦予新值,或可以安全的使用而不依賴於當前的值。另一方面,移動操作后對源對象留下的值沒有任何要求。因此我們的程序不應依賴於移后源對象中的值。
如果一個類定義了自己的拷貝構造函數,拷貝賦值運算符或析構函數,編譯器就不會為它合成移動構造函數和移動賦值運算符了。只有當一個類沒有定義任何版本的拷貝控制成員,且類的每個非static成員都可移動時,編譯器才會為它合成移動構造函數和移動賦值運算符。編譯器可以移動內置類型的成員,如果一個成員是類類型,且該類有對應的移動操作,編譯器也能移動這個成員。
需要析構函數的類也需要拷貝和賦值操作
需要拷貝操作的類也需要賦值操作,反之亦然
使用=default
將拷貝控制成員定義為=default,來現實的要求編譯器生成合成的版本。
class Foo{
public:
Foo()=default;
Foo(const Foo& )=default;
Foo& operator=(const Foo& );
~Foo()=default;
};
Foo& Foo::operator=(const Foo& )=default;
當我們在類內用=default修飾成員的聲明時,合成的函數隱式的聲明為內聯的。如果我們不希望合成的構造函數是內聯的,應該只對成員的類外定義使用=default.就像對拷貝賦值運算符所做的那樣。
我們只能對具有合成版本的成員函數使用=default(即,默認構造函數或拷貝控制成員)。
阻止拷貝
為了阻止拷貝,看起來可能應該不定義拷貝控制成員,但是,這種策略是無效的,如果我們的類為定義這些操作,編譯器會為他們合成新的版本。
我們可以將拷貝構造函數和賦值運算符定義為刪除的函數來阻止拷貝,刪除的函數看起來是這樣一種函數:我們雖然聲明了他們,但我們不能使用他們。在函數的參數列表后面加上=delete來指出我們希望它定義為刪除的。
class NoCopy{
public:
NoCopy()=default; //使用合成的默認構造函數
NoCopy(const NoCopy& )=delete; //阻止拷貝
NoCopy& operator=(const NoCopy& )=delete; //阻止拷貝
~NoCopy()=delete; //使用合成的默認構造函數
};
與=default不同。=delete必須出現在函數第一次聲明的時候。我們可以對任何函數指定=delete(我們只能對編譯器可以合成的默認構造函數或拷貝控制成員使用=default).析構函數不能是刪除的成員。如果析構函數被刪除,就無法銷毀此類型的對象了,對於一個刪除了析構函數的類型,編譯器將不允許定義該類型的變量或創建該類型的臨時對象,沒有定義創建就可以。對於析構函數已經刪除的類型,不能定義該類型的變量或釋放指向該類型動態分配對象的指針。


在繼承中基類的析構函數是虛函數,派生類繼承的是虛屬性,而不是析構函數。
