1. 程式人生 > >c++ primer第五版----學習筆記(十三)Ⅰ

c++ primer第五版----學習筆記(十三)Ⅰ

知識點:

拷貝控制操作:拷貝、移動、賦值和銷燬

在定義一個類時,我們可以顯式或隱式的定義在此型別的物件拷貝、賦值、移動、銷燬是做什麼,主要通過五種特殊的成員函式來完成這些操作:拷貝建構函式、拷貝複製運算子、移動建構函式、移動複製運算子和解構函式

1.拷貝建構函式

  • 定義: 一個建構函式的第一個引數是自身型別的引用,且任何額外引數都有預設值
  • 拷貝建構函式在幾種情況下都會被隱式使用,因此拷貝建構函式通常不應該是explicit的
  • 如果我們未自定義拷貝建構函式,則編譯器為我們定義一個,稱為合成拷貝建構函式(一般情況下,合成拷貝建構函式將其引數的成員逐個拷貝到正在建立的物件中)
  • 每個成員的型別決定了它的拷貝方式,對於類型別,將呼叫其拷貝建構函式進行拷貝;對於內建型別,則會直接拷貝;對於陣列的拷貝是逐個元素的拷貝,若陣列的元素是類型別,則使用拷貝建構函式來拷貝
  • 直接初始化與拷貝初始化:
string s(dots);          //直接初始化
string s2 = dots;        //拷貝初始化
  • 拷貝初始化發生的情況:
/*1.用=定義變數時發生
  2.將一個物件作為實參傳遞給一個非引用型別的形參
  3.從一個返回型別為非引用型別的函式返回一個物件
  4.用花括號列表初始化一個數組中的元素或一個聚合類中的成員*/
  • 拷貝初始化的限制:
vector<int> v1(10);        //直接初始化
vector<int> v2 = 10;       //錯誤:接受大小引數的建構函式是explicit的
void f(vector<int>);       //f的引數進行拷貝初始化
f(10);                    //錯誤:不能用一個explicit的建構函式拷貝一個實參
f(vector<int> 10);       //從一個int直接構造一個臨時vector

2.拷貝賦值運算子

  • 賦值運算子就是一個名為operator=的函式;類似其他函式,運算子函式也有一個返回型別和一個引數列表
class Foo {    
public:
    Foo& operator=(const Foo&);  //賦值運算子
    //...
};

  • 賦值運算子通常應該返回一個指向其左側運算物件的引用
  • 標準庫通常要求儲存在容器中的型別要具有賦值運算子,且其返回值是左側運算物件的引用
  • 合成拷貝賦值運算子:將右側運算物件的每個非static成員賦予左側運算物件的對應成員

3.解構函式

  • 解構函式:類的一個成員函式,名字由波浪號接類名構成
class Foo {    
public:
    ~Foo();  //解構函式
    //...
};

  • 作用: 釋放物件使用的資源,並銷燬物件的非static資料成員  (成員按初始化順序的逆序銷燬)
  • 銷燬類型別的成員需要執行自己的解構函式; 隱式銷燬一個內建指標型別的成員不會delete它所指向的物件
  • 呼叫解構函式:
/*1.變數在離開其作用域時被銷燬
  2.當一個物件被銷燬時,其成員被銷燬
  3.容器被銷燬時,其元素被銷燬
  4.對於動態分配的物件,當對指向它的指標應用delete運算子時被銷燬
  5.對於臨時物件,當建立它的完整表示式結束時被銷燬*/

//當指向一個物件的引用或指標離開作用域時,解構函式不會執行
  • 合成解構函式:函式體為空

4.三/五法則和使用=default

  • 如果一個類需要自定義解構函式,幾乎可以肯定它也需要自定義拷貝賦值運算子和拷貝建構函式
  • 需要拷貝操作的類也需要賦值操作,反之亦然
  • 當在類內用=default修飾成員的宣告時,合成的函式將隱式地宣告為內聯的
  • 只能對具有合成版本的成員函式使用=default(即,預設建構函式或拷貝控制成員)

5.阻止拷貝i:

  • 通過將拷貝建構函式和拷貝賦值運算子定義為刪除的函式來阻止拷貝:
struct NoCopy{
    //....
    NoCopy(const NoCopy&) = delete;        //阻止拷貝
    NoCopy &operator=(const NoCopy&) = delete;  //阻止賦值
    //....
};

//可以對任何函式指定=delete
  • 對於一個刪除了解構函式的型別,編譯器將不允許定義該型別的變數或建立該類的臨時物件
  • 對於解構函式已刪除的型別,不能定義該型別的變數或釋放指向該型別動態分配物件的指標
  • 本質上,當不可能拷貝、賦值或銷燬類的成員時,類的合成拷貝控制成員就被定義為刪除的

6.拷貝控制和資源管理

  • 管理類外資源的類必須定義拷貝控制成員;一旦一個類需要解構函式,那麼它幾乎肯定也需要一個拷貝建構函式和一個拷貝賦值運算子
  • 為了定義拷貝控制成員,可以定義拷貝操作,使類的行為看起來像一個值或者像一個指標
  • 類的行為像一個值,拷貝發生時,副本和原物件是完全獨立的,改變副本不會對原物件產生影響
  • 類的行為像一個指標,拷貝發生時,副本和原物件公用底層資料,改變副本也會改變原物件
  • 標準庫容器和string類的行為像一個值,shared_ptr類就是像值的類,IO類和unique_ptr不允許拷貝和賦值,所以不是
  • 行為像值的類:
