第12課 特殊成員函數的生成機制

一. 特殊成員函數ios

(一)概述編程

  1. 特殊成員函數指C++會自行生成的成員函數,主要有6種:默認構造函數、析構函數、複製構造函數、複製賦值函數、移動構造函數和移動賦值函數函數

  2. 默認生成的特殊成員函數都具備public訪問權限且是inline的非虛函數(除析構例外)。一般,若是這些函數不被相關代碼使用,編譯器不會爲其產生真正的函數代碼this

  3. 若是基類析構函數是虛函數,則編譯器爲派生類生成的析構函數也是個虛函數。spa

  4. 默認的複製或移動都是指對非靜態成員的操做,它們都是「按成員複製」或「按成員移動」的而「按成員移動」實際上更像是按成員移動的請求,由於有些類型不具有移動操做,對於這些對象會經過其複製操做來實現「移動」。日誌

(二)C++11中的生成機制code

 

  1. 默認構造函數:僅當類中不包含用戶聲明的構造函數時(含無參/帶參構造函數、複製構造和移動構造)才生成。對象

  2. 析構函數:僅當基類的析構函數爲virtual時,派生類的析構函數纔是虛的。其與C++98的機制基本相同。惟一區別在於析構函數默認爲noexcept。blog

  3. 複製構造函數:生成條件(該類未聲明覆制構造;②該類未聲明移動操做;③該類未聲明覆制賦值或析構函數)。注意,在當前聲明覆制賦值或析構函數時,仍會生成默認的複製構造函數,但該行爲會被逐漸廢棄。資源

  4. 複製賦值函數:生成條件(① 該類未聲明覆制賦值;②該類未聲明移動操做;③該類未聲明覆制構造或析構函數)。注意,在當前聲明覆制構造或析構函數時,仍會生成默認的複製賦值函數,但該行爲會被逐漸廢棄。

  5. 移動構造和移動賦值函數:僅當類中不包含用戶聲明的複製操做、移動操做和析構函數3個條件知足時才生成。

2、理解生成機制

(一)生成機制背後的思想

  1. 複製操做是彼此獨立的,即聲明瞭其中一個,並不會阻止編譯器生成另外一個。

    早期編譯器認爲默認的複製構造和複製賦值都是按成員複製的。所以,它仍願意提供這種最簡單的「淺拷貝」方式以供調用(如遇到成員變量是引用或常量時,就能夠有機會提示報錯信息)。而若是須要進行「深拷貝」則須由用戶自行定義相應的函數。

  2. 移動操做並不彼此獨立,即聲明瞭其中一個,就會阻止編譯器生成另外一個。

    假設若是聲明一個移動構造函數,實際上表示移動操做與編譯器生成的按默認「按成員移動」是不一樣的。而若是「按成員移動」操做不符合要求時,按成員進行的移動賦值也極及可能不符合要求。

  3. 複製操做與移動操做會相互影響

    ①若是聲明覆制操做(不管是複製構造或複製賦值)的行爲,這代表對象按成員複製不符合要求。編譯器認爲,既然按成員複製不符合要求,那麼「按成員移動」也很可能不符合要求。

    ②反之,若是聲明瞭移動操做(移動構造或移動賦值),就表示「按成員移動」不符合要求,也就沒理由指望按成員複製符合要求。

(二)大三律及推論

  1. 大三律(Rule of Three)若是聲明瞭複製構造、複製賦值或析構函數中的任何一個,就得同時聲明全部這三個

    ①在一種複製操做中進行資源管理,也極有可能在另外一種複製操做中也須要進行。

    ②該類的析構函數也會參與到該資源的管理中(釋放之)。

  2. 推論

    ①若是聲明的析構函數,則平凡的按成員複製不適於該類。即複製操做不會被自動生成,由於它們的行爲都是不正確的。但在C++11中仍保留生成複製操做函數,僅僅是出於兼容C++98的遺留代碼而己,這種行爲將逐漸被廢棄。

    ②若是聲明瞭析構函數就不會生成移動操做(由於析構函數會抑制複製操做,而複製操做又會阻止生成移動操做)。

(三)注意事項

  1. 爲了消除因相互抑制而產生的依賴關係,在C++11中能夠經過=default來顯式指示編譯器生成默認版本的特殊成員函數。

  2. 成員函數模板在任何狀況下都不會抑制特殊成員函數的生成。

  3. 在己經顯式聲明析構函數時,會阻止生成移動操做,而但仍會生成複製操做(將逐漸被廢棄)。這可能會產生反作用,即原來移動操做可能會變成複製操做

  4. 根據大三律原則,當聲明析構函數時,通常應同時提供複製操做和移動操做函數。

【編程實驗】特殊成員函數生成機制

#include <iostream>
#include <map>

using namespace std;

//1. 演示特殊成員函數之間的抑制關係
//1.1 自定義析構函數會阻止默認移動操做(含移動構造和移動賦值)
class Foo
{
private:
    int i;
public:
    Foo() = default;
    Foo(const Foo& f) = delete;    //刪除複製構造函數!讓移動操做只會去找移動操做的函數,而不是被複製取代!
    Foo& operator=(const Foo&) = delete; //理由同上!

    ~Foo() {};  //這裏自定義析構函數!
};

//1.2 自定義析構函數使「移動操做」變爲「複製操做」!
class StringTable
{
    std::map<int, std::string> values;
    
    void makeLogEntry(std::string s){}; //日誌記錄
public:
    StringTable()
    {
        makeLogEntry("Creating StringTable Object");    
    }
    
    ~StringTable()   //注意,這裏自定義了析構函數,會產生反作用!
    {
        makeLogEntry("Destroying StringTable Object");
    }
};

//2. 大三律
class Bar
{
public:
    virtual ~Bar() = default;  //自定義了析構函數,則應同時提供複製和移動操做!
    Bar(Bar&&) = default;     //提供移動操做的支持
    Bar& operator=(Bar&&) = default;

    Bar(const Bar&) = default;//提供移動操做的支持
    Bar& operator=(const Bar&) = default;
};

//3. 特種成員函數模板
class Widget
{
public:
    template<typename T>
    Widget(const T& rhs) { cout <<"Widget(const T& rhs)" << endl; }

    template<typename T>
    Widget& operator=(const T& rhs) 
    { 
        cout << "Widget& operator=(const T& rhs)" << endl;
        return *this; 
    }
    
public:
    Widget() = default;    
};

int main()
{
    //1. 自定義析構函數,會阻止移動操做
    Foo f;
    //Foo f1 = std::move(f);   //編譯失敗! 因爲自定義了析構函數,默認移動操做被刪除。
    //f1 = std::move(w);       //編譯失敗! 緣由同上。
    
    StringTable st1;
    StringTable st2 = std::move(st1); //這裏本意是調用移動構造函數,但因該類的析構函數阻止生成
                                      //默認的移動操做,轉而變爲複製操做。而對於values對象的復
                                      //制,而td::map<int, std::string>的複製每每比移動慢不少!

    //3. 特種成員函數模板不會抑制編譯器生成默認的特殊成員函數
    Widget w;
    Widget w1 = w; //調用編譯器生成的默認複製構造函數
    Widget w2 = 2; //調用函數模板:Widget(const T& rhs)
    
    w1 = w2; //調用編譯器生成的默認複製賦值函數
    w1 = 2;  //調用函數模板:Widget& operator=(const T& rhs)

    return 0;
}
/*輸出結果
Widget(const T& rhs)
Widget& operator=(const T& rhs)
*/
相關文章
相關標籤/搜索