建構函式 (C++)

若要自訂類別初始化其成員的方式,或在建立類別的物件時叫用函式,請定義建 構函式 。 建構函式的名稱與類別的名稱相同,但沒有傳回值。 您可以視需要定義多個多載建構函式,以各種方式自訂初始化。 建構函式通常具有公用協助工具,讓類別定義或繼承階層以外的程式碼可以建立 類別的物件。 但您也可以將建構函式宣告為 protectedprivate

建構函式可以選擇性地取得成員初始化運算式清單。 初始化類別成員的方式比在建構函式主體中指派值更有效率。 下列範例顯示具有三個多載建構函式的類別 Box 。 最後兩個使用成員 init 清單:

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 };
};

當您宣告 類別的實例時,編譯器會根據多載解析的規則選擇要叫用的建構函式:

int main()
{
    Box b; // Calls Box()

    // Using uniform initialization (preferred):
    Box b2 {5}; // Calls Box(int)
    Box b3 {5, 8, 12}; // Calls Box(int, int, int)

    // Using function-style notation:
    Box b4(2, 4, 6); // Calls Box(int, int, int)
}
  • 建構函式可以宣告為 inline 、、 friendexplicitconstexpr
  • 建構函式可以初始化已宣告為 constvolatileconst volatile 的物件。 物件會在 const 建構函式完成之後變成 。
  • 若要在實作檔案中定義建構函式,請為它指定限定名稱,就像任何其他成員函式一樣: Box::Box(){...}

成員初始化運算式清單

建構函式可以選擇性地擁有 成員初始化運算式清單,此清單 會在建構函式主體執行之前初始化類別成員。 (成員初始化運算式清單與 類型的 std::initializer_list<T> 初始化運算式清單 不同

偏好成員初始化運算式清單,而不是在建構函式主體中指派值。 成員初始化運算式清單會直接初始化成員。 下列範例顯示成員初始化運算式清單,其中包含冒號之後的所有 identifier(argument) 運算式:

    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height)
    {}

識別碼必須參考類別成員;它會使用 引數的值初始化。 引數可以是其中一個建構函式參數、函式呼叫或 std::initializer_list<T>

const 成員和參考型別的成員必須在成員初始化運算式清單中初始化。

為了確保在衍生建構函式執行之前已完全初始化基類,請在初始化運算式清單中呼叫任何參數化基類建構函式。

預設建構函式

預設建 構函式通常沒有參數,但它們可以有具有預設值的參數。

class Box {
public:
    Box() { /*perform any required default initialization steps*/}

    // All params have default values
    Box (int w = 1, int l = 1, int h = 1): m_width(w), m_height(h), m_length(l){}
...
}

預設建構函式是其中 一個特殊成員函式 。 如果類別中未宣告任何建構函式,編譯器會提供隱含的預設建構函式 inline

#include <iostream>
using namespace std;

class Box {
public:
    int Volume() {return m_width * m_height * m_length;}
private:
    int m_width { 0 };
    int m_height { 0 };
    int m_length { 0 };
};

int main() {
    Box box1; // Invoke compiler-generated constructor
    cout << "box1.Volume: " << box1.Volume() << endl; // Outputs 0
}

如果您依賴隱含的預設建構函式,請務必初始化類別定義中的成員,如上一個範例所示。 如果沒有這些初始化運算式,成員將會未初始化,而 Volume() 呼叫會產生垃圾值。 一般而言,即使不依賴隱含預設建構函式,也最好以這種方式初始化成員。

您可以藉由將編譯器定義為 已刪除 ,以防止編譯器產生隱含的預設建構函式:

    // Default constructor
    Box() = delete;

如果任何類別成員不是預設建構的,編譯器產生的預設建構函式將會定義為已刪除。 例如,類別類型的所有成員及其類別類型成員都必須具有可存取的預設建構函式和解構函式。 參考型別和所有成員的所有 const 資料成員都必須有預設成員初始化運算式。

當您呼叫編譯器產生的預設建構函式並嘗試使用括弧時,會發出警告:

class myclass{};
int main(){
myclass mc();     // warning C4930: prototyped function not called (was a variable definition intended?)
}

此語句是「大部分 Vexing 剖析」問題的範例。 您可以將 解譯為函式宣告,或解譯 myclass md(); 為預設建構函式的調用。 因為 C++ 剖析器偏好宣告,因此運算式會被視為函式宣告。 如需詳細資訊,請參閱 大部分的 Vexing 剖析

如果宣告任何非預設建構函式,編譯器就不會提供預設建構函式:

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;

};

