- 作者:老汪软件技巧
- 发表时间:2024-09-09 00:06
- 浏览量:
以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」/s/Q1UxKf4mU…
这是《掌握 C++ 异常艺术:构建健壮程序的秘诀与实战策略》系列文章的第六篇,文末有链接可以查看系列里其它文章。
构造函数发生了异常,问君能有几多愁?
C++ 标准规定,当构造函数在执行过程中抛出异常时,由于该对象尚未完成构造,所以并不会进入正常对象的生命期,也就不存在可以被析构的对象实例,因此即使立刻退出构造函数的执行流程也不会触发系统调用该对象的析构函数。
具体描述这个过程,当构造函数抛出异常时:
系统在退出当前对象构造函数的运行栈之前,会先执行构造函数中已经被初始化过的成员对象和当前对象基类的析构函数。
注意: 在构造函数内,仍然可能会分配不受栈内管控的资源,比如通过 new 操作符在堆内分配的资源(文件句柄、socket、内存缓冲区等)。由于抛出异常导致系统在退出当前运行栈时,系统不会负责此类资源的释放,也就是会泄漏在系统内,必须另行释放。
接着,系统会跳过当前正在构造的对象的其余未执行的构造代码,由于对象未被完整构造,所以对象的析构函数也不会被调用。
异常被抛出后,系统将沿着调用栈向上查找匹配的异常处理块(catch 代码块),同时传递异常对象。
思路描述完了,来看看实际代码,定义一个将要被抛出的异常类 CustomException:
using namespace std;
class CustomException
: public std::exception {
public:
explicit CustomException(const char* message)
: msg(message) {}
virtual const char* what()
const noexcept override {
return msg.c_str();
}
private:
std::string msg;
};
再定义成员对象的类型 memberDataClass,方便演示数据成员的释放过程。
class memberDataClass
{
public:
memberDataClass(int flag1,
int flag2)
: flag1_(flag1), flag2_(flag2) {
cout << "memberDataClass con:"
<< flag1_ << flag2_ << endl;
}
~memberDataClass() {
cout << "memberDataClass des:"
<< flag1_ << flag2_ << endl;
}
private:
int flag1_, flag2_;
};
然后,定义对象的基类 baseObjClass,用于演示构造函数发生异常后基类析构函数的执行。
class baseObjClass
{
public:
baseObjClass(int flag)
: type(flag) {
cout << "baseObjClass con:"
<< type << endl;
}
~baseObjClass() {
cout << "baseObjClass des:"
<< type << endl;
}
private:
int type;
};
接着,定义对象类 objClass,继承自 baseObjClass,声明三个成员属性,分别是整形变量 type,memberDataClass 类型的实例,memberDataClass 类型的实例指针。objClass 构造函数在初始化完成成员属性后,即抛出自定义的异常信号 CustomException。
class objClass
: public baseObjClass
{
public:
objClass(int flag)
: baseObjClass(flag),
type(flag),
data(flag, 1) {
p = new memberDataClass(flag,
2);
cout << "con:"
<< type << endl;
ostringstream oss;
oss << "throw:" << type;
throw CustomException(
oss.str().c_str());
cout << "it won't be run after exception!"
<< endl;
}
~objClass() {
cout << "des:"
<< type << endl;
}
private:
int type;
memberDataClass data;
memberDataClass *p;
};
最后,实例化两个 objClass 对象,分别使用局部变量、堆内分配两种形式,同时捕获异常信号。
int main()
{
try {
objClass obj(1);
} catch(const exception& e) {
cerr << "caught excep:"
<< e.what() << '\n';
}
try {
objClass *p
= new objClass(2);
} catch(const exception& e) {
cerr << "caught excep:"
<< e.what() << '\n';
}
return 0;
}
编译程序看看输出:
baseObjClass con:1
memberDataClass con:11
memberDataClass con:12
con:1
memberDataClass des:11
baseObjClass des:1
caught excep:throw:1
baseObjClass con:2
memberDataClass con:21
memberDataClass con:22
con:2
memberDataClass des:21
baseObjClass des:2
caught excep:throw:2
可见,无论以何种方式实例化 objClass 对象,在 objClass 构造函数中抛出异常信号后,都会逆向顺序释放资源,释放成员对象实例 data,调用基类 baseObjClass 的析构函数。
但是,memberDataClass 类型的对象指针指向的堆资源没有被释放,objClass 对象的析构函数也没有被调用,明显发生了资源泄漏。
如何防止资源泄漏
上面提到如果构造函数包含了额外的堆资源分配,应确保构造函数内初始化的堆资源被正确管理,这类资源的父对象析构函数是指望不上了,必须另行释放,比如通过智能指针或 RAII 设计原则来确保这类资源能得到正确释放。
换句话来说,对于构造函数内部手动分配的额外资源,程序员仍需自己妥善清理。
关于 RAII 的概念,笔者八戒在之前的文章中多有讲解,有兴趣的读者朋友可以参考文末的阅读连接。
下面就利用智能指针来解决上面代码中没被释放的资源,简单修改如下:
class objClass
: public baseObjClass
{
public:
objClass(int flag)
: baseObjClass(flag),
type(flag),
data(flag, 1) {
p = make_unique(flag,
2);
cout << "con:"
<< type << endl;
ostringstream oss;
oss << "throw:" << type;
throw CustomException(
oss.str().c_str());
}
// ...
private:
int type;
memberDataClass data;
unique_ptr p;
};
其实就是用智能指针 std::unique_ptr 对象替换了原来的裸指针,std::unique_ptr 对象被释放后,std::unique_ptr 管理的指针所指向资源也会被同步释放。效果看执行输出:
baseObjClass con:1
memberDataClass con:11
memberDataClass con:12
con:1
memberDataClass des:12
memberDataClass des:11
baseObjClass des:1
caught excep:throw:1
baseObjClass con:2
memberDataClass con:21
memberDataClass con:22
con:2
memberDataClass des:22
memberDataClass des:21
baseObjClass des:2
caught excep:throw:2
效果一目了然,原来在堆内分配的 memberDataClass 动态实例已被释放。虽然异常发生后 objClass 对象的析构函数任然没有被调用,但已经不影响系统资源的回收了。
如果对象在抛出异常之前,除了动态分配成员对象,内部还动态初始化了一些资源,比如内存缓冲、socket、文件句柄、信号量等等,又或者执行了某些操作要求退出之前必须反向操作。
可以将这些资源或者操作集合放置于一个单独的数据成员内,类似 memberDataClass,智能指针也只需单独管理这个数据成员即可,集中管理避免分散。
关于智能指针的概念,和如何理解智能指针安全管理资源的生命周期?建议查阅「引用计数」的技术介绍,八戒在之前的几期文章里有相关内容介绍,欢迎关注我。