虚析构函数
定义与作用
虚析构函数是基类的虚析构函数,确保通过基类指针 delete 派生类对象时,派生类的析构函数被正确调用:
class Base {
public:
virtual ~Base() = default; // 虚析构:正确释放派生类资源
};
如果基类析构函数不是虚的,delete 基类指针时只调用基类析构函数,派生类特化的资源永远不会释放——这是 C++ 中最经典的资源泄漏陷阱。
核心原理
非虚析构 vs 虚析构
构造与析构的调用链
完整示例
示例一:资源泄漏的经典案例
场景说明:数据库连接池中,连接基类析构非虚导致派生类资源泄漏——逐层修复。
#include <iostream>
#include <string>
#include <vector>
#include <memory>
// ==== 错误版本:基类析构非虚 ====
class BadConnection {
public:
BadConnection(std::string host) : host_(std::move(host)) {
std::cout << "[连接] 连接 " << host_ << std::endl;
}
~BadConnection() { // 非虚析构!
std::cout << "[断开] " << host_ << std::endl;
}
virtual void query(const std::string& sql) {
std::cout << "普通查询: " << sql << std::endl;
}
private:
std::string host_;
};
class BadMySQLConnection : public BadConnection {
public:
BadMySQLConnection(std::string host, std::string db)
: BadConnection(std::move(host)), db_name_(std::move(db)),
buffer_(new char[1024 * 1024]) { // 分配 1MB 缓冲区
std::cout << " [MySQL] 选择数据库 " << db_name_
<< ",分配缓冲区" << std::endl;
}
~BadMySQLConnection() {
std::cout << " [MySQL] 释放缓冲区" << std::endl;
delete[] buffer_;
}
void query(const std::string& sql) override {
std::cout << "MySQL(" << db_name_ << ") 查询: " << sql << std::endl;
}
private:
std::string db_name_;
char* buffer_; // 动配资源
};
// ==== 正确版本:基类析构为虚 ====
class GoodConnection {
public:
GoodConnection(std::string host) : host_(std::move(host)) {
std::cout << "[安全连接] " << host_ << std::endl;
}
virtual ~GoodConnection() = default; // 虚析构!
virtual void query(const std::string& sql) {
std::cout << "查询: " << sql << std::endl;
}
private:
std::string host_;
};
class GoodMySQLConnection : public GoodConnection {
public:
GoodMySQLConnection(std::string host, std::string db)
: GoodConnection(std::move(host)), db_name_(std::move(db)),
buffer_(new char[1024 * 1024]) {
std::cout << " [MySQL] 选择数据库 " << db_name_
<< ",分配缓冲区" << std::endl;
}
~GoodMySQLConnection() override {
std::cout << " [MySQL] 正确释放缓冲区" << std::endl;
delete[] buffer_;
}
void query(const std::string& sql) override {
std::cout << "MySQL(" << db_name_ << ") 安全查询: " << sql << std::endl;
}
private:
std::string db_name_;
char* buffer_;
};
int main() {
std::cout << "===== BAD: 基类析构非虚 → 资源泄漏 =====" << std::endl;
{
BadConnection* conn = new BadMySQLConnection("localhost", "school_db");
conn->query("SELECT * FROM students");
delete conn; // 只调用 BadConnection::~BadConnection !!!
// BadMySQLConnection::~BadMySQLConnection 不会调用
// 1MB 缓冲区泄漏!
std::cout << "(conn 已删除,但缓冲区泄漏了)" << std::endl;
}
std::cout << "\n===== GOOD: 基类析构为虚 → 正确释放 =====" << std::endl;
{
GoodConnection* conn = new GoodMySQLConnection("localhost", "school_db");
conn->query("SELECT * FROM students");
delete conn; // 正确:GoodMySQLConnection::~ → GoodConnection::~
}
// 更安全的做法:智能指针 + 虚析构
std::cout << "\n===== BEST: 智能指针 + 虚析构 =====" << std::endl;
{
std::unique_ptr<GoodConnection> conn =
std::make_unique<GoodMySQLConnection>("localhost", "school_db");
conn->query("SELECT * FROM students");
} // unique_ptr 自动释放,正确调用派生类析构
return 0;
}
预期输出:
===== BAD: 基类析构非虚 → 资源泄漏 =====
[连接] 连接 localhost
[MySQL] 选择数据库 school_db,分配缓冲区
MySQL(school_db) 查询: SELECT * FROM students
[断开] localhost
(conn 已删除,但缓冲区泄漏了)
===== GOOD: 基类析构为虚 → 正确释放 =====
[安全连接] localhost
[MySQL] 选择数据库 school_db,分配缓冲区
MySQL(school_db) 安全查询: SELECT * FROM students
[MySQL] 正确释放缓冲区
===== BEST: 智能指针 + 虚析构 =====
[安全连接] localhost
[MySQL] 选择数据库 school_db,分配缓冲区
MySQL(school_db) 安全查询: SELECT * FROM students
[MySQL] 正确释放缓冲区
逐段分析:
- BAD 版本:
delete conn只调用了BadConnection::~BadConnection(),BadMySQLConnection的 1MB 缓冲区永远不会释放——经典资源泄漏 - GOOD 版本:基类析构是虚函数,
delete conn正确先调~GoodMySQLConnection()再调~GoodConnection() - BEST 版本:
unique_ptr<GoodConnection>+ 虚析构——智能指针自动管理生命周期,结合虚析构保证正确释放 - 关键原则:如果类有任何虚函数(说明将被多态使用),析构函数必须是虚的
示例二:资源池的实际应用
#include <iostream>
#include <vector>
#include <memory>
#include <string>
#include <chrono>
// 资源基类:虚析构保证正确清理
class Resource {
public:
Resource(std::string id) : id_(std::move(id)) {}
virtual ~Resource() = default;
virtual void close() = 0;
virtual std::string type() const = 0;
const std::string& id() const { return id_; }
private:
std::string id_;
};
class FileHandle : public Resource {
public:
FileHandle(std::string id, std::string path)
: Resource(std::move(id)), path_(std::move(path)) {
std::cout << " 打开文件: " << path_ << std::endl;
}
~FileHandle() override { close(); }
void close() override {
std::cout << " 关闭文件: " << path_ << std::endl;
}
std::string type() const override { return "文件"; }
private:
std::string path_;
};
class SocketHandle : public Resource {
public:
SocketHandle(std::string id, int port)
: Resource(std::move(id)), port_(port) {
std::cout << " 打开端口: " << port_ << std::endl;
}
~SocketHandle() override { close(); }
void close() override {
std::cout << " 关闭端口: " << port_ << std::endl;
}
std::string type() const override { return "网络"; }
private:
int port_;
};
int main() {
std::cout << "===== 资源池(依赖虚析构正确清理) =====" << std::endl;
std::vector<std::unique_ptr<Resource>> pool;
pool.push_back(std::make_unique<FileHandle>("f1", "data.txt"));
pool.push_back(std::make_unique<SocketHandle>("s1", 8080));
pool.push_back(std::make_unique<FileHandle>("f2", "config.json"));
std::cout << "\n资源池包含:" << std::endl;
for (const auto& r : pool) {
std::cout << " " << r->type() << ": " << r->id() << std::endl;
}
std::cout << "\n清理资源池..." << std::endl;
// unique_ptr 自动调用 delete,虚析构保证每个子类的 close() 被调用
pool.clear();
return 0;
}
预期输出:
===== 资源池(依赖虚析构正确清理) =====
打开文件: data.txt
打开端口: 8080
打开文件: config.json
资源池包含:
文件: f1
网络: s1
文件: f2
清理资源池...
关闭文件: config.json
关闭端口: 8080
关闭文件: data.txt
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 基类有虚函数但析构非虚 | 派生类资源泄漏 | 所有多态基类的析构函数必须为 virtual |
std::unique_ptr<Base> 使用非虚析构的 Base | Undefined Behavior | 基类析构必须 virtual |
| 析构函数调用虚函数 | 只调基类版本 | 析构中不调虚函数 |
| 最终类(final)析构为虚 | 性能损耗可忽略 | 最终类析构不必是虚的 |
| 误以为不需要虚析构(如 CRTP) | CRTP 基类析构通常也应是 virtual | 除非确定不会通过基类指针 delete |
常见面试问题
为什么基类的析构函数需要是虚函数?——当通过基类指针
delete派生类对象时,如果析构非虚,只调用基类析构,派生类资源泄漏。虚析构确保正确的析构调用链。什么时候析构函数可以不设为 virtual?——当类不会被继承(
final)、不会通过基类指针delete、或者继承是 private 且仅作实现复用时。纯虚析构函数和普通虚析构的区别?——纯虚析构
= 0使类成为抽象类,但仍需提供函数体。普通虚析构不强制类为抽象类。析构函数的调用顺序?——派生类析构 → 成员析构(声明逆序) → 基类析构。这是虚析构底层的保证。
std::shared_ptr对虚析构有什么特殊处理?——shared_ptr在构造时捕获删除器,即使基类析构非虚,shared_ptr<Base>(new Derived())也能正确调用派生类析构。但不应依赖此特性——基类析构仍应该是虚的。
小结
- 任何可能作为多态基类的类,析构函数必须为
virtual - 判断准则:如果有任何虚函数 → 析构一定是虚的
- 非虚析构 +
delete基类指针 = 派生类资源泄漏(C++ 经典陷阱 Top 3) unique_ptr没有shared_ptr的类型擦除机制——基类析构非虚时更危险- 纯虚析构使类抽象化,但必须提供函数体