构造函數是 OOP 中一類特殊的成員函數,用於創建物件並初始化成員變量,在 C++ 中,構造函數和類相同,沒有返回值。
一般在以下情況下需要自定義構造函數:
- 自定義類的成員初始化方式
- 創建類的物件時調用函數
構造函數一般是
public
的,但也可以被聲明為protecated
、private
等
默認構造函數#
默認構造函數沒有任何提供任何參數,在類定義時可以不顯式地定義,當不顯式定義時,編譯器會自動生成一個構造函數,一個默認構造函數的例子是:
class MyClass{
public:
MyClass(){
// do something..
}
}
關於默認構造函數的 “最棘手分析”#
因為 C++ 的分析程序更偏向於聲明,因此在調用編輯器生成的默認構造函數並嘗試使用括號的時候會出現警告:
class MyClass{};
int main(){
MyClass myClass();
// warning 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){
// do something..
}
}
此處使用了成員初始化表達式列表,其表達式具體形式為:
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) {}
};