拷貝控制c++primer13章

1、一個類通過定義五種特殊的成員函數來啊控制這些操作:拷貝構造函數、拷貝賦值運算符、移動構造函數、移動賦值運算符、析構函數

2、拷貝構造函數:如果一個構造函數的第一個參數是自身類類型的引用,且任何額外參數都有默認值,則此構造函數是拷貝構造函數。且通常不爲explicit的,eg:

class foo{
public:
   foo();
   foo(const foo&);//拷貝構造函數
};

參數爲引用的原因:若爲非引用參數,則我們需要調用實參的拷貝來初始化一個對象,在調用實參的拷貝時我們又需要進行拷貝會陷入死循環。

3、合成拷貝構造函數:如果我們沒有定義一個拷貝構造函數,即使我們定義了其他構造函數,編譯器也會定義一個合成拷貝構造函數。
編譯器從給定對象中依次將每個非static成員拷貝到正在創建的對象中。

4、每個成員的類型決定了其如何拷貝
類類型:使用其拷貝構造函數來拷貝;
內置類型:直接拷貝;
數組:逐一拷貝一個數組類型成員;
若一個數組元素是類類型:使用元素的拷貝構造函數來拷貝。

5、拷貝初始化不僅在用=時發生,下列情況也會:
將一個對象作爲實參傳遞給一個非引用類型的形參;
從一個返回類型爲非引用類型的函數返回一個對象;
用花括號列表初始化一個數據中的元素或一個聚合類中的成員;
某些類類型還會對它們所分配的對象使用拷貝初始化,eg:容器調用insert、push。

6、即使編譯器可以繞過拷貝構造函數,但在拷貝點上,拷貝構造函數必須是存在並且可以訪問的。

7、拷貝賦值運算符:一個名爲operator=的函數,返回=左側對象的引用。

8、合成拷貝賦值運算符:未定義時,編譯器自行定義。

9、析構函數:析構函數釋放對象使用的資源,並銷燬對象的非static數據成員。在一個析構函數中,首先執行函數體,然後銷燬成員。成員按初始化順序的逆序銷燬。注意在一個析構函數中不存在類似構造函數中初始化列表的東西來控制成員如何銷燬,析構部分是隱式的。成員銷燬時發生什麼完全依賴於成員的類型。

10、調用析構函數的時間
變量在離開其作用域時被銷燬;
當一個對象被銷燬時,其成員被銷燬;
容器(無論是標準庫容器還是數組)被銷燬時,其元素被銷燬;
對於動態分配的對象,當對指向它的指針應用delete運算符時被銷燬;
對於臨時對象,當創建它的完整表達式結束時被銷燬。

11、當指向一個對象的引用或指針離開作用域時,析構函數不執行。

12、三/五法則
需要析構函數的類也需要拷貝構造函數和拷貝賦值函數;
需要拷貝操作的類也需要賦值操作,反之亦然;
析構函數不能是刪除的,對於定義了刪除的析構函數的類,編譯器將不允許定義該類型的變量或創建該類型的變量或臨時對象;
如果一個類有刪除的或不可訪問的析構函數,那麼其默認和拷貝構造函數會被定義爲刪除的;
如果一個類有const或引用成員,則不能使用合成的拷貝賦值操作;
新標準加入了移動構造函數因此對三/五法則更新如下:
所有五個拷貝控制成員應該看作一個整體:一般來說,如果一個類定義了任何一個拷貝操作,他就應該應以所有五個操作。

13、類的行爲像一個值,意味着它應該也有自己的狀態。當拷貝一個像值的對象時,副本和原對象是完全獨立的。改變副本不會對原對象有任何影響,反之亦然。爲了提供類值的行爲,對於類管理的資源,每個對象都應該擁有一份自己的拷貝。

14、行爲像指針的類共享狀態。當拷貝一個這種類的對象時,副本和原對象使用相同的底層數據。改變副本也會改變原對象,反之亦然。

15、定義行爲像值的類時,對於拷貝賦值運算符來說,其一般結合了析構函數和拷貝構造函數的操作,所以我們要保證,即使將一個對象賦予其自身,我們也要保證能夠安全運行,所以要注意析構和拷貝的順序。因此,一個好的模式是,先將右側運算對象拷貝到一個局部臨時對象中,當拷貝完成後,銷燬左側運算對象。

16、令一個類展現類似指針的行爲最好的辦法是用sahred_ptr。但當我們想要直接管理資源時,我們應該引入引用計數,並將其保存在動態內存中。

