看似微小的語法選擇,長期積累決定程式碼是「資產」還是「負債」。分享三條血淚凝練的鐵律。
class Ball { virtual bool IsKickable() = 0; };
class Football { bool IsKickable() override { return true; } };
class Basketball { bool IsKickable() override { return false; } };
問題:IsKickable 是球類的本質靜態屬性,不隨物件狀態改變,無需動態派發。虛函數帶來:vptr 佔用 8 位元組、間接呼叫開銷、vtable 膨脹、二進位相容性負擔。
class Ball {
protected:
bool m_kickable;
public:
bool IsKickable() const { return m_kickable; } // 非虛、內聯、零開銷
};
class Football : public Ball { public: Football() : m_kickable(true) {} };
class Basketball : public Ball { public: Basketball() : m_kickable(false) {} };
判斷準則:若派生類覆寫內容僅是 return 常數; 或存取固定資料成員,必為靜態屬性,勿用虛函數。
|| 做複雜邏輯判斷
// 難以閱讀的單行斷言
assert(IsA() || IsB() || bOk);
// 邏輯等價於:(!IsA() && !IsB()) ⇒ bOk
維護者需心理模擬德摩根定律,極易誤讀。除錯時無法在中間條件設中斷點,覆蓋率分析也失真。
if (IsC()) {
assert(bOk); // 清楚表達:C 狀態下 bOk 必為真
}
或提煉具名輔助函數:bool IsValidState() { return IsA() || IsB() || bOk; } 並加註解。
class Base {
public:
virtual ~Base() = 0; // 純虛析構函數,使 Base 成為抽象類
};
Base::~Base() { printf("~Base()\n"); } // 必須提供定義!派生類析構會呼叫
class Derived : public Base {
public:
~Derived() override { printf("~Derived()\n"); }
};
關鍵點:
1. = 0 只影響「可否實例化」,不影響「是否有函數體」。
2. 純虛析構函數必須提供定義,因為派生類析構鏈會隱式呼叫基類析構函數。
3. 同理可用於其他必須覆寫但有通用預設邏輯的虛函數(如 virtual void Initialize() = 0; 提供預設初始化流程)。
虛函數用於「行為多型」,非「資料多型」;邏輯表達優先「人類可讀」而非「機器短碼」;純虛與實作並不互斥,善用語法特性表達設計意圖。三條準則,寫出經得起十年演進的程式碼。