軟考在即,趁機複習一下 C++ 的核心難點——構造函數。構造函數不僅是創建對象的關鍵,更關係到對象初始化的正確性與效率。本文系統梳理構造函數的機制、默認構造函數的生成規則、參數化構造函數的設計技巧,以及初始化列表的最佳實踐。
構造函數是用來創建並初始化對象的特殊成員函數。它的名稱與類名相同,沒有返回類型(連 void 都沒有),在對象創建時自動被調用。
關鍵規則:當且僅當類中沒有聲明任何構造函數時,編譯器才會自動生成一個默認構造函數。 這個編譯器生成的默認構造函數不執行任何初始化工作,成員變量的值將是不確定的(垃圾值)。
例如:
class Example {
// 沒有聲明任何構造函數
};
Example testA; // 調用編譯器生成的默認構造函數
一旦你在類中聲明了任何構造函數(哪怕只是帶參數的),編譯器就**不再**自動生成默認構造函數。此時若嘗試寫 Example testA; 將導致編譯錯誤,因為沒有匹配的無參構造函數可供調用。
這體現了 C++ 的一項核心原則:禁止創建未初始化的對象。構造函數的存在正是為了確保對象出生時就處於已知、合法、合理的狀態。
有兩種標準方法可以讓類同時擁有參數化構造函數和默認構造函數:
class Example {
public:
Example(int a = 0, int b = 0, int c = 0)
: m_a(a), m_b(b), m_c(c) {}
private:
int m_a, m_b, m_c; // 建議用 m_ 前綴區分成員與參數
};
此時 Example testA; 合法,等價於 Example testA(0, 0, 0);。
class Example {
public:
Example() : m_a(0), m_b(0), m_c(0) {} // 初始化列表寫法
Example(int a, int b, int c) : m_a(a), m_b(b), m_c(c) {}
private:
int m_a, m_b, m_c;
};
兩種方法**不能同時使用**,否則會產生歧義(兩個默認構造函數),導致編譯錯誤。
推薦使用初始化列表:
// 推薦:初始化列表
Example() : m_a(0), m_b(0), m_c(0) {}
// 不推薦:函數體內賦值
Example() {
m_a = 0; m_b = 0; m_c = 0;
}
初始化列表直接初始化成員,避免了「先默認構造再賦值」的額外開銷;對於 const 成員、引用成員、無默認構造函數的類類型成員,**必須**使用初始化列表。
擁有默認構造函數後,以下聲明均合法:
Example testA; —— 最常用,調用默認構造函數Example testB = Example(); —— 顯式調用默認構造函數(可能產生臨時對象,現代編譯器會優化掉)Example* ptr = new Example; —— 動態分配,調用默認構造函數注意: Example testD(); **不是**創建對象,而是聲明一個返回 Example 對象的函數(最令人頭疼的「最令人惱人的解析」)。
若構造函數參數與成員變量同名:
// 錯誤示範:參數遮蓋成員
Example(int a, int b, int c) { /* 無法區分 */ }
// 正確做法:成員加 m_ 前綴
Example(int a, int b, int c) : m_a(a), m_b(b), m_c(c) {}
良好的命名規範(如 m_ 前綴)能有效避免此類問題。
《On to C++》一書總結了默認構造函數的三大特徵:
const double PI = 3.14;
class TestA {
public:
double radius, length;
// 默認構造函數
TestA() : radius(2.0), length(4.0) {}
// 參數化構造函數
TestA(double r, double l) : radius(r), length(l) {}
double volume() { return PI * radius * radius * length; }
};
private 成員在哪裡?構造函數家族還有拷貝構造函數、移動構造函數(C++11)、委派構造函數(C++11)等進階主題,將在後續筆記中詳細展開。