C++11中對類(class)新增的特性

大CC發表於2016-03-26

default/delete 控制預設函式

在我們沒有顯式定義類的複製建構函式和賦值操作符的情況下,編譯器會為我們生成預設的這兩個函式:

預設的賦值函式以記憶體複製的形式完成物件的複製。

這種機制可以為我們節省很多編寫複製建構函式和賦值操作符的時間,但是在某些情況下,比如我們不希望物件被複制,

在之前我們需要將複製建構函式和賦值操作符宣告為private,現在可以使用delete關鍵字實現:

class X {
    // …
    X& operator=(const X&) = delete;   // 禁用類的賦值操作符
    X(const X&) = delete;
};

顯式地使用default關鍵字宣告使用類的預設行為,對於編譯器來說明顯是多餘的,但是對於程式碼的閱讀者來說,使用default顯式地定義複製操作,則意味著這個複製操作就是一個普通的預設的複製操作。

override /final 強制重寫/禁止重寫虛擬函式

派生類中可以不實現基類虛擬函式,也可以實現,但不使用virtual關鍵字;

這很容易給人造成混淆,有時為了確認某個函式是否是虛擬函式,我們不得不追溯到基類檢視;

C++11引入了兩個新的識別符號: override和final

override,表示函式應當重寫基類中的虛擬函式。(用於派生類的虛擬函式中)

final,表示派生類不應當重寫這個虛擬函式。(用於基類中)

struct B {
       virtual void f();
       virtual void g() const;
       virtual void h(char);
       void k();      // non-virtual
       virtual void m() final; 
};

struct D : B {
       void f() override;     // OK: 重寫 B::f()
       void g() override;     // error: 不同的函式宣告,不能重寫
       virtual void h(char);  // 重寫 B::h( char ); 可能會有警告
       void k() override;     // error: B::k() 不是虛擬函式
       virtual void m();       // error: m()在基類中宣告禁止重寫
};

有了這對兄弟,我們的虛擬函式用起來更為安全,也更好閱讀;

委託建構函式 Delegating constructors

在C++98中,如果你想讓兩個建構函式完成相似的事情,可以寫兩個大段程式碼相同的建構函式,或者是另外定義一個init()函式,讓兩個建構函式都呼叫這個init()函式。例如:

class X {
        int a;
        // 實現一個初始化函式
        validate(int x) {
            if (0<x && x<=max) a=x; else throw bad_X(x);
        }
    public:
        // 三個建構函式都呼叫validate(),完成初始化工作
        X(int x) { validate(x); }
        X() { validate(42); }
        X(string s) {
            int x = lexical_cast<int>(s); validate(x);
        }
        // …
    };

這樣的實現方式重複羅嗦,並且容易出錯。

在C++0x中,我們可以在定義一個建構函式時呼叫另外一個建構函式:

class X {
        int a;
    public:
        X(int x) { if (0<x && x<=max) a=x; else throw bad_X(x); }
        // 建構函式X()呼叫建構函式X(int x)
        X() :X{42} { }
        // 建構函式X(string s)呼叫建構函式X(int x)
        X(string s) :X{lexical_cast<int>(s)} { }
        // …
    };

繼承的建構函式 Inheriting constructors

C++11提供了將建構函式晉級的能力:

比如以下這個示例,基類提供一個帶引數的建構函式,而派生類沒有提供;

如果直接使用D1 d(6);將會報錯;通過將基類建構函式晉級,派生類中會隱式宣告建構函式D1(int);

需要注意的是,晉級後的基類建構函式是無法初始化派生類的成員變數的,所以如果派生類中有成員變數,

需要使用初始化列表初始化;

struct B1 {
        B1(int) { }
    };
    struct D1 : B1 {
        using B1::B1; // 隱式宣告建構函式D1(int)
        // 注意:在宣告的時候x變數已經被初始化
        int  x{0};
    };
    void test()
    {
        D1 d(6);    // d.x的值是0
    }

類內部成員的初始化 Non-static data member initializers

在C++98標準裡,只有static const宣告的整型成員能在類內部初始化,並且初始化值必須是常量表示式。這些限制確保了初始化操作可以在編譯時期進行。

class X {
    static const int m1 = 7;   // 正確
    const int m2 = 7;    // 錯誤:無static
    static int m3 = 7;              // 錯誤:無const
    static const string m5 = “odd”; //錯誤:非整型
};

C++11的基本思想是,允許非靜態(non-static)資料成員在其宣告處(在其所屬類內部)進行初始化。這樣,在執行時,需要初始值時建構函式可以使用這個初始值。現在,我們可以這麼寫:

class A {
public:
    int a = 7;
};
它等同於使用初始化列表:
class A {
public:
    int a;
    A() : a(7) {}
};

單純從程式碼來看,這樣只是省去了一些文字輸入,但在有多個建構函式的類中,其好處就很明顯了:

class A {
    public:
         A(): a(7), b(5), hash_algorithm(“MD5″),
           s(“Constructor run”) {}
        A(int a_val) :
          a(a_val), b(5), hash_algorithm(“MD5″),
          s(“Constructor run”)
          {}
        A(D d) : a(7), b(g(d)),
            hash_algorithm(“MD5″), s(“Constructor run”)
            {}
        int a, b;
    private:
        // 雜湊加密函式可應用於類A的所有例項
        HashingFunction hash_algorithm;
        std::string s;  // 用以指明物件正處於生命週期內何種狀態的字串
    };

可以簡化為:

class A {
    public:
        A() {}
        A(int a_val) : a(a_val) {}
        A(D d) : b(g(d)) {}
        int a = 7;
        int b = 5;
    private:
        //雜湊加密函式可應用於類A的所有例項
        HashingFunction hash_algorithm{“MD5″};
        //用以指明物件正處於生命週期內何種狀態的字串
        std::string s{“Constructor run”};

多麼優雅!

移動構造和移動賦值

在C++98中,我們自定義的類,會預設生成拷貝賦值操作符函式和拷貝賦值函式以及解構函式;

在C++11中,依賴於新增的move語義,預設生成的函式多了2個移動相關的:移動賦值操作符( move assignment )和移動建構函式( move constructor );

BS建議,如果你顯式宣告瞭上述 5 個函式或操作符中的任何一個,你必須考慮其餘的 4 個,並且顯式地定義你需要的操作,或者使用這個操作的預設行為。

一旦我們顯式地指明( 宣告 , 定義 , =default , 或者 =delete )了上述五個函式之中的任意一個,編譯器將不會預設自動生成move操作。

一旦我們顯式地指明( 宣告 , 定義 , =default , 或者 =delete )了上述五個函式之中的任意一個,編譯器將預設自動生成所有的拷貝操作。但是,我們應該儘量避免這種情況的發生,不要依賴於編譯器的預設動作。

如果你宣告瞭上述 5 個預設函式中的任何一個,強烈建議你顯式地宣告所有這 5 個預設函式。例如:

template<class T>
class Handle {
    T* p;
public:
    Handle(T* pp) : p{pp} {}
    // 使用者定義建構函式: 沒有隱式的拷貝和移動操作
        ~Handle() { delete p; }
    Handle(Handle&& h) :p{h.p}
        { h.p=nullptr; }; // transfer ownership
    Handle& operator=(Handle&& h)
        { delete p; p=h.p; h.p=nullptr; } // 傳遞所有權
    Handle(const Handle&) = delete;  // 禁用拷貝建構函式
    Handle& operator=(const Handle&) = delete;
    // ...
};

參考

http://www.stroustrup.com/C++11FAQ.html

https://www.chenlq.net/books/cpp11-faq

相關文章