目录一、一个会泄漏内存的程序二、为什么会这样——静态绑定 vs 动态绑定解决方案把基类析构函数声明为虚函数三、虚析构函数的原理虚析构函数也是虚函数析构函数的执行顺序四、虚析构函数的开销1. 对象内存增加一个vptr2. 调用开销增加一次间接寻址什么时候不需要虚析构函数五、完整例子虚析构函数救火六、抽象类中的虚析构函数七、虚析构函数与“三/五法则”八、三个常见错误1. 忘记把析构函数声明为虚函数2. 把不是基类的类析构函数设为虚函数浪费3. 在析构函数中调用虚函数九、这一篇的收获一、一个会泄漏内存的程序先看这段代码猜猜会发生什么cpp#include iostream #include string using namespace std; class Base { public: Base() { cout Base构造 endl; } ~Base() { cout Base析构 endl; } // 注意不是虚函数 }; class Derived : public Base { private: string* data; public: Derived() { data new string(派生类分配的资源); cout Derived构造分配了内存 endl; } ~Derived() { delete data; cout Derived析构释放了内存 endl; } }; int main() { Base* ptr new Derived(); delete ptr; // 这里会发生什么 return 0; }输出textBase构造 Derived构造分配了内存 Base析构 ← 只有基类析构被调用问题Derived的析构函数没有被调用data指向的内存永远没有释放——内存泄漏。这个bug非常隐蔽因为程序不一定会崩溃内存泄漏通常不会立即显现在大型项目中很难追踪二、为什么会这样——静态绑定 vs 动态绑定回忆第14篇的静态绑定与动态绑定非虚函数调用在编译时决定静态绑定虚函数调用在运行时决定动态绑定析构函数也是函数。如果基类析构函数不是虚函数delete ptr时编译器采用静态绑定cppdelete ptr; // ptr的类型是Base*所以调用Base::~Base()编译器不知道ptr实际指向的是Derived对象所以不会去调用Derived的析构函数。解决方案把基类析构函数声明为虚函数cppclass Base { public: virtual ~Base() { cout Base析构 endl; } // 加virtual };现在输出textBase构造 Derived构造分配了内存 Derived析构释放了内存 ← 调用了 Base析构原理析构函数是虚函数delete ptr时动态绑定先调用派生类析构再自动调用基类析构。三、虚析构函数的原理虚析构函数也是虚函数虚析构函数和普通虚函数一样会进入虚函数表vtable。cppclass Base { public: virtual ~Base() {} }; class Derived : public Base { public: ~Derived() override {} // 重写基类的虚析构函数 };vtable布局textBase_vtable: [0] → Base::~Base() Derived_vtable: [0] → Derived::~Derived() ← 覆盖了基类的槽位当通过Base*删除对象时从对象中取出vptr从vtable中取第0个函数析构函数地址调用该函数——由于派生类覆盖了这个槽位调用的是Derived::~Derived()Derived析构函数执行完毕后自动调用Base::~Base()析构函数的执行顺序无论析构函数是否是虚函数执行顺序都是先派生类后基类。区别在于非虚只调用基类析构跳过了派生类部分虚通过动态绑定确保派生类析构先被调用四、虚析构函数的开销虚析构函数带来两个开销1. 对象内存增加一个vptrcppclass NoVirtual { public: ~NoVirtual() {} // 没有其他虚函数 }; class WithVirtual { public: virtual ~WithVirtual() {} }; cout sizeof(NoVirtual) endl; // 1空类占1字节 cout sizeof(WithVirtual) endl; // 864位系统vptr占8字节如果类本身已经有其他虚函数vptr已经存在加虚析构函数不增加额外内存。2. 调用开销增加一次间接寻址非虚析构直接调用虚析构vptr → vtable → 间接调用多2次内存访问什么时候不需要虚析构函数规则只有当类会作为基类被多态使用时才需要虚析构函数。cpp// 不需要虚析构函数不会作为基类 class Point { int x, y; public: ~Point() {} // 非虚即可 }; // 需要虚析构函数会作为基类被多态使用 class Shape { public: virtual void draw() 0; virtual ~Shape() {} // 必须虚 };标准库中的例子std::string没有虚析构函数不应该被继承std::vector没有虚析构函数不应该被继承std::exception有虚析构函数可以被继承五、完整例子虚析构函数救火一个模拟数据库连接池的例子展示没有虚析构函数的后果cpp#include iostream #include string #include vector using namespace std; // 资源类模拟数据库连接 class DBConnection { private: int id; static int nextId; public: DBConnection() : id(nextId) { cout 数据库连接 id 已建立 endl; } ~DBConnection() { cout 数据库连接 id 已释放 endl; } void query(const string sql) { cout 连接 id 执行: sql endl; } }; int DBConnection::nextId 0; // 基类数据访问层没有虚析构函数——错误示范 class BadDataAccess { protected: DBConnection* conn; public: BadDataAccess() { conn new DBConnection(); cout BadDataAccess构造 endl; } ~BadDataAccess() { // ❌ 非虚析构 delete conn; cout BadDataAccess析构 endl; } virtual void execute(const string sql) { conn-query(sql); } }; // 派生类用户数据访问 class BadUserDataAccess : public BadDataAccess { private: string* cache; public: BadUserDataAccess() : BadDataAccess() { cache new string(用户数据缓存); cout BadUserDataAccess构造分配了缓存 endl; } ~BadUserDataAccess() { delete cache; cout BadUserDataAccess析构释放了缓存 endl; } void execute(const string sql) override { cout 【使用缓存】 endl; conn-query(sql); } }; // 正确版本基类有虚析构函数 class GoodDataAccess { protected: DBConnection* conn; public: GoodDataAccess() { conn new DBConnection(); cout GoodDataAccess构造 endl; } virtual ~GoodDataAccess() { // ✅ 虚析构 delete conn; cout GoodDataAccess析构 endl; } virtual void execute(const string sql) { conn-query(sql); } }; class GoodUserDataAccess : public GoodDataAccess { private: string* cache; public: GoodUserDataAccess() : GoodDataAccess() { cache new string(用户数据缓存); cout GoodUserDataAccess构造分配了缓存 endl; } ~GoodUserDataAccess() override { delete cache; cout GoodUserDataAccess析构释放了缓存 endl; } void execute(const string sql) override { cout 【使用缓存】 endl; conn-query(sql); } }; int main() { cout 错误示范基类析构函数非虚 endl; BadDataAccess* badPtr new BadUserDataAccess(); badPtr-execute(SELECT * FROM users); delete badPtr; // 只调用 ~BadDataAccess()缓存泄漏 cout \n 正确示范基类析构函数虚 endl; GoodDataAccess* goodPtr new GoodUserDataAccess(); goodPtr-execute(SELECT * FROM users); delete goodPtr; // 先 ~GoodUserDataAccess()再 ~GoodDataAccess() return 0; }输出text 错误示范基类析构函数非虚 数据库连接 1 已建立 BadDataAccess构造 BadUserDataAccess构造分配了缓存 【使用缓存】 连接1 执行: SELECT * FROM users BadDataAccess析构 数据库连接 1 已释放 ← 注意没有释放cache 正确示范基类析构函数虚 数据库连接 2 已建立 GoodDataAccess构造 GoodUserDataAccess构造分配了缓存 【使用缓存】 连接2 执行: SELECT * FROM users GoodUserDataAccess析构释放了缓存 ← 正确释放了 GoodDataAccess析构 数据库连接 2 已释放六、抽象类中的虚析构函数抽象类有纯虚函数必须提供虚析构函数即使它是空的cppclass Shape { public: virtual double getArea() 0; virtual ~Shape() {} // 即使是空的也必须写 };为什么派生类对象通过Shape*删除时需要调用正确的析构函数如果不写编译器生成的析构函数是非虚的导致上述问题最佳实践任何作为基类使用的类都应该有虚析构函数。七、虚析构函数与“三/五法则”回顾第4篇的三法则如果类需要自定义析构函数通常也需要自定义拷贝构造和拷贝赋值。对于多态基类这条规则有一个例外cppclass PolymorphicBase { public: virtual ~PolymorphicBase() default; // 虚析构但使用默认实现 // 不需要自定义拷贝构造/赋值因为不管理资源 };现代C建议如果类是作为基类使用virtual ~ClassName() default;如果类管理资源遵循五法则析构、拷贝构造、拷贝赋值、移动构造、移动赋值如果不作为基类且不管理资源让编译器生成默认析构函数八、三个常见错误1. 忘记把析构函数声明为虚函数cppclass Base { public: ~Base() {} // 非虚 }; Base* p new Derived(); delete p; // 泄漏2. 把不是基类的类析构函数设为虚函数浪费cppclass Point { // 不会被继承 public: virtual ~Point() {} // 不必要的vptr开销 int x, y; }; // 每个Point对象多8字节浪费内存3. 在析构函数中调用虚函数cppclass Base { public: virtual ~Base() { cleanup(); } // 调用的是Base::cleanup不是派生类版本 virtual void cleanup() {} };和构造函数一样析构函数中虚函数不产生多态行为——派生类部分已经先析构了。九、这一篇的收获你现在应该理解如果基类析构函数不是虚函数delete基类指针时只调用基类析构派生类资源不会被释放规则任何作为基类多态使用的类都应该有虚析构函数虚析构函数会引入vptr带来8字节内存开销但如果已有其他虚函数没有额外开销抽象类必须提供虚析构函数即使是空的虚析构函数遵循动态绑定先调用派生类析构再调用基类析构 小作业写一个Logger基类有虚函数log(const string)以及虚析构函数。实现FileLogger写入文件和ConsoleLogger输出到控制台每个派生类在析构时关闭各自的资源。通过基类指针删除对象验证析构顺序正确。下一篇预告第18篇《多继承与菱形继承一二义性问题与虚拟继承》——C支持一个类继承多个基类。但当两个基类有同名成员或者出现“菱形继承”时会出现二义性问题。虚拟继承是解决方案——但它也有自己的复杂性。下篇开始讲多继承的坑与解法。