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 时触发析构,顺序同上 |
| 静态/全局对象 | 程序退出时析构,顺序与构造相反 |
析构函数调用流程图
下面这张图展示了继承+成员组合场景下的完整析构流程:
几个容易忽略的细节
最后,我再补充几个实际开发中容易踩的点:
- 如果没写析构函数:编译器会生成一个默认的,什么都不做。如果你的类有动态资源,那就等着内存泄漏吧。
- 析构函数中不要抛出异常:C++ 规定析构函数默认不抛异常,如果抛了且没被捕获,程序会直接 terminate。我一般用 try-catch 在析构内部消化掉所有异常。
- 基类析构函数要加 virtual:否则通过基类指针 delete 派生类对象时,只会调用基类的析构函数,派生类的资源就泄漏了。
- 智能指针可以省掉很多析构函数:现代 C++ 里,能用 unique_ptr、shared_ptr 就别手写 new/delete。我现在的项目里,90% 的类都不需要自定义析构函数。
我的建议:刚开始学的时候,每个类都手写一个空的析构函数,哪怕什么都不做。这样能帮你养成“资源管理”的意识。等熟练了再用 =default 或者依赖智能指针。
好了,析构函数的基础就讲到这里。记住一句话:谁申请,谁释放;后构造,先析构。这十六个字能帮你避开 80% 的资源管理问题。
公众号:蓝海资料掘金营,微信deep3321