拷貝控制c++primer13章

TinnCHEN發表於2019-04-18

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來區分一個成員函式的過載版本。

相關文章