通常,建構函式具有public可訪問性,但也可以將建構函式宣告為 protected 或 private。建構函式可以選擇採用成員初始化表示式列表,該列表會在建構函式主體執行之前初始化類成員。與在建構函式主體中賦值相比,初始化類成員是更高效的方式。首選成員初始化表示式列表,而不是在建構函式主體中賦值。
注意:
- 成員初始化表示式的引數可以是建構函式引數之一、函式呼叫或 std::initializer_list
。 - const 成員和引用型別的成員必須在成員初始化表示式列表中進行初始化。
- 若要確保在派生建構函式執行之前完全初始化基類,需要在初始化表示式中初始化化基類建構函式。
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只能用於修飾只有一個引數的類建構函式,表明該建構函式是顯示的而非隱式的。
- explicit關鍵字的作用就是防止類建構函式的隱式自動轉換。
- 如果類建構函式引數大於或等於兩個時, 不會產生隱式轉換的, explicit關鍵字無效。
- 例外, 就是當除了第一個引數以外的其他引數都有預設值的時候, explicit關鍵字依然有效。
- 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));
}
建立移動建構函式
- 定義一個空的建構函式,建構函式的引數型別為右值引用;
- 在移動建構函式中,將源物件中的類資料成員新增到要構造的物件;
- 將源物件的資料成員置空。 這可以防止解構函式多次釋放資源(如記憶體)。
MemoryBlock(MemoryBlock&& other)
: _data(nullptr)
, _length(0)
{
_data = other._data;
_length = other._length;
other._data = nullptr;
other._length = 0;
}
建立移動賦值運算子
- 定義一個空的賦值運算子,該運算子引數型別為右值引用,返回一個引用型別;
- 防止將物件賦給自身;
- 釋放目標物件中所有資源(如記憶體),將資料成員從源物件轉移到要構造的物件;
- 返回對當前物件的引用。
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
// ...
};
建構函式執行順序
- 按宣告順序呼叫基類和成員建構函式。
- 如果類派生自虛擬基類,則會將物件的虛擬基指標初始化。
- 如果類具有或繼承了虛擬函式,則會將物件的虛擬函式指標初始化。 虛擬函式指標指向類中的虛擬函式表,確保虛擬函式正確地呼叫繫結程式碼。
- 執行自己函式體中的所有程式碼。
如果基類沒有預設建構函式,則必須在派生類建構函式中提供基類建構函式引數
下面程式碼,首先,呼叫基建構函式。 然後,按照在類宣告中出現的順序初始化基類成員。 最後,呼叫派生建構函式。
#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詳解