10. 析构函数基础:什么是析构函数?析构函数的语法与特点、析构函数的调用顺序

好,咱们今天来聊聊析构函数。说实话,很多初学者对构造函数的重视程度远高于析构函数,觉得对象创建完了就完事了。但我在实际项目中踩过不少坑,才真正意识到——不会写析构函数,就等于不会管理资源

什么是析构函数?

析构函数,说白了就是对象“临终前”执行的最后一段代码。当对象生命周期结束,或者被 delete 掉的时候,编译器会自动调用它。

它的核心使命只有一个:释放对象占用的资源

你想想看,构造函数里你 new 了内存、打开了文件、申请了锁,如果没人去清理,程序跑着跑着就炸了。析构函数就是干这个的——擦屁股。

核心定义:析构函数(Destructor)是类的一个特殊成员函数,在对象销毁时自动调用,用于释放对象持有的资源。

析构函数的语法与特点

语法其实很简单,我直接上代码:

class MyClass {
public:
    // 构造函数
    MyClass() {
        data = new int[100];
    }
    
    // 析构函数
    ~MyClass() {
        delete[] data;
    }
    
private:
    int* data;
};

看到没?析构函数的名字就是类名前加个波浪号 ~。没有返回值,没有参数,也不能被重载——一个类只能有一个析构函数。

这里有几个关键特点,我列出来:

  • 自动调用:对象离开作用域时自动执行,不需要你手动调用
  • 无参无返回值:不接受任何参数,也不返回任何值
  • 不可重载:每个类只有一个析构函数
  • 可被显式调用:虽然极少这么做,但语法上允许 obj.~MyClass()
  • 可声明为 virtual:基类的析构函数通常要加 virtual,否则多态删除时会出问题

个人习惯:我写类的时候,只要类里用了动态内存、文件句柄、数据库连接这些资源,第一件事就是先把析构函数写好。这叫“先想好怎么死,再想怎么活”。

析构函数的调用顺序

嗯,这里要重点讲一下。析构函数的调用顺序和构造函数正好相反——后构造的先析构。这个规则在继承和组合关系中尤其重要。

1. 单个对象的析构顺序

先看一个简单例子:

class Demo {
public:
    Demo() { cout << "构造\n"; }
    ~Demo() { cout << "析构\n"; }
};

int main() {
    Demo d;
    cout << "函数体中\n";
    return 0;
}
// 输出:
// 构造
// 函数体中
// 析构

对象在 main 函数结束时自动析构。局部对象的析构顺序和构造顺序相反。

2. 继承体系下的析构顺序

这个我当年刚学的时候搞反过,后来在项目里排查内存泄漏才彻底弄明白。

class Base {
public:
    ~Base() { cout << "Base 析构\n"; }
};

class Derived : public Base {
public:
    ~Derived() { cout << "Derived 析构\n"; }
};

int main() {
    Derived d;
    return 0;
}
// 输出:
// Derived 析构
// Base 析构

看到了吗?先析构派生类,再析构基类。为什么?因为派生类可能依赖基类的成员,如果基类先挂了,派生类再去访问基类成员就崩了。

我曾经踩过的坑:有一次写一个网络库,基类里有个 socket 句柄,派生类在析构函数里还要用这个句柄发关闭消息。结果我基类析构函数先执行了,socket 被关闭,派生类再去发消息直接段错误。后来改成派生类先发关闭消息,再让基类关 socket,问题解决。

3. 成员对象的析构顺序

如果类里有其他类的对象作为成员,析构顺序是:先执行类自身的析构函数体,再按照成员声明顺序的逆序析构成员对象

class MemberA {
public:
    ~MemberA() { cout << "A 析构\n"; }
};

class MemberB {
public:
    ~MemberB() { cout << "B 析构\n"; }
};

class Container {
private:
    MemberA a;
    MemberB b;
public:
    ~Container() { cout << "Container 析构\n"; }
};

int main() {
    Container c;
    return 0;
}
// 输出:
// Container 析构
// B 析构
// A 析构

注意:成员 a 先声明,b 后声明。析构时先析构 b,再析构 a。这就是“后构造的先析构”原则。

完整的析构顺序总结

我把整个调用顺序整理成了一张表,方便你对照:

场景 析构顺序
局部对象 后定义的对象先析构
继承关系 派生类先析构 → 基类后析构
成员对象 类自身析构函数体先执行 → 成员按声明逆序析构
动态对象(new/delete) 调用 delete 时触发析构,顺序同上
静态/全局对象 程序退出时析构,顺序与构造相反

析构函数调用流程图

下面这张图展示了继承+成员组合场景下的完整析构流程:

析构函数调用顺序(继承 + 成员组合) Derived 对象 ① 执行 Derived 析构函数体 ② 析构成员对象(逆序) 成员 B(后声明) 成员 A(先声明) 先析构 后析构 ③ 执行 Base 析构函数体 析构顺序:派生类自身 → 成员对象(逆序)→ 基类

几个容易忽略的细节

最后,我再补充几个实际开发中容易踩的点:

  • 如果没写析构函数:编译器会生成一个默认的,什么都不做。如果你的类有动态资源,那就等着内存泄漏吧。
  • 析构函数中不要抛出异常:C++ 规定析构函数默认不抛异常,如果抛了且没被捕获,程序会直接 terminate。我一般用 try-catch 在析构内部消化掉所有异常。
  • 基类析构函数要加 virtual:否则通过基类指针 delete 派生类对象时,只会调用基类的析构函数,派生类的资源就泄漏了。
  • 智能指针可以省掉很多析构函数:现代 C++ 里,能用 unique_ptr、shared_ptr 就别手写 new/delete。我现在的项目里,90% 的类都不需要自定义析构函数。

我的建议:刚开始学的时候,每个类都手写一个空的析构函数,哪怕什么都不做。这样能帮你养成“资源管理”的意识。等熟练了再用 =default 或者依赖智能指针。

好了,析构函数的基础就讲到这里。记住一句话:谁申请,谁释放;后构造,先析构。这十六个字能帮你避开 80% 的资源管理问题。


公众号:蓝海资料掘金营,微信deep3321