/*1.類值版本,可能需要動態分配其成員的副本
  2.賦值運算子通常組合了解構函式和建構函式的操作;首先銷燬左側運算物件的資源,再從右側運算子物件拷貝物件
  3.對於存在這樣一個順序,所以我們必須保證這樣的拷貝賦值運算子是正確的;所以先將右側運算子物件拷貝到一個臨時物件中,再銷燬左側的運算物件的現有成員,之後將臨時物件中的資料成員拷貝至左側物件中(防範自賦值的情況發生—首先就銷燬了自身的成員,再進行拷貝自身則會訪問到已經釋放的記憶體中)*/
  • 行為像指標的類:
/*1.定義行為像指標的類,在希望直接管理資源的情況使用自己設計的引用計數來判斷是否釋放記憶體
  2.除了初始化物件外,每個建構函式還要建立一個引用計數用來記錄共享狀態
  3.拷貝建構函式不分配新的計數器,而是拷貝給定物件的資料成員,包括計數器;拷貝建構函式遞增計數器
  4.解構函式遞減計數器,指出共享狀態的使用者少了一個;如果計數器變為0,則解構函式釋放狀態
  5.拷貝賦值運算子遞增右側運算物件的計數器,遞減左側運算物件的計數器*/

7.swap函式

  • 如果類定義了自己的swap,那麼演算法將使用類自定義版本
  • 在進行兩個物件交換的時候,我們不希望進行新的記憶體分配,只希望將其指標進行拷貝賦值,省去不必要的記憶體分配,可以定義自己的swap函式
  • 與拷貝控制成員不同,swap並不是必要的。但是,對於分配了資源的類,定義swap可能是一種很重要的優化手段
  • 在賦值運算子中使用swap,以傳值的方式傳入新物件,再進行拷貝賦值,在一定程度上比較安全

8.拷貝控制示例與動態記憶體管理類

對於這兩節,書中的Message和Folder類,StrVec類一定要自己親手寫一遍並且理解程式的每個功能。。。

9.物件移動

  • 在重新分配記憶體的過程中,從舊元素將元素拷貝到新記憶體是不必要的,更好的方式是移動元素;標準庫容器、string、shared_ptr既支援移動也支援拷貝,IO類和unique_ptr類可以移動但不能拷貝
  • 右值引用:必須繫結到右值的引用,通過&&來獲得右值引用;只繫結到一個將要銷燬的物件,因此,我們可以自由的將右值引用的資源“移動”到另一個物件中
  • 一個右值引用本質上也只是一個物件的另外一個名字而已。對於常規的引用:稱之為左值引用,我們不能將其繫結到所要轉換的表示式,而右值引用可以繫結:
int i = 42;             
int &r = i;                    //正確:r引用i
int &&rr = i;                  //錯誤:不能將一個右值引用繫結到一個左值上
int &r2 = i * 42;              //錯誤:i * 42是一個右值
const int &r3 = i * 42;        //正確:我們可以將一個const的引用繫結到一個右值上
int &&rr2 = i * 42;            //正確:rr2繫結到乘法結果上
  • 變數是左值,因此我們不能將一個右值引用直接繫結到一個變數上,即使這個變數是右值引用型別也不行
  • 標準庫定義了一個名為move的函式,可以顯式地將一個左值轉換為對應的右值引用型別,該函式定義在標頭檔案utility中,使用時應該使用std::move
int &&rr3  = std::move(rr1);   //顯式轉換
  • 可以銷燬一個移後源物件,也可以賦予新值,但不能使用一個源物件的值

10.移動建構函式和移動賦值運算子

  • 移動建構函式:與拷貝建構函式類似,不過第一個引數為一個右值引用;一旦資源被移動,源物件對移動之後的資源已經不再有控制權,最後源物件會被銷燬。注意,移動建構函式是“竊取”資源,並不會分配資源。
  • 不丟擲異常的移動建構函式和移動賦值運算子必須標記為noexcept-----新標準引入,必須在標頭檔案的宣告中和定義中都指定noexcept
class StrVec;
StrVec(StrVec &&s)  noexcept;
  • 通過將移後源物件的指標成員置為nullptr來確保移後源物件可析構
  • 只有當一個類沒有定義任何自己版本的拷貝控制成員,且它的所有資料成員都能移動構造或移動賦值時,編譯器才會為它合成移動建構函式或移動賦值運算子;定義了一個移動建構函式或移動賦值運算子的類必須也定義自己的拷貝操作,不然這些成員預設地定義為刪除的
  • 移動右值,拷貝左值,但如果沒有移動建構函式,右值也被拷貝
class Foo {
public:
	Foo() = default;
	Foo(const Foo&);       //拷貝建構函式
     //其他成員定義,但Foo未定義移動建構函式
};
Foo x;
Foo y(x);                       //拷貝建構函式;x是一個左值
Foo z(std::move(x));            //拷貝建構函式,因為未定義移動建構函式
  • 所有五個拷貝控制成員應該在類中一起出現!
  • 移動賦值運算子必須銷燬左側運算物件的舊狀態

11.右值引用和成員函式

  • 區分拷貝和移動的過載函式通常有一個版本接收const T &(左值引用),一個版本接受T&&(右值引用引數)——引用限定符,這樣我們呼叫函式時,實參的型別決定了新元素是進行拷貝還是移動操作
void push_back(const string&);        //拷貝元素
void push_back(string&&);             //移動元素
  • 在函式的引數列表之後加上&或者&&表示該函式只能用於左值或者右值
Foo sorted() &&;           //可用於改變的右值
Foo sorted() const &;      //可用於任何型別的Foo
  • 當我們定義兩個或兩個以上的具有同名且引數列表相同的成員函式,必須對所有函式都加上引用限定符或者都不加