目錄結構:ios
定義一個類,會顯式或隱式指定此類型的對象拷貝、移動、賦值和銷燬時作什麼。類經過定義五種特殊的成員函數來控制這些操做,包括:拷貝構造函數(copy constructor)、拷貝賦值運算符(copy-assignment operator)、移動構造函數(move constructor)、移動賦值運算符(move-assignment operator)和析構函數(destructor)。
其中拷貝構造和移動構造器定義了當用同類型的另外一個對象初始化本對象時的行爲。而拷貝賦值和移動賦值定義了將一個對象賦予同類型的另外一個對象時的行爲。析構函數定義了此類型對象銷燬時作什麼。這些統稱爲拷貝控制操做。
對於沒有顯式定義這些成員的,編譯器會自動定義默認的版本。一些類必需要本身定義拷貝控制成員,另一些則不須要。因此,什麼時候須要本身去定義就考察程序員的功底了。git
移動語義是C++11新引入的,事後再談。程序員
僅有一個參數爲自身類類型引用的構造函數就是拷貝構造函數,形如:github
class Foo{ public: Foo(); //默認構造函數 Foo(const Foo&); //拷貝構造函數 }
該參數必須是引用類型,通常是const引用。因爲拷貝構造函數會在幾種狀況下隱式地調用,因此通常不是explicit。
若是本身不定義,編譯器就會合成一個默認的(合成拷貝構造函數)。合成的拷貝構造函數會把參數成員逐個拷貝到正在建立的對象中(非static成員)。
成員的類型決定了拷貝的方式:類類型的成員會用它本身的拷貝構造函數來拷貝;內置類型則直接值拷貝。數組會逐個複製,若是數組成員是類類型,會逐個調用成員自己的拷貝構造函數。算法
class Sales_data{ public: Sales_data(const Sales_data&); private: std::string bookNo; int units_sold = 0; double revenue = 0.0; }; //定義Sales_data的拷貝構造函數,與Sales_data的合成拷貝構造函數等價 Sales_data::Sales_data(const Sales_data &orig) : bookNo(orig.bookNo), //使用string的拷貝構造函數 units_sold(orig.units_sold), //拷貝orig.units_sold revenue(orig.revenue) //拷貝orig.revenue {} //空函數體
拷貝初始化和直接初始化的差別:
數組
string dots(10,','); //直接初始化 string s(dots); //直接初始化 string s2 = dots; //拷貝初始化 string null_book = "9-999-99999-9"; //拷貝初始化 string nines = string(100, '9'); //拷貝初始化
拷貝初始化通常由拷貝構造函數完成,之因此說通常是由於移動語義的引入,致使若是類由移動構造函數時,拷貝初始化有時會使用移動構造函數而非拷貝構造函數。
拷貝初始化不只在用=定義變量時發生,在下列情形也會發生:
將一個對象做爲實參傳遞給一個非引用類型的形參
從一個返回類型爲非引用類型的函數返回一個對象
用花括號列表初始化一個數組中的元素或一個聚合類中的成員
某些類類型還會對它們所分配的對象使用拷貝初始化。如初始化標準庫容器或調用其insert或push成員時,容器會對其元素進行拷貝初始化。而emplace建立的元素都是直接初始化。安全
拷貝構造函數被用來初始化非引用類類型參數,因此拷貝構造函數自身的參數必須是引用類型。否則的話,就兩者矛盾而無限循環了。
爲了調用拷貝構造函數,咱們必須拷貝它的實參,但爲了拷貝實參,咱們又須要調用拷貝構造函數。函數
Sales_data trans, accum; trans = accum; //使用Sales_data的拷貝賦值運算符
若是類未定義,編譯器會合成一個(合成拷貝賦值運算符)。
這個函數的定義涉及了重載運算符的概念,這裏重載的是賦值運算符。
重載運算符本質上是函數,名字由operator關鍵字接要定義的運算符符號組成。因此,賦值運算符就對應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; //返回左側對象的引用 }
與構造執行的操做相反。
析構函數名字比構造函數多了一個~。沒有返回值,也沒有參數。
class Foo{ public: ~Foo(); //析構函數 ... };
析構函數不能被重載,是唯一的。
調用析構的時機:
變量在離開做用域時被銷燬
當一個對象被銷燬時,其成員被銷燬
容器被銷燬時(標準庫容器或數組),其元素被銷燬
動態分配的對象,當對指向它的指針應用delete時被銷燬
臨時對象,當建立它的完整表達式結束時被銷燬
{//新做用域 //p和p2指向動態分配對象 Sales_data *p = new Sales_data;//p是一個內置指針 auto p2 = make_shared<Sales_data>(); //p2是一個shared_ptr Sales_data item(*p); //拷貝構造函數將*p拷貝到item中 vector<Sales_data> vec; //局部對象 vec.push_back(*p2); //拷貝p2指向的對象 delete p; //對p指向的對象執行析構函數 }//退出局部做用域;對item、p2和vec調用析構函數 //銷燬p2會遞減其引用計數;若是引用計數變爲0,則對象釋放 //銷燬vec會銷燬它的元素
若是類未定義析構,則編譯器會自動合成(合成析構函數)。
class Sales_data{ public: //成員會被自動銷燬,除此以外不須要作其餘事情 ~Sales_data(){} //其餘成員的定義 ... };
析構函數體(空)執行完畢後,成員會被自動銷燬。本例中string的析構函數會被調用,釋放bookNo的內存。析構函數體自己不直接銷燬成員,它們是在函數體以後隱含的析構階段中被銷燬的。析構函數體只是析構過程的一部分。
這裏解釋一下三五法則(分別是Three Rule,Five Rule)。Three Rule指的是定義的類有拷貝構造函數,拷貝賦值運算符,和析構函數。而Five Rule就是除了前面的三種,還有移動賦值運算符,移動構造函數。
這裏是一個Five Rule的案例:
class rule_of_five { char* cstring; // raw pointer used as a handle to a dynamically-allocated memory block public: rule_of_five(const char* s = "") : cstring(nullptr) { if (s) { std::size_t n = std::strlen(s) + 1; cstring = new char[n]; // allocate std::memcpy(cstring, s, n); // populate } } ~rule_of_five() { delete[] cstring; // deallocate } rule_of_five(const rule_of_five& other) // copy constructor : rule_of_five(other.cstring) {} rule_of_five(rule_of_five&& other) noexcept // move constructor : cstring(std::exchange(other.cstring, nullptr)) {} rule_of_five& operator=(const rule_of_five& other) // copy assignment { return *this = rule_of_five(other); } rule_of_five& operator=(rule_of_five&& other) noexcept // move assignment { std::swap(cstring, other.cstring); return *this; } // alternatively, replace both assignment operators with // rule_of_five& operator=(rule_of_five other) noexcept // { // std::swap(cstring, other.cstring); // return *this; // } };
更詳細的能夠查看:https://en.cppreference.com/w/cpp/language/rule_of_three
須要析構函數的類也須要拷貝和賦值操做。
由於析構函數須要去手工delete成員指針。這種狀況下,編譯器合成的拷貝構造和賦值運算符就會有問題,由於僅僅只是完成了淺拷貝,拷貝了成員指針的地址值,這可能引發問題。因此這種狀況咱們要本身寫深拷貝代碼。
須要拷貝操做的類也須要賦值操做,反之亦然
由於語義上拷貝構造和賦值操做是一致的,只是調用時機不一樣。提供了一個就說明須要特化某些操做,那麼對應的另外一個也要一致。但須要兩者卻不必定須要一個析構。
=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 &Sales_data::operator=(const Sales_data&) = default;
類內使用=default聲明,合成的函數會隱式地聲明爲inline。
=delete
有些狀況咱們但願阻止類的拷貝或賦值。好比iostream就阻止了拷貝,避免多個對象寫入或讀取相同的IO緩衝。
struct NoCopy{ NoCopy() = default; //合成的默認構造函數 NoCopy(const NoCopy&) = delete; //阻止拷貝 NoCopy& operator=(const NoCopy&) = delete; //阻止賦值 ~NoCopy() = default; };
=delete通知編譯器,不但願定義這些成員。
注意,析構函數不能刪除,其餘任何函數均可以指定=delete。雖然語法上容許析構函數指定=delete,但這樣一來涉及到該類的對象都不能用,由於它沒法銷燬。
因此,記着析構函數不能加=delete這條軟規則便可。
若是一個類有數據成員不能默認構造、拷貝、複製或銷燬,那麼對應的成員函數將被定義爲刪除的。這就意味着,composite模式的數據成員自身殘疾將影響整個團隊殘疾。
具備引用成員或沒法默認構造的const成員的類,編譯器不會合成默認構造函數。若是類有const成員,則它不能使用合成的拷貝賦值運算符(新值是不能給const對象的)。
在沒有=delete以前,C++是經過private權限限制拷貝構造函數和拷貝賦值運算符來阻止拷貝的。這種方法有一個疏漏,就是友元函數和成員函數是能夠進行拷貝的。
類一旦管理了類外資源,每每就須要自定義析構,根據三五法則也就意味着要自定義拷貝構造和拷貝賦值運算符。
而定義拷貝控制成員時,首先要肯定類的拷貝語義,咱們是讓類的行爲看起來像值仍是像指針。
若是是像值,好比string、標準庫容器類等,它們的拷貝會使得副本對象和原對象徹底獨立,改變副本不會影響原對象。
若是是像指針,好比shared_ptr,那麼拷貝的就是指針,指向的是同一個對象。
固然,也能夠設置爲不容許拷貝或賦值,此時既不像值也不像指針。
行爲像值的類
class HasPtr{ public: HasPtr(const std::string &s = std::string()):ps(new std::string(s)), i(0){} //對ps指向的string,每一個HasPtr對象都有本身的拷貝 HasPtr(const HasPtr &p):ps(new std::string(*p.ps)), i(p.i){} HasPtr& operator=(const HasPtr &); ~HasPtr(){delete ps;} private: std::string *ps; int i; }; HasPtr& HasPtr::operator=(const HasPtr &rhs) { //這裏必定要先new再delete,由於賦值操做賦值給本身是合法的 //若是賦值給本身,先delete意味着rhs.ps就丟了 auto newp = new string(*rhs.ps); //拷貝底層string delete ps; //釋放舊內存 ps = newp; //從右側運算對象拷貝數據到本對象 i = rhs.i; return *this; //返回本對象 }
賦值運算符要謹記一個好習慣,在銷燬左側運算對象資源以前先拷貝右側運算對象資源。
行爲像指針的類
class HasPtr{ public: //構造函數分配新的string和新的計數器,將計數器置爲1 HasPtr(const std::string &s = std::string()):ps(new std::string(s)), i(0), use(new std::size_t(1)){} //拷貝構造函數拷貝全部3個數據成員,並遞增計數器 HasPtr(const HasPtr &p):ps(p.ps), i(p.i), use(p.use){++*use;} HasPtr& operator=(const HasPtr&); ~HasPtr(); private: std::string *ps; int i; std::size_t *use; //用來記錄有多少個對象共享*ps的成員 }; HasPtr::~HasPtr() { if(--*use == 0){ //若是引用計數變爲0 delete ps; //釋放string內存 delete use; //釋放計數器內存 } } HasPtr& HasPtr::operator=(const HasPtr &rhs) { ++*rhs.use; //遞增右側運算對象的引用計數 if(--*use == 0){ //而後遞減本對象的引用計數 delete ps; //若是沒有其餘用戶 delete use; //釋放本對象分配的成員 } ps = rhs.ps; //將數據從rhs拷貝到本對象 i = rhs.i; use = rhs.use; return *this; //返回本對象 }
賦值運算符要考慮自賦值的狀況,因此在左側遞減引用計數以前先遞增右側引用計數。
除了拷貝控制成員外,管理資源的類通常還定義一個swap函數。對與重排元素順序的算法一塊兒使用的類來講,swap很是重要,由於這些算法交換兩個元素時會調用swap。
若是類本身定義了swap,算法就使用自定義版本,不然使用標準庫定義的swap。
class HasPtr{ friend void swap(HasPtr&, HasPtr&)); //其餘成員定義 ... }; inline void swap(HasPtr &lhs, HasPtr &rhs) { using std::swap; swap(lhs.ps, rhs.ps); // 交換指針,而不是string數據 swap(lhs.i, rhs.i); // 交換int成員 }
swap不是必要的,但對分配了資源的類來講,定義swap是一種很重要的優化手段。
swap定義的一個坑:
//Foo有類型爲HasPtr的成員h void swap(Foo &lhs, Foo &rhs) { //錯誤:這個函數使用了標準庫版本的swap,而不是HasPtr版本 std::swap(lhs.h, rhs.h); // 交換類型Foo的其餘成員 } //正確的寫法: void swap(Foo &lhs, Foo &rhs) { using std::swap; swap(lhs.h, rhs.h); //使用HasPtr版本的swap //交換類型Foo的其餘成員 }
這種未加限定的寫法之因此可行,本質上是由於類型特定的swap版本匹配程度優於聲明的std::swap版本。而對std::swap的聲明可使得在找不到類型特定版本時能夠正確的找到std中的版本。
swap經常使用於賦值運算符,它能夠一步到位完成拷貝並交換的技術。
//注意rhs是按值傳遞的,意味着HasPtr的拷貝構造函數將右側運算對象中的string拷貝到rhs HasPtr& HasPtr::operator=(HasPtr rhs) { //交換左側運算對象和局部變量rhs的內容 swap(*this, rhs); //rhs如今指向本對象曾經使用的內存 return *this; //rhs被銷燬,從而delete了rhs中的指針 }
這裏的參數不是引用,右側運算對象是值傳遞,因此rhs是右側運算對象的副本。所以直接swap就一步到位了,自動銷燬rhs時就自動銷燬了原對象(執行析構)。
使用拷貝和交換的賦值運算符天生異常安全,且能正確處理自賦值。
C++11引入了一個特性:能夠移動而非拷貝對象。移動而非拷貝對象會大幅度提高性能。
舊版本即便在沒必要拷貝對象的狀況下,也不得不拷貝,對象若是巨大,那麼拷貝的代價是昂貴的。在舊版本的標準庫中,容器所能保存的類型必須是可拷貝的。但在新標準中,能夠用容器保存不可拷貝,但可移動的類型。
標準庫容器、string和shared_ptr類既支持移動也支持拷貝。IO類和unique_ptr類能夠移動但不能拷貝。
爲了支持移動操做,C++11引入了一個新的引用類型——右值引用(rvalue reference)。所謂右值引用就是必須綁定到右值的引用。經過&&來得到右值引用(左值引用是經過&)。右值引用只能綁定到一個將要銷燬的對象。所以,才得以自由地將一個右值引用的資源轉移給另外一個對象。
int i = 42; int &r = i; //正確:r引用i,r是左值引用 int &&rr = i; //錯誤:右值引用不能綁定到左值上 int &r2 = i*42; //錯誤:i*42是右值 const int &r3 = i*42; //正確:能夠將一個const引用綁定到一個右值上 int &&rr2 = i*42; //正確:將rr2綁定到乘法結果上
最特別的就是const左值引用是能夠綁定到右值的。
變量表達式都是左值,因此不能將一個右值引用直接綁定到一個變量上,即便這個變量的類型是右值引用也不行。
int &&rr1 = 42; //正確:字面常量是右值 int &&rr2 = rr1; //錯誤:表達式rr1是左值
左值是持久的,右值是短暫的。
雖然右值引用不能綁定到左值,但能夠顯式地將左值轉換爲對應的右值引用類型。調用move函數能夠得到綁定在左值上的右值引用,此函數定義在頭文件utility中。
int &&rr3 = std::move(rr1); //正確
move告訴編譯器:咱們有一個左值,但咱們但願像一個右值同樣處理它。但使用move就意味着承諾:除了對rr1賦值或銷燬它外,咱們將再也不使用它。
能夠銷燬一個移後源對象,也能夠賦予它新值,但不能使用移後源對象的值。
調用move函數的代碼應該使用std::move而非move,這樣作能夠避免潛在的名字衝突。
移動構造函數相似拷貝構造,第一個參數是該類類型的引用。不一樣於拷貝構造函數,這個引用參數在移動構造函數中是一個右值引用。其餘任何額外參數都必須有默認值(與拷貝構造一致)。
除了完成資源移動,移動構造函數還要保證移後源對象處於一個狀態:銷燬它是無害的。移動以後,源對象必須再也不指向被移動的資源,這些資源歸新對象全部。
StrVec::StrVec(StrVec &&s) noexcept //移動構造不該該拋任何異常 //成員初始化器接管s中資源 : elements(s.elements), first_free(s.first_free), cap(s.cap) { //令s進入這樣一個狀態————對其運行析構函數是安全的 s.elements = s.first_free = s.cap = nullptr; }
移動構造函數不會分配任何新內存,它接管給定的StrVec的內存。接管以後,源對象的指針置nullptr。
移動操做一般不分配任何資源,所以移動操做一般不拋出任何異常。而經過noexcept能夠通知標準庫構造函數不會拋出異常,若是不通知,那麼標準庫會認爲移動構造函數可能會拋出異常,爲此會作一些額外的工做。
爲何要指出移動操做不拋出異常呢?由於標準庫能對異常發生時其自身的行爲提供保證,好比vector保證push_back時發生異常不會改變vector自己。
之所不異常時不改變vector,是由於拷貝構造函數中發生異常時,舊元素的內存空間是沒有變化的,至於新內存空間儘管發生了異常,vector能夠直接釋放新分配的內存(還沒有成功構造)並返回,這不會影響vector原有的元素。但移動語義就不一樣,若是移動了部分元素時發生了異常,那麼這時源元素就已經被改變了,這就沒法知足自身保持不變的要求了。
因此除非vector知道元素類型的移動構造函數不會拋異常,不然在從新分配內存時,它必須使用拷貝構造而不是移動構造。基於此,若是但願vector從新分配內存時可使用自定義類型對象的移動操做而不是拷貝操做,那就要顯式的聲明咱們的移動構造函數是noexcept的。
相似移動構造,若是不拋出任何異常,也要標記爲noexcept。
StrVec &StrVec::operator=(StrVec &&rhs) noexcept { //直接檢測自賦值 if(this != &rhs){ free(); //釋放已有元素 elements = rhs.elements; /從rhs接管資源 first_free = rhs.first_free; cap = rhs.cap; //將rhs置於可析構狀態 rhs.elements = rhs.first_free = rhs.cap = nullptr; } return *this; }
若是本身不定義,編譯器也會自動合成移動操做,但這和拷貝操做不一樣,它須要一些條件。
若是一個類定義了本身的拷貝構造函數、拷貝賦值運算符或者析構函數,編譯器就不會合成移動操做。
只有當一個類沒有定義任何本身版本的拷貝控制成員,且類的每一個非static數據成員均可以移動時,編譯器纔會爲它合成移動構造和移動賦值運算符。
//編譯器爲X和hasX合成移動操做 struct X{ int i; //內置類型能夠移動 std::string s; //string定義了本身的移動操做 }; struct hasX{ X mem; //X有合成的移動操做 }; X x, x2 = std::move(x); //使用合成的移動構造函數 hasX hx, hx2 = std::move(hx); //使用合成的移動構造函數
與拷貝操做不一樣,移動操做永遠不會被隱式定義爲刪除的函數。但若是顯式地要求編譯器生成=default的移動操做,且編譯器不能移動所有成員,則移動操做會被定義爲刪除的函數。
定義了移動構造或移動賦值的類也必須定義本身的拷貝操做,不然拷貝操做默認被定義爲刪除的。
若是類既有移動構造,也有拷貝構造,那麼編譯器使用普通的函數匹配規則來肯定使用哪一個構造函數。賦值也相似。
StrVec v1, v2; v1 = v2; //v2是左值,使用拷貝賦值 StrVec getVec(istream &s); //getVec返回一個右值 v2 = getVec(cin); //getVec(cin)是一個右值,使用移動賦值
若是類有拷貝構造,但沒有移動構造,函數匹配規則會保證該類型的對象會被拷貝:
class Foo{ public: Foo() = default; Foo(const Foo&); ... }; Foo x; Foo y(x); //拷貝構造函數,x是左值 Foo z(std::move(x)); //拷貝構造函數,由於未定義移動構造函數
在未定義移動構造的情境下,Foo z(std::move(x)之因此可行,是由於咱們能夠把Foo&&轉換爲一個const Foo&。
五個拷貝控制成員應該當成一個總體來對待。若是一個類須要任何一個拷貝操做,它就應該定義全部五個操做。
C++11標準庫定義了移動迭代器(move iterator)適配器。一個移動迭代器經過改變給定迭代器的解引用運算符的行爲來適配此迭代器。移動迭代器的解引用運算符返回一個右值引用。調用make_move_iterator函數能將一個普通迭代器轉換成移動迭代器。原迭代器的全部其餘操做在移動迭代器中都照常工做。
最好不要在移動構造函數和移動賦值運算符這些類實現代碼以外的地方隨意使用move操做。std::move是危險的。
在非static成員函數的形參列表後面添加引用限定符(reference qualifier)能夠指定this的左值/右值屬性。引用限定符能夠是&或者&&,分別表示this能夠指向一個左值或右值對象。引用限定符必須同時出如今函數的聲明和定義中。
class Foo { public: Foo &operator=(const Foo&) &; // 只能向可修改的左值賦值 // 其餘成員 }; Foo &Foo::operator=(const Foo &rhs) & { // 執行將rhs賦予本對象所需的工做 return *this; }
一個非static成員函數能夠同時使用const和引用限定符,此時引用限定符跟在const限定符以後。
class Foo { public: Foo someMem() & const; // error Foo anotherMem() const &; // ok };
引用限定符也能夠區分紅員函數的重載版本。
class Foo { public: Foo sorted() &&; // 可用於可改變的右值 Foo sorted() const &; // 可用於任何類型的Foo //Foo其餘成員 private: vector<int> data; }; //本對象爲右值,所以能夠原址排序 Foo Foo::sorted() && { sort(data.begin(), data.end()); return *this; } //本對象是const或是一個左值,哪一種狀況咱們都不能對其進行原址排序 Foo Foo::sorted() const &{ Foo ret(*this); sort(ret.data.begin(), ret.data.end()); return ret; } retVal().sorted(); //retVal()是右值,調用Foo::sorted() && retFoo().sorted(); //retFoo()是左值,調用Foo::sorted() const &
若是定了兩個或兩個以上具備相同名字和相同參數列表的成員函數,要麼都加引用限定符,要麼都不加,這一點不受const this的影響。
class Foo { public: Foo sorted() &&; Foo sorted() const; // 錯誤:必須加上引用限定符 // Comp是函數類型的類型別名 // 此函數類型能夠用來比較int值 using Comp = bool(const int&, const int&); Foo sorted(Comp*); // 正確:不一樣的參數列表 Foo sorted(Comp*) const; //正確:兩個版本都沒有引用限定符 };
原文連接:
https://r00tk1ts.github.io/2018/11/29/C++%20Primer%20-%20%E6%8B%B7%E8%B4%9D%E6%8E%A7%E5%88%B6/