虛擬解構函數是 C++ 多型類別設計的**基石**。缺少它,透過基類指標刪除派生類物件將導致**未定義行為** (通常表現為派生類解構函數不被呼叫、資源洩漏、Heap Corruption)。本文深度剖析虛擬解構機制、經典錯誤案例、純虛擬解構函數、C++11/20 現代演進與設計原則。
#include
using namespace std;
class Base {
public:
Base() { cout << "Base::Base()\n"; }
~Base() { cout << "Base::~Base()\n"; } // 非 virtual
};
class Derived : public Base {
int* data_; // 模擬資源
public:
Derived() : data_(new int[100]) { cout << "Derived::Derived()\n"; }
~Derived() { delete[] data_; cout << "Derived::~Derived()\n"; }
};
int main() {
Base* ptr = new Derived(); // 多型建立
delete ptr; // 透過基類指標刪除
return 0;
}
Base::Base()
Derived::Derived()
Base::~Base() // 僅呼叫 Base 解構函數!
// Derived::~Derived() **未被呼叫**
// data_ 記憶體洩漏!
delete ptr 在編譯期僅知道 ptr 型別為 Base*。Base::~Base()**,完全不知派生類存在。
class Base {
public:
Base() { cout << "Base::Base()\n"; }
virtual ~Base() { cout << "Base::~Base()\n"; } // 加上 virtual
};
class Derived : public Base {
int* data_;
public:
Derived() : data_(new int[100]) { cout << "Derived::Derived()\n"; }
~Derived() override { delete[] data_; cout << "Derived::~Derived()\n"; } // override 明確語意
};
int main() {
Base* ptr = new Derived();
delete ptr; // 現在正確!
return 0;
}
Base::Base()
Derived::Derived()
Derived::~Derived() // 派生類解構函數先執行
Base::~Base() // 基類解構函數後執行
// 記憶體正確釋放
delete ptr 透過 ptr->vptr 找到完整物件的 vtable,**動態分派呼叫正確的解構函數** (先派生類、後基類,遞迴向上)。
class Interface {
public:
virtual ~Interface() = 0; // 純虛擬:強制衍生類實作,但基類**必須提供定義**
};
// 必須在類別外提供定義 (即使是純虛擬)!
Interface::~Interface() {
cout << "Interface::~Interface()\n"; // 可執行清理邏輯
}
class Concrete : public Interface {
public:
~Concrete() override { cout << "Concrete::~Concrete()\n"; }
};
| 策略 | 適用場景 | 範例 |
|---|---|---|
virtual ~Base() = default; | 標準多型基類 (公開繼承、基類指標刪除) | 所有介面類別、抽象基類 |
protected: ~Base() = default; | **禁止**透過基類指標刪除 (強制派生類管理生命週期) | CRTP 基類、非多型基類、啟用 std::enable_shared_from_this 類別 |
virtual ~Base() = 0; + 定義 | 純介面類別 (強制抽象) | Plugin 介面、Strategy 介面 |
= default / = delete 明確語意
class Base {
public:
virtual ~Base() = default; // 明確使用編譯器合成版本 (高效、正確)
// 或
virtual ~Base() {} // 手寫空本體 (等價但較舊風格)
};
class NonPolymorphic {
protected:
~NonPolymorphic() = default; // 禁止基類指標 delete
public:
// 提供靜態工廠回傳 unique_ptr
static std::unique_ptr Create() {
return std::unique_ptr(new NonPolymorphic());
}
};
class Base { public: virtual ~Base() = default; };
class Derived : public Base { /* ... */ };
// unique_ptr 刪除器預設呼叫 delete,需虛擬解構函數
std::unique_ptr ptr = std::make_unique();
// ptr 离开作用域 -> delete -> 虛擬分派 -> Derived::~Derived() -> Base::~Base()
// shared_ptr 同理
std::shared_ptr sptr = std::make_shared();
// 參考計數歸零 -> delete -> 虛擬分派
final) 可省略虛擬解構函數?
class Base { public: virtual ~Base() = default; };
class Derived final : public Base { // final 禁止進一步繼承
// 若無人繼承 Derived,且僅透過 Derived* 刪除,技術上可非虛擬
// 但**強烈建議保持 virtual** (繼承自 Base 已虛擬,自動為虛擬)
~Derived() override = default; // 明確 override,文檔化語意
};
class Base {
public:
virtual void Foo() {} // 有虛函數
~Base() {} // 但解構函數非 virtual!危險!
};
// 正確:只要類別設計為多型基類,解構函數**必須**宣告 virtual
template
class CRTPBase {
public:
// virtual ~CRTPBase() = default; // 錯誤!CRTP 靜態多型,不需動態分派
// 虛擬函數會增加 vptr/vtable 開銷,破壞 CRTP 零成本抽象
protected:
~CRTPBase() = default; // 正確:protected 非虛擬,禁止基類指標 delete
};
std::vector vec;
vec.push_back(new Derived());
// 刪除迴圈
for (auto p : vec) delete p; // 需虛擬解構函數
// 現代寫法:vector> 自動管理,同樣需虛擬解構函數
| 項目 | 開銷 | 備註 |
|---|---|---|
| vptr (每物件) | 1 指標大小 (8 bytes x64) | 含任何虛函數即存在,非虛擬解構函數專屬 |
| vtable (每類別) | 虛函數數量 × 指標大小 | 解構函數佔 1 入口 |
| 動態分派指令 | 1-2 條間接跳轉指令 | 現代 CPU 分支預測極佳,開銷可忽略 |
| 編譯器優化 | 最終類別/單一繼承可去虛擬化 | final、-fdevirtualize (GCC/Clang) |
**結論**:虛擬解構函數開銷極小,換取正確性絕對值得。嚴禁因「效能」理由省略。
protected 禁止基類指標 delete)。virtual**。
virtual ~Base() = 0; + 提供定義。virtual ~Base() = default; (C++11 推薦)。delete)。protected 非虛擬解構函數。final) → 繼承自虛擬解構基類自動為虛擬,明確 override 文檔化。虛擬解構函數不是選項,是**多型類別正確性的契約**。在 2024 年,請對所有多型基類無條件寫上 virtual ~Base() = default;,讓編譯器為你守護資源安全。