int main(){

    Box box1(1, 2, 3);
    Box box2{ 2, 3, 4 };
    Box box3; // C2512: no appropriate default constructor available
}

如果類別沒有預設建構函式,就不能單獨使用方括弧語法來建構該類別的物件陣列。 例如,假設先前的程式碼區塊,則無法宣告 Boxes 的陣列,如下所示:

Box boxes[3]; // C2512: no appropriate default constructor available

不過,您可以使用一組初始化運算式清單來初始化 Box 物件的陣列:

Box boxes[3]{ { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };

如需詳細資訊,請參閱 初始化運算式

複製建構函式

複製建 構函式 會從相同型別的物件複製成員值,以初始化 物件。 如果您的類別成員都是純量值等簡單類型,編譯器產生的複製建構函式就已足夠,而且您不需要定義自己的。 如果您的類別需要更複雜的初始化,則需要實作自訂複製建構函式。 例如,如果類別成員是指標,則您需要定義複製建構函式來配置新的記憶體,並從另一個指向的物件複製值。 編譯器產生的複製建構函式只會複製指標,讓新的指標仍然指向另一個的記憶體位置。

複製建構函式可能有下列其中一個簽章:

    Box(Box& other); // Avoid if possible--allows modification of other.
    Box(const Box& other);
    Box(volatile Box& other);
    Box(volatile const Box& other);

    // Additional parameters OK if they have default values
    Box(Box& other, int i = 42, string label = "Box");

當您定義複製建構函式時,也應該定義複製指派運算子 (=)。 如需詳細資訊,請參閱 指派 複製建構函式和複製指派運算子

您可以藉由將複製建構函式定義為已刪除,以防止複製物件:

    Box (const Box& other) = delete;

嘗試複製物件會產生錯誤 C2280:嘗試參考已刪除的函式

移動建構函式

移動建構函 式是特殊的成員函式,可將現有物件資料的擁有權移至新的變數,而不需要複製原始資料。 它會採用右值參考作為其第一個參數,而且任何稍後的參數都必須有預設值。 移動建構函式可在傳遞大型物件時大幅提升程式的效率。

Box(Box&& other);

如果另一個物件即將終結且不再需要其資源,則編譯器會在物件由相同類型的另一個物件初始化時,選擇移動建構函式。 下列範例顯示多載解析選取移動建構函式時的一個案例。 在呼叫 get_Box() 的建構函式中,傳回的值是 xvalue (eXpiring 值)。 它不會指派給任何變數,因此即將超出範圍。 為了提供此範例的動機,讓我們為 Box 提供代表其內容的大型字串向量。 移動建構函式不是複製向量及其字串,而是從過期的值 「box」 中「竊取」它,讓向量現在屬於新的 物件。 的呼叫 std::move 是所需的一切,因為 vectorstring 類別都實作自己的移動建構函式。

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

class Box {
public:
    Box() { std::cout << "default" << std::endl; }
    Box(int width, int height, int length)
       : m_width(width), m_height(height), m_length(length)
    {
        std::cout << "int,int,int" << std::endl;
    }
    Box(Box& other)
       : m_width(other.m_width), m_height(other.m_height), m_length(other.m_length)
    {
        std::cout << "copy" << std::endl;
    }
    Box(Box&& other) : m_width(other.m_width), m_height(other.m_height), m_length(other.m_length)
    {
        m_contents = std::move(other.m_contents);
        std::cout << "move" << std::endl;
    }
    int Volume() { return m_width * m_height * m_length; }
    void Add_Item(string item) { m_contents.push_back(item); }
    void Print_Contents()
    {
        for (const auto& item : m_contents)
        {
            cout << item << " ";
        }
    }
private:
    int m_width{ 0 };
    int m_height{ 0 };
    int m_length{ 0 };
    vector<string> m_contents;
};

Box get_Box()
{
    Box b(5, 10, 18); // "int,int,int"
    b.Add_Item("Toupee");
    b.Add_Item("Megaphone");
    b.Add_Item("Suit");

    return b;
}

int main()
{
    Box b; // "default"
    Box b1(b); // "copy"
    Box b2(get_Box()); // "move"
    cout << "b2 contents: ";
    b2.Print_Contents(); // Prove that we have all the values

    char ch;
    cin >> ch; // keep window open
    return 0;
}

如果類別未定義移動建構函式,如果沒有任何使用者宣告的複製建構函式、複製指派運算子、移動指派運算子或解構函式,編譯器會產生隱含的建構函式。 如果未定義明確或隱含移動建構函式,則其他會使用移動建構函式的作業會改用複製建構函式。 如果類別宣告移動建構函式或移動指派運算子,則會將隱含宣告的複製建構函式定義為已刪除。

如果類別類型的任何成員缺少解構函式,或編譯器無法判斷要用於移動作業的建構函式,則會將隱含宣告的移動建構函式定義為已刪除。

如需如何撰寫非簡單移動建構函式的詳細資訊,請參閱 移動建構函式和移動指派運算子 (C++)。

明確預設和已刪除的建構函式

您可以明確 預設 複製建構函式、預設建構函式、移動建構函式、複製指派運算子、移動指派運算子和解構函式。 您可以明確 刪除 所有特殊成員函式。

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;
    //...
};

