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