构造函式是 OOP 中一類特殊的成員函式,用於建立物件並初始化成員變數,在 C++ 中,建構函式和類別相同,沒有返回值。
一般在以下情況下需要自訂建構函式:
- 自訂類別的成員初始化方式
- 建立類別的物件時呼叫函式
建構函式一般是
public
的,但也可以被宣告為protecated
、private
等
預設建構函式#
預設建構函式沒有提供任何參數,在類別定義時可以不顯式地定義,當不顯式定義時,編譯器會自動產生一個建構函式,一個預設建構函式的例子是:
class MyClass{
public:
MyClass(){
// 做一些事情..
}
}
關於預設建構函式的「最棘手分析」#
因為 C++ 的解析程式更偏向於宣告,所以在呼叫編輯器生成的預設建構函式並嘗試使用括號時會出現警告:
class MyClass{};
int main(){
MyClass myClass();
// 警告 C4930
}
在這個例子中,MyClass myClass (); 不會像期望的那樣呼叫 MyClass 的預設建構函式來建立 myClass 物件。相反,這行程式碼實際上被解析為一個函式宣告,而不是一個物件宣告。這裡,myClass 被解釋為一個函式,它不帶參數並返回 MyClass 型別的物件。
這種現象稱為「最棘手的分析」(Most Vexing Parse),是 C++ 語言中一個著名的語法解析問題。這個問題源自 C++ 語法的複雜性,其中某些語法結構可能根據上下文被解釋為多種不同的事物。在這種情況下,編譯器優先選擇將表達式解釋為函式宣告而非物件建構。
通過顯式建立物件避免該問題:
int main() {
MyClass myClass;
}
參數化建構函式#
參數化建構函式帶有多個參數的初始化:
class MyClass{
public:
int x, y;
MyClass(int a, int b): x(a), y(b){
// 做一些事情..
}
}
此處使用了成員初始化表達式列表,其表達式具體形式為:
identifier(argument)
成員初始化表達式列表由冒號後所有的表達式組成:
MyFunc(int a, int b, int c):
m_a(a), m_b(b), m_c(c){}
複製建構函式#
複製建構函式用於根據同型別的一個物件初始化另一個新的物件,通常出現在物件的複製場景中:
class MyClass{
public:
int x, y;
MyClass(const MyClass& mc):
x(mc.x), y(mc.y) {}
}
上面的簡單例子的成員都是標量,這種簡單情況下編譯器就足以生成複製建構函式,無需自己定義,當複製建構函式需要實現特殊行為,例如分類新記憶體時,就需要自己實現複製建構函式。
在定義建構函式時,需要重載複製賦值運算符=
移動建構函式#
移動建構函式是 C++11 後的新特性,用於將資源從一個物件轉移到另一個物件,通常在支援右值引用的場景下使用,它將現有物件資料的所有權移交給新變數,而不複製原始資料。 它採用 rvalue 引用作為其第一個參數,以後的任何參數都必須具有預設值。 移動建構函式在傳遞大型物件時可以顯著提高程式的效率:
//todo 左值和右值
class MyClass{
public:
char *str;
MyClass(MyClass && obj): str(obj.str){
obj.str = nullptr; // 避免原物件析構時釋放記憶體
}
}
委託建構函式#
委託建構函式仍然是 C++11 後引入的新特性,它允許一個建構函式在當前類別中呼叫另一個建構函式,進一步解耦程式碼:
class MyClass{
public:
int x, y, z;
MyClass(int a, int b):x(a), y(b), z(0) {}
MyClass(int a, int b, int c): Point(a, b) {z = c;}
}
繼承建構函式#
繼承建構函式仍然是 C++11 後引入的概念,它運訓從基類中繼承建構函式,使用using
關鍵字實現:
class Base{
public:
Base(int x){};
};
class MyClass: public Base{
using Base::Base; // 繼承Base的建構函式
}
顯式建構函式和隱式建構函式#
通過explicit
關鍵字宣告的建構函式只能用於直接初始化,它防止了建構函式在需要隱式轉換時被意外呼叫:
class MyClass {
public:
explicit MyClass(int a) {}
};