「基類析構函數必須聲明為 virtual」是 C++ 面向對象設計的鐵律之一。違背它會導致資源洩漏、行為未定義,且極難調試。本文通過三個層層遞進的代碼實驗,從底層機制揭示「為什麼」、「什麼時候」以及「不這樣做的後果」。
單獨對象或組合關係中,析構順序固定:當前類析構體 → 成員對象析構(逆構造順序)。
class Student {
char* name_;
public:
Student() { name_ = new char[32]; }
~Student() { delete[] name_; std::cout << "Student dtor\n"; }
};
class Normal { Student stu_; }; // 組合
int main() {
Normal* p = new Normal();
delete p; // 輸出: Normal dtor → Student dtor (正確釋放 name_)
}
無多態,靜態綁定,一切按預期運行。
派生類對象經派生類指針刪除:派生類析構 → 成員析構 → 基類析構。靜態類型與動態類型一致,虛表機制未介入,非 virtual 也能正確調用完整鏈。
class Base { ~Base() { std::cout << "~Base\n"; } };
class Derived : public Base {
Student stu_;
~Derived() { std::cout << "~Derived\n"; }
};
int main() {
Derived* pd = new Derived();
delete pd; // 輸出: ~Derived → Student dtor → ~Base (正確)
}
基類指針指向派生類對象,刪除基類指針。若基類析構非 virtual:
Base*,直接調用 Base::~Base()。Student stu_ 也不析構。Student::name_ 內存洩漏,若基類管理資源同樣洩漏。class Base { ~Base() { std::cout << "~Base\n"; } }; // 非 virtual!
class Derived : public Base {
Student stu_;
~Derived() { std::cout << "~Derived\n"; }
};
int main() {
Base* pb = new Derived(); // 多態
delete pb; // 僅輸出: ~Base —— 派生類與成員資源全部洩漏!
}
派生類含 std::string、std::vector 等成員時,洩漏更隱蔽(無裸指針),借檢工具也難發現:
class Derived : public Base {
std::string data_; // 非 virtual 基類刪除時,data_ 析構不執行 → 內存洩漏
};
class Base {
public:
virtual ~Base() { std::cout << "~Base\n"; } // 關鍵字 virtual
};
// 輸出: ~Derived → Student dtor → ~Base (完美)
機制:virtual 使析構函數進入虛表。delete pb 時,通過虛表查找動態類型 (Derived) 的析構函數入口,啟動正確的析構鏈。
virtual ~Base() = 0; 仍需提供定義(供派生類析構調用),可構造抽象基類。class Leaf final { ~Leaf(); } 不可被繼承,析構可非 virtual(但加上也無害)。std::vector<Base*> 存儲派生對象指針時,必須 保證 Base 析構為 virtual,否則 clear() 洩漏慘重。虛析構函數不是「最佳實踐」,而是正確性前提。只要類可能被繼承、且可能經基類指針/引用刪除,virtual ~Base() 就是強制性的。寫在類定義第一行,養成肌肉記憶,避免無數個深夜的內存洩漏排查。