C++ 建構函式 explicit 關鍵字 成員初始化列表

永不停转發表於2024-03-19

通常,建構函式具有public可訪問性,但也可以將建構函式宣告為 protected 或 private。建構函式可以選擇採用成員初始化表示式列表,該列表會在建構函式主體執行之前初始化類成員。與在建構函式主體中賦值相比,初始化類成員是更高效的方式。首選成員初始化表示式列表,而不是在建構函式主體中賦值。

注意

  1. 成員初始化表示式的引數可以是建構函式引數之一、函式呼叫或 std::initializer_list
  2. const 成員和引用型別的成員必須在成員初始化表示式列表中進行初始化。
  3. 若要確保在派生建構函式執行之前完全初始化基類,需要在初始化表示式中初始化化基類建構函式。
class Box {
public:
    // Default constructor
    Box() {}

    // Initialize a Box with equal dimensions (i.e. a cube)
    explicit Box(int i) : m_width(i), m_length(i), m_height(i) // member init list
    {}

    // Initialize a Box with custom dimensions
    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height)
    {}

    int Volume() { return m_width * m_length * m_height; }

private:
    // Will have value of 0 when default constructor is called.
    // If we didn't zero-init here, default constructor would
    // leave them uninitialized with garbage values.
    int m_width{ 0 };
    int m_length{ 0 };
    int m_height{ 0 };
};

派生建構函式執行之前完全初始化基類

class Box {
public:
    Box(int width, int length, int height){
       m_width = width;
       m_length = length;
       m_height = height;
    }

private:
    int m_width;
    int m_length;
    int m_height;
};

class StorageBox : public Box {
public:
    StorageBox(int width, int length, int height, const string label&) : Box(width, length, height){
        m_label = label;
    }
private:
    string m_label;
};

建構函式可以宣告為 inline、explicit、friend 或 constexpr。可以顯式設定預設複製建構函式、移動建構函式、複製賦值運算子、移動賦值運算子和解構函式。

class Box2
{
public:
    Box2() = delete;
    Box2(const Box2& other) = default;
    Box2& operator=(const Box2& other) = default;
    Box2(Box2&& other) = default;
    Box2& operator=(Box2&& other) = default;
    //...
};

一、預設建構函式

如果類中未宣告建構函式,則編譯器提供隱式 inline 預設建構函式。編譯器提供的預設建構函式沒有引數。如果使用隱式預設建構函式,須要在類定義中初始化成員。

class Box {
public:
    int Volume() {return m_width * m_height * m_length;}
private:
    // 如果沒有這些初始化表示式,成員會處於未初始化狀態,Volume() 呼叫會生成垃圾值。
    int m_width { 0 };
    int m_height { 0 };
    int m_length { 0 };
};

如果宣告瞭任何非預設建構函式,編譯器不會提供預設建構函式。如果不使用編譯器生成的建構函式,可以透過將隱式預設建構函式定義為已刪除來阻止編譯器生成它。

class Box {
public:
    // 只有沒宣告建構函式時此語句有效
    Box() = delete;
    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height){}