如需詳細資訊,請參閱 明確預設和已刪除的函式

constexpr 建構函式

如果 ,建構函式可能會宣告為 constexpr

  • 它要麼宣告為預設值,要麼滿足一般 constexpr 函 式的所有條件 ;
  • 類別沒有虛擬基類;
  • 每個參數都是常 數值型別 ;
  • 主體不是函式 try-block;
  • 所有非靜態資料成員和基類子物件都會初始化;
  • 如果類別是具有變體成員的聯集,或 (b) 具有匿名聯集,則只會初始化其中一個等位成員;
  • 類別類型的每個非靜態資料成員,而且所有基類子物件都有 constexpr 建構函式

初始化運算式清單建構函式

如果建構函式採用 std::initializer_list<T> 做為其參數,而任何其他參數都有預設引數,當類別透過直接初始化具現化時,就會在多載解析中選取該建構函式。 您可以使用initializer_list來初始化任何可接受它的成員。 例如,假設 Box 類別 (如先前所示) 具有 std::vector<string> 成員 m_contents 。 您可以提供如下所示的建構函式:

    Box(initializer_list<string> list, int w = 0, int h = 0, int l = 0)
        : m_contents(list), m_width(w), m_height(h), m_length(l)
{}

然後建立 Box 物件,如下所示:

    Box b{ "apples", "oranges", "pears" }; // or ...
    Box b2(initializer_list<string> { "bread", "cheese", "wine" }, 2, 4, 6);

明確建構函式

如果類別的建構函式具有單一參數,或者,所有參數 (但其中一個除外) 都有預設值,則參數類型可以隱含地轉換為類別類型。 例如,如果 Box 類別具有建構函式,如下:

Box(int size): m_width(size), m_length(size), m_height(size){}

可以像這樣初始化 Box:

Box b = 42;

或將 int 傳遞給採用 Box 的函式:

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

private:
    Box m_box;
    double m_postage;
}
//elsewhere...
    ShippingOrder so(42, 10.8);

在某些情況下,這類轉換十分有用,但它們可能更常導致您程式碼中的細微但嚴重的錯誤。 一般情況下,您應該在建構函式上使用 explicit 關鍵字(和使用者定義運算子),以防止這類隱含類型轉換:

explicit Box(int size): m_width(size), m_length(size), m_height(size){}

建構函式是明確建構函式時,此行會造成編譯器錯誤:ShippingOrder so(42, 10.8);。 如需詳細資訊,請參閱 使用者定義型別轉換

建構順序

建構函式會依此順序執行其工作:

  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

衍生類別建構函式一定會呼叫基底類別建構函式,因此,它可以依賴完全建構的基底類別,才進行任何額外的工作。 基類建構函式會依衍生順序呼叫,例如,如果 ClassA 衍生自 ClassBClassCClassC 衍生自 ,則先呼叫建構函式,再呼叫建 ClassB 構函式,再呼叫建 ClassA 構函式。

如果基類沒有預設建構函式,您必須在衍生類別建構函式中提供基類建構函式參數:

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;
};

int main(){

    const string aLabel = "aLabel";
    StorageBox sb(1, 2, 3, aLabel);
}

如果建構函式擲回例外狀況,解構順序是建構順序的相反:

  1. 在建構函式主體中的程式碼會回溯。

  2. 基底類別和成員物件會依宣告的反向順序終結。

  3. 如果建構函式未委派,則會終結所有完整建構的基類物件和成員。 不過,由於物件本身並未完全建構,因此不會執行解構函式。

衍生的建構函式和擴充匯總初始化

如果基類的建構函式是非公用的,但衍生類別可以存取,則您無法使用空白大括弧,在 Visual Studio 2017 和更新版本中初始化衍生型 /std:c++17 別的物件。

下列範例顯示 C++14 一致性行為:

struct Derived;

struct Base {
    friend struct Derived;
private:
    Base() {}
};

