C++ 構造函數 explicit 關鍵字 成員初始化列表

通常,構造函數具有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詳解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章