private:
    int m_width;
    int m_length;
    int m_height;

};
int main(){

    Box box1(1, 2, 3);
    Box box2{ 2, 3, 4 };
    Box box3; // 編譯錯誤 C2512: no appropriate default constructor available
    Box boxes[3]; // 編譯錯誤 C2512: no appropriate default constructor available
    Box boxes[3]{ { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; // 正確
}

二、顯式建構函式

如果類的建構函式只有一個引數,或是除了一個引數之外的所有引數都具有預設值,則會發生隱式型別轉換。

class Box {
public:
    Box(int size): m_width(size), m_length(size), m_height(size){}
private:
    int m_width;
    int m_length;
    int m_height;

};
class ShippingOrder
{
public:
    ShippingOrder(Box b, double postage) : m_box(b), m_postage(postage){}

private:
    Box m_box;
    double m_postage;
}
int main(){
    Box b = 42; // 隱式型別轉換
    ShippingOrder so(42, 10.8); // 隱式型別轉換
}

explicit關鍵字可以防止隱式型別轉換的發生。explicit只能用於修飾只有一個引數的類建構函式,表明該建構函式是顯示的而非隱式的。

  1. explicit關鍵字的作用就是防止類建構函式的隱式自動轉換。
  2. 如果類建構函式引數大於或等於兩個時, 不會產生隱式轉換的, explicit關鍵字無效。
  3. 例外, 就是當除了第一個引數以外的其他引數都有預設值的時候, explicit關鍵字依然有效。
  4. explicit只能寫在在宣告中,不能寫在定義中。

三、複製建構函式

從 C++11 中開始,支援兩類賦值:複製賦值和移動賦值。賦值操作和初始化操作都會導致物件被複制。

賦值:將一個物件的值分配給另一個物件時,第一個物件將複製到第二個物件。
初始化:在宣告新物件、按值傳遞函式引數或從函式返回值時,將發生初始化。

編譯器預設會生成複製建構函式。如果類成員都是簡單型別(如標量值),則編譯器生成的複製建構函式已夠用。 如果類需要更復雜的初始化,則需要實現自定義複製建構函式。例如,如果類成員是指標,編譯器生成的複製建構函式只是複製指標,以便新指標仍指向原記憶體位置。

複製建構函式宣告方式如下:

    Box(Box& other); // 儘量避免這種方式,這種方式允許修改other
    Box(const Box& other); // 儘量使用這種方式,它可防止複製建構函式意外更改複製的物件。
    Box(volatile Box& other);
    Box(volatile const Box& other);

    // 後續引數必須要有預設值
    Box(Box& other, int i = 42, string label = "Box");

    Box& operator=(const Box& x);

定義複製建構函式時,還應定義複製賦值運算子 (=)。如果不宣告覆制賦值運算子,編譯器將自動生成複製賦值運算子。如果只宣告覆制建構函式,編譯器自動生成複製賦值運算子;如果只宣告覆制賦值運算子,編譯器自動生成複製建構函式。 如果未定義顯式或隱式移動建構函式,則原本使用移動建構函式的操作會改用複製建構函式。 如果類宣告瞭移動建構函式或移動賦值運算子,則隱式宣告的複製建構函式會定義為已刪除。

阻止複製物件時,需要將複製建構函式宣告為delete。如果要禁止物件複製,應該這樣做。

  Box (const Box& other) = delete;

三、移動建構函式

當物件由相同型別的另一個物件初始化時,如果另一物件即將被毀且不再需要其資源,則編譯器會選擇移動建構函式。 移動建構函式在傳遞大型物件時可以顯著提高程式的效率。

#include "MemoryBlock.h"
#include <vector>

using namespace std;

int main()
{
   // vector 類使用移動語義,透過移動向量元素(而非複製它們)來高效地執行插入操作。
   vector<MemoryBlock> v;
  // 如果 MemoryBlock 沒有定義移動建構函式,會按照以下順序執行
  // 1. 建立物件 MemoryBlock(25)
  // 2. 複製 MemoryBlock 給push_back
  // 3. 刪除 MemoryBlock 物件
   v.push_back(MemoryBlock(25));
  // 如果 MemoryBlock 有移動建構函式,按照以下順序執行
  // 1. 建立物件 MemoryBlock(25)
  // 2. 執行push_back時會呼叫移動建構函式,直接使用MemoryBlock物件而不是複製
   v.push_back(MemoryBlock(75));

}

建立移動建構函式

  1. 定義一個空的建構函式,建構函式的引數型別為右值引用;
  2. 在移動建構函式中,將源物件中的類資料成員新增到要構造的物件;
  3. 將源物件的資料成員置空。 這可以防止解構函式多次釋放資源(如記憶體)。
MemoryBlock(MemoryBlock&& other)
   : _data(nullptr)
   , _length(0)
{
    _data = other._data;
    _length = other._length;
    other._data = nullptr;
    other._length = 0;
}

建立移動賦值運算子

  1. 定義一個空的賦值運算子,該運算子引數型別為右值引用,返回一個引用型別;
  2. 防止將物件賦給自身;
  3. 釋放目標物件中所有資源(如記憶體),將資料成員從源物件轉移到要構造的物件;
  4. 返回對當前物件的引用。
MemoryBlock& operator=(MemoryBlock&& other)
{
    if (this != &other)
    {
        delete[] _data;
        _data = other._data;
        _length = other._length;

        other._data = nullptr;
        other._length = 0;
    }

    return *this;
}

如果同時提供了移動建構函式和移動賦值運算子,則可以編寫移動建構函式來呼叫移動賦值運算子,從而消除冗餘程式碼。

MemoryBlock(MemoryBlock&& other) noexcept
   : _data(nullptr)
   , _length(0)
{
   *this = std::move(other);
}

四、委託建構函式

委託建構函式就是呼叫同一類中的其他建構函式,完成部分初始化工作。 可以在一個建構函式中編寫主邏輯,並從其他建構函式呼叫它。委託建構函式可以減少程式碼重複,使程式碼更易於瞭解和維護。

class Box {
public:
    // 預設建構函式
    Box() {}

    // 建構函式
    Box(int i) :  Box(i, i, i)  // 委託建構函式
    {}

    // 建構函式,主邏輯
    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height)
    {}
};

注意:不能在委託給其他建構函式的建構函式中執行成員初始化

class class_a {
public:
    class_a() {}
    // 成員初始化,未使用代理
    class_a(string str) : m_string{ str } {}

    // 使用代理時不能在此初始化成員,否則會出現以下錯誤
    // error C3511: a call to a delegating constructor shall be the only member-initializer
    class_a(string str, double dbl) : class_a(str) , m_double{ dbl } {}

    // 其它成員正確的初始化方式
    class_a(string str, double dbl) : class_a(str) { m_double = dbl; }

    double m_double{ 1.0 };
    string m_string;
};

注意:建構函式委託語法能迴圈呼叫,否則會出現堆疊溢位。

class class_f{
public:
    int max;
    int min;

    // 這樣做語法上允許,但是會在執行時出現堆疊溢位
    class_f() : class_f(6, 3){ }
    class_f(int my_max, int my_min) : class_f() { }
};

五、繼承建構函式

派生類可以使用 using 宣告從直接基類繼承建構函式。一般而言,當派生類未宣告新資料成員或建構函式時,最好使用繼承建構函式。如果基類的建構函式具有相同簽名,則派生類無法從多個基類繼承。

#include <iostream>
using namespace std;

class Base
{
public:
    Base() { cout << "Base()" << endl; }
    Base(const Base& other) { cout << "Base(Base&)" << endl; }
    explicit Base(int i) : num(i) { cout << "Base(int)" << endl; }
    explicit Base(char c) : letter(c) { cout << "Base(char)" << endl; }

private:
    int num;
    char letter;
};

class Derived : Base
{
public:
    // 從基類 Base 繼承全部建構函式
    using Base::Base;

private:
    // 基類建構函式無法初始化該成員
    int newMember{ 0 };
};

int main()
{
    cout << "Derived d1(5) calls: ";
    Derived d1(5);
    cout << "Derived d1('c') calls: ";
    Derived d2('c');
    cout << "Derived d3 = d2 calls: " ;
    Derived d3 = d2;
    cout << "Derived d4 calls: ";
    Derived d4;
}

/* Output:
Derived d1(5) calls: Base(int)
Derived d1('c') calls: Base(char)
Derived d3 = d2 calls: Base(Base&)
Derived d4 calls: Base()*/

類别範本可以從型別引數繼承所有建構函式:

template< typename T >
class Derived : T {
    using T::T;   // declare the constructors from T
    // ...
};

建構函式執行順序

  1. 按宣告順序呼叫基類和成員建構函式。
  2. 如果類派生自虛擬基類,則會將物件的虛擬基指標初始化。
  3. 如果類具有或繼承了虛擬函式,則會將物件的虛擬函式指標初始化。 虛擬函式指標指向類中的虛擬函式表,確保虛擬函式正確地呼叫繫結程式碼。
  4. 執行自己函式體中的所有程式碼。

如果基類沒有預設建構函式,則必須在派生類建構函式中提供基類建構函式引數

下面程式碼,首先,呼叫基建構函式。 然後,按照在類宣告中出現的順序初始化基類成員。 最後,呼叫派生建構函式。

#include <iostream>

using namespace std;

class Contained1 {
public:
    Contained1() { cout << "Contained1 ctor\n"; }
};

class Contained2 {
public:
    Contained2() { cout << "Contained2 ctor\n"; }
};

class Contained3 {
public:
    Contained3() { cout << "Contained3 ctor\n"; }
};

class BaseContainer {
public:
    BaseContainer() { cout << "BaseContainer ctor\n"; }
private:
    Contained1 c1;
    Contained2 c2;
};

class DerivedContainer : public BaseContainer {
public:
    DerivedContainer() : BaseContainer() { cout << "DerivedContainer ctor\n"; }
private:
    Contained3 c3;
};

int main() {
    DerivedContainer dc;
}

輸出如下:
Contained1 ctor
Contained2 ctor
BaseContainer ctor
Contained3 ctor
DerivedContainer ctor

參考文章:
建構函式 (C++)
QT學習記錄(008):explicit 關鍵字的作用
C++中的explicit詳解

相關文章