struct Derived : Base {};

Derived d1; // OK. No aggregate init involved.
Derived d2 {}; // OK in C++14: Calls Derived::Derived()
               // which can call Base ctor.

在 C++17 中,Derived 已視作彙總類型。 因此,透過私用預設建構函式將 Base 初始化會直接包含在擴充彙總初始化規則的過程。 先前,私用 Base 建構函式是透過 Derived 建構函式呼叫的,而且因為 friend 宣告而成功。

下列範例示範 Visual Studio 2017 和更新版本中 /std:c++17 的 C++17 行為:

struct Derived;

struct Base {
    friend struct Derived;
private:
    Base() {}
};

struct Derived : Base {
    Derived() {} // add user-defined constructor
                 // to call with {} initialization
};

Derived d1; // OK. No aggregate init involved.

Derived d2 {}; // error C2248: 'Base::Base': can't access
               // private member declared in class 'Base'

具有多個繼承之類別的建構函式

如果類別衍生自多個基類,則會依照衍生類別宣告中所列的順序叫用基類建構函式:

#include <iostream>
using namespace std;

class BaseClass1 {
public:
    BaseClass1() { cout << "BaseClass1 ctor\n"; }
};
class BaseClass2 {
public:
    BaseClass2() { cout << "BaseClass2 ctor\n"; }
};
class BaseClass3 {
public:
    BaseClass3() { cout << "BaseClass3 ctor\n"; }
};
class DerivedClass : public BaseClass1,
                     public BaseClass2,
                     public BaseClass3
                     {
public:
    DerivedClass() { cout << "DerivedClass ctor\n"; }
};

int main() {
    DerivedClass dc;
}

可預期下列輸出:

BaseClass1 ctor
BaseClass2 ctor
BaseClass3 ctor
DerivedClass ctor

委派建構函式

委派建構函式會呼叫相同類別中的不同建構 函式,以執行一些初始化工作。 當您有多個建構函式必須執行類似的工作時,這項功能非常有用。 您可以在一個建構函式中撰寫主要邏輯,並從其他建構函式叫用它。 在下列簡單範例中,Box(int) 會將其工作委派給 Box(int,int,int):

class Box {
public:
    // Default constructor
    Box() {}

    // Initialize a Box with equal dimensions (i.e. a cube)
    Box(int i) :  Box(i, i, i)  // delegating constructor
    {}

    // Initialize a Box with custom dimensions
    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height)
    {}
    //... rest of class as before
};

在任何建構函式完成時,建構函式建立的物件會立即完全初始化。 如需詳細資訊,請參閱 委派建構函式

繼承建構函式 (C++11)

衍生類別可以使用 宣告,從直接基類 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:
    // Inherit all constructors from Base
    using Base::Base;

private:
    // Can't initialize newMember from Base constructors.
    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()*/

Visual Studio 2017 和更新版本 using 模式和更新版本中的 語句 /std:c++17 會將基類的所有建構函式範圍納入範圍,但衍生類別中建構函式具有相同簽章的建構函式除外。 一般而言,當衍生類別未宣告任何新的資料成員或建構函式時,最好使用繼承建構函式。

如果類型引數指定基底類別,則類別樣板可以繼承該類型的所有建構函式:

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

如果這些基類具有具有相同簽章的建構函式,衍生類別就無法繼承自多個基類。

建構函式和複合類別

包含類別類型成員的類別稱為 複合類別 。 在建立複合類別的類別類型成員時,會先呼叫建構函式,然後呼叫類別自己的建構函式。 當包含的類別缺少預設建構函式時,您必須在複合類別的建構函式中使用初始設定清單。 在先前的 StorageBox 範例中,如果將 m_label 成員變數的類型變更為新的 Label 類別,您必須呼叫基底類別建構函式和初始化 m_label 建構函式中的 StorageBox 變數:

class Label {
public:
    Label(const string& name, const string& address) { m_name = name; m_address = address; }
    string m_name;
    string m_address;
};

class StorageBox : public Box {
public:
    StorageBox(int width, int length, int height, Label label)
        : Box(width, length, height), m_label(label){}
private:
    Label m_label;
};

int main(){
// passing a named Label
    Label label1{ "some_name", "some_address" };
    StorageBox sb1(1, 2, 3, label1);

    // passing a temporary label
    StorageBox sb2(3, 4, 5, Label{ "another name", "another address" });

    // passing a temporary label as an initializer list
    StorageBox sb3(1, 2, 3, {"myname", "myaddress"});
}

本節內容

另請參閱

類別和結構