构造函数抛出异常时,如何释放API占用的资源?
嘿,这个坑我可太熟悉了——用C封装GLFW、OpenGL这类C风格API时,构造函数抛异常导致析构函数不执行、资源直接泄漏的情况真的很头疼。你现在的问题核心在于:**当类A的构造函数抛出异常时,对象并没有完全构造完成,C不会调用它的析构函数**,之前调用的B()、B1()这些API申请的资源就成了没人管的“孤儿”。
既然不能用智能指针,那我们可以手动实现轻量化的RAII(资源获取即初始化)小类,把每个资源的创建和释放绑定到独立的小对象上,利用C++的自动析构机制来兜底。
具体解决方案:为每个资源写迷你RAII类
针对每一对资源分配/释放API,比如B()和C()、B1()和C1(),分别创建对应的小类,把资源的初始化放在构造函数,释放放在析构函数,同时禁止拷贝赋值避免资源重复释放:
// 对应B()和C()的RAII类 class ResourceB { public: ResourceB() { // 调用GLFW/OpenGL的资源分配API B(); } ~ResourceB() { // 对应的资源释放API C(); } // 禁止拷贝和赋值,防止资源被意外复制导致重复释放 ResourceB(const ResourceB&) = delete; ResourceB& operator=(const ResourceB&) = delete; }; // 对应B1()和C1()的RAII类 class ResourceB1 { public: ResourceB1() { B1(); } ~ResourceB1() { C1(); } ResourceB1(const ResourceB1&) = delete; ResourceB1& operator=(const ResourceB1&) = delete; };
修改类A的结构
把这些RAII小类作为类A的成员变量,原来构造函数里的B()、B1()调用就可以去掉了——因为成员对象会在A的构造函数执行前,按照声明顺序自动构造:
class A { private: // 注意:成员变量的声明顺序就是构造顺序,析构时会逆序执行 ResourceB b_resource; ResourceB1 b1_resource; // 如果有更多资源,继续添加对应的RAII类成员 public: A() { // 这里可以放你的业务逻辑,如果抛出异常 throw 1; } // 现在不需要手动在~A()里调用C()、C1()了! // 成员对象的析构函数会自动处理资源释放 };
为什么这个方案有效?
C++有个关键规则:当构造函数抛出异常时,已经成功构造的成员对象会按照逆序被析构。比如:
- 先构造
b_resource(调用B()) - 再构造
b1_resource(调用B1()) - 然后A的构造函数抛出异常
- 此时会先析构
b1_resource(调用C1()),再析构b_resource(调用C())
完美实现了“已申请的资源必须释放”的需求,完全不需要依赖A的析构函数。
进阶:处理依赖型资源
如果某些资源的初始化依赖其他资源(比如必须先创建GLFW窗口,才能初始化OpenGL上下文),只需要调整成员变量的声明顺序即可——因为成员是按声明顺序构造的,逆序析构。比如B1()依赖B()执行完成,就把ResourceB放在ResourceB1前面声明,确保构造顺序正确。
如果资源初始化需要参数(比如GLFW窗口需要宽高),给RAII类的构造函数加参数就行:
class ResourceB { public: ResourceB(int window_width, int window_height) { B(window_width, window_height); } ~ResourceB() { C(); } ResourceB(const ResourceB&) = delete; ResourceB& operator=(const ResourceB&) = delete; };
然后在A的构造函数初始化列表里初始化:
A() : b_resource(800, 600), b1_resource() { throw 1; }
这种方式本质上是把大的资源管理拆成了一个个独立的小单元,每个单元只负责自己的资源生命周期,既符合RAII的核心思想,又完全避开了构造函数异常导致的析构失效问题。
内容的提问来源于stack exchange,提问作者L.Nam