17、引用計數的工作方式:
(1)除了初始化對象外,每個構造函數(拷貝構造函數除外)還要創建一個引用計數,用來記錄有多少對象與正在創建的對象共享狀態。當我們創建一個對象時,只有一個對象共享狀態,因此將引用計數初始化爲1。
(2)拷貝構造函數不分配新的計數器,而是拷貝給定對象的數據成員,包括計數器。拷貝構造函數遞增共享的計數器,指出給定對象的狀態又被一個新用戶所共享。
(3)析構函數遞減計數器,指出共享狀態的用戶又少了一個。如果計數器變爲0,則析構函數釋放狀態。
(4)拷貝賦值運算符遞增右側運算對象的計數器,遞減左側運算對象的計數器。如果左側運算對象的計數器變爲0,意味着它的共享狀態沒有用戶了,拷貝賦值運算符就必須銷燬狀態。

19、swap函數在類中比較微妙,對於內置類型,由於內置類型沒有自己定義的swap,因此會調用std::swap,但對於一個類的成員有自己特定的swap函數,調用std::swap是錯誤的,儘管編譯會通過。eg:

//假定我們有一個Foo類,他有一個類型爲HasPtr的成員h,HasPtr定義了自己的swap
//我們採用如下操作就是錯誤的
void swap(Foo &lhs, Foo &rhs){
     std::swap(lhs.h, rhs.h);
}
//我們希望調用HasPtr的swap,正確方案如下:
void swap(Foo &lhs, Foo &rhs){
     using std::swap;  //此處儘管顯示的用use聲明,但沒有隱藏HasPtr版本swap的聲明
     swap(lhs.h, rhs.h);
}

20、定義swap的類通常用swap來定義它們的賦值運算符。這些運算符使用了叫做(copy and swap)的操作。這種技術將左側運算對象與右側運算對象的一個副本進行了交換。

//注:HasPtr爲一個自己定義的類似shared_ptr的類
HasPtr& Hasptr::operator=(HasPtr rhs){
//將一個對象以傳值的方式傳遞給了賦值運算符(有可能是拷貝也有可能是移動構造)
       swap(*this, rhs); //rhs 現在指向本對象曾經食用的內存
       return *this;   //rhs被銷燬,從而delete了rhs中的指針。
}

上例中的賦值運算符從底層效率來看並不理想:
個人理解爲:即使他可以處理左值或者右值引用,但也需要等實參的類型確定了才能決定具體採用哪種做法,即是否需要對實參進行拷貝,可以進一步分爲兩種情況即將參數定義爲const &或者&&這樣就可以直接精準匹配。

21、右值引用:右值引用只能綁定到一個將要銷燬的對象。(也解釋了unique_ptr的特性)右值引用可以綁定到要求轉換的表達式、字面常量、返回右值的表達式。左值引用正好相反。

返回左值引用的函數,聯通複製、下標、解引用和前置遞增/減都是返回左值的表達式。

返回非引用類型的函數,連同算術、關係、位以及後置遞增/減都生成右值。我們可以將一個const&或者&&綁定到這類表達式上。

因爲右值引用只能綁定在臨時對象所以我們可知,該引用的對象將要被銷燬,並且該對象沒有其他用戶,也就意味着,右值引用可以自由地接管所引用的對象的資源。也叫做“竊取”狀態。

22、移動構造函數參數爲右值引用。並且還需要保證移動後對象處於一個可以安全被銷燬的狀態。一旦資源移動完成,原對象不再繼續指向該資源。該過程不分配任何新的內存,這點與拷貝構造函數不同,也大大提升了效率。

23、我們需要一個移動操作不拋出異常,是因爲兩個互相關聯的事實:首先,雖然移動操作通常不拋出異常,但拋出異常也是允許的。其次,標準庫容器能對異常發生時其自身行爲提供保障。例如我們定義了一個vector的移動版本的push_back,假設當我們在需要分配新的空間時,將舊空間內的元素移動到新的空間時發生了異常,注意此時有部分移動過去的元素在舊空間中已經不存在,有部分元素還未移動,此使我們無法保證vector的穩定性。因此我們要事先聲明不會又異常發生即:noexcept。

24、建議不要隨便使用移動操作:由於一個移後源對象具有不確定狀態,對其調用std::move是危險的,當我們調用move時,必須確認移後源對象沒有其他用戶。通過在類代碼中小心地使用move,可以大幅度提升新能。

25、就標準中,我們允許向字符串s1,s2的連接結果賦值,新標準爲了兼容仍然允許此方法,但我們爲了斌面這樣的操作,在參數列表後放置一個引用限定符。eg:

//
s1 + s2 = "wow";
//
Foo &operator=(const Foo&) &;//只能向可更改的左值賦值

引用限定符可以爲&或者&&分別表示this指向的是一個左值還是右值,只能用於非static成員函數。可以與const一起用即:

Foo someMem() const &;

26、可以用引用限定符來區分重載版本(與const一樣)。可以綜合引用限定符和const來區分一個成員函數的重載版本。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章