读书笔记 effective c++ Item 8 不要让异常(exceptions)离开析构函数

简介: 1.为什么c++不喜欢析构函数抛出异常 C++并没有禁止析构函数出现异常,但是它肯定不鼓励这么做。这是有原因的,考虑下面的代码: 1 class Widget { 2 3 public: 4 5 .

1.为什么c++不喜欢析构函数抛出异常

C++并没有禁止析构函数出现异常,但是它肯定不鼓励这么做。这是有原因的,考虑下面的代码:

 1 class Widget {
 2 
 3 public:
 4 
 5 ...
 6 
 7 ~Widget() { ... } // assume this might emit an exception
 8 
 9 };
10 
11 void doSomething()
12 
13 {
14 
15 std::vector<Widget> v;
16 
17 ...
18 
19 } // v is automatically destroyed here

当vector V被销毁,V有责任将它包含的所有Widgets都销毁。假设v含有有10个Widgets对象,当销毁第一个Widgets对象时,抛出了一个异常。其余的9个仍然需要被释放掉(否则它们拥有的资源会被泄露),所以V应该触发其余9个对象所有的析构函数。但是假设在这9个析构函数调用过程中,第二个Widget的析构函数抛出了一个异常。现在有两个主动抛出的异常了,这对c++来说太多了。在两个异常同时出现的情况下,程序的执行要么终止要么产生未定义行为。在这个例子中,它会产生未定义行为。使用任何其他标准库容器(如list或set)或者TR1中的容器,甚至一个数组也将会产生同样的未定义行为。出现这种麻烦并不只是在容器或者数组中出现。在不使用容器或者数组的情况下,析构函数抛出的异常也可以使程序过早终止或者出现未定义行为。C++不喜欢析构函数发出异常!

2.一个例子-DB资源管理类

这很容易理解,但是析构函数需要执行的操作有可能由于异常被抛出而导致失败,这时候我们应该怎么做?举个例子,假设你在实现一个关于数据库连接的类:

 1 class DBConnection {
 2 
 3 public:
 4 
 5 ...
 6 
 7 static DBConnection create(); // function to return
 8 
 9 // DBConnection objects; params
10 
11 // omitted for simplicity
12 
13 void close(); // close connection; throw an
14 
15 }; // exception if closing fails

为了确保客户端不会忘记调用DBConnection对象的close函数,为DBConnestion创建一个资源管理类是一个理想的方法,close函数会在资源管理类的析构函数中被调用。这样的资源管理类将在第三章有详细的讲述,在这里,考虑这样一个类的析构函数会长成什么样子就足够了:

 1 class DBConn { // class to manage DBConnection
 2 
 3 public: // objects
 4 
 5 ...
 6 
 7 ~DBConn() // make sure database connections
 8 
 9 { // are always closed
10 
11 db.close();
12 
13 }
14 
15 private:
16 
17 DBConnection db;
18 
19 };

于是客户端代码可以写成这样:

 1 { // open a block
 2 
 3 DBConn dbc(DBConnection::create()); // create DBConnection object
 4 
 5 // and turn it over to a DBConn
 6 
 7 // object to manage
 8 
 9 ... // use the DBConnection object
10 
11 // via the DBConn interface
12 
13 } // at end of block, the DBConn
14 
15 // object is destroyed, thus
16 
17 // automatically calling close on
18 
19 // the DBConnection object

 

只要close函数的调用成功了这个实现就是很好的,但是如果调用产生一个异常,DBConn的析构函数会传播这个异常,也就是允许异常离开析构函数。这是一个问题,因为在析构函数中发生throw就意味这麻烦。

3.如何阻止析构函数中的异常被传播出去

有两种方法来避免这个麻烦。DBConn的析构函数可以这么做:

3.1用abort函数使程序终止

如果close函数抛出异常就将程序终止,可以调用abort函数:

 1 DBConn::~DBConn()
 2 
 3 {
 4 
 5 try { db.close(); }
 6 
 7 catch (...) {
 8 
 9 make log entry that the call to close failed;
10 
11 std::abort();
12 
13 }
14 
15 }

如果在执行析构函数的时候遇到一个错误程序就不能继续运行了,上面的做法会是一个合理的选择。它的优点是能够阻止异常从析构函数传播出去,传播异常会导致未定义行为。因此,对于未定义行为,调用abort能够先发制人。

3.2 将异常吞掉

将调用close时抛出的异常吞掉

 1 DBConn::~DBConn()
 2 
 3 {
 4 
 5 try { db.close(); }
 6 
 7 catch (...) {
 8 
 9 make log entry that the call to close failed;
10 
11 }
12 
13 }

在一般情况下,将异常吞掉是一个坏的方法,因为它会抑制重要错误信息-有一些失败的事情-的出现!但是有时候,比起程序过早终止或者未定义行为,将异常吞掉会是更好的方法。这是一个可行的选择,程序必须能够可靠的继续执行下去甚至在碰到错误出现然后将其忽略的情况。

这两种方法都不是特别吸引人。这两种的方法的问题是,程序没有办法在第一时间对导致close抛出异常的条件做出反应。

 

4.一个更好的方法-使类能够对异常做出反应

 

一个更好的方法是对DBConn的接口进行设计,于是客户端有机会对可能出现的问题做出反应。举个例子,DBConn类自己可以提供一个close函数,这就可以给客户端一个处理从close抛出异常的机会,同时也能够追踪DBConnection是否已经被关掉了,如果在close中没有被关掉就在析构函数中再次执行。这就阻止了连接无法被正确释放。如果在DBConn的析构函数中对close的调用将会失败,我们还得使用终止程序或者吞掉异常的方法:

 1 class DBConn {
 2 
 3 public:
 4 
 5 ...
 6 
 7 void close() // new function for
 8 
 9 { // client use
10 
11 db.close();
12 
13 closed = true;
14 
15 }
16 
17 ~DBConn()
18 
19 {
20 
21 if (!closed) {
22 
23 try { // close the connection
24 
25 db.close(); // if the client didn’t
26 
27 }
28 
29 catch (...) { // if closing fails,
30 
31 make log entry that call to close failed; // note that and
32 
33 ... // terminate or swallow
34 
35 }
36 
37 }
38 
39 }
40 
41 private:
42 
43 DBConnection db;
44 
45 bool closed;
46 
47 };

将调用close的责任从DBConn的析构函数转移到DBConn的客户端(因为DBConn的析构函数有一个“备份”调用)可能会给你肆无忌惮转移负担的印象。你可能甚至将这种做法当成Item18给出意见的反例(使接口容易被正确使用)。事实上,这两种想法都是错的。如果一个操作有可能因为抛出异常而导致失败,而我们有可能需要去处理这个异常,这个异常必须来自非析构函数才可以。因为析构函数抛出异常是很危险的,常常会导致程序过早终止或者未定义行为。在这个例子中,告诉客户端自己调用close函数并没有给它们增加负担;这反而给了它们一个处理错误的机会,否则就没有机会对错误做出反应了。如果他们发现这个机会没有什么用(可能因为他们相信没有错误会发生),他们可以忽略它,仅依靠DBConn的析构函数在调用close。如果这时出现了错误-close确实抛出了异常-他们没有资格抱怨DBConn吞掉了异常或者终止了程序。毕竟,他们原来有机会处理这个问题,但是他们没有这么做。 

5.总结

  • 析构函数不能够发出任何异常。如果在析构函数中调用某个函数可能会发生throw,析构函数应该catch所有异常然后吞掉他们或者终止程序。
  • 如果类的客户端需要对一个操作的异常throw做出反应,这个类应该提供一个普通函数来执行这个操作。


作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

目录
相关文章
|
4月前
|
安全 编译器 C++
C++异常详细介绍
C++异常详细介绍
339 10
|
安全 程序员 编译器
【C++】异常
C++异常处理机制允许在程序运行时出现错误时,通过`try`、`catch`和`throw`关键字将错误信息传递回调用栈,进行异常处理。它支持异常的重新抛出、自定义异常体系以及标准库提供的异常类层次结构,如`std::exception`及其派生类。异常处理提高了代码的健壮性和可维护性,但也带来了性能开销和代码复杂性等问题。合理使用异常机制,可以有效提升程序的稳定性和安全性。
271 3
|
6月前
|
存储 监控 算法
基于跳表数据结构的企业局域网监控异常连接实时检测 C++ 算法研究
跳表(Skip List)是一种基于概率的数据结构,适用于企业局域网监控中海量连接记录的高效处理。其通过多层索引机制实现快速查找、插入和删除操作,时间复杂度为 $O(\log n)$,优于链表和平衡树。跳表在异常连接识别、黑名单管理和历史记录溯源等场景中表现出色,具备实现简单、支持范围查询等优势,是企业网络监控中动态数据管理的理想选择。
190 0
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
812 4
|
C++ 运维
开发与运维函数问题之析构函数在C++类中起什么作用如何解决
开发与运维函数问题之析构函数在C++类中起什么作用如何解决
124 11
C++ 异常机制问题之捕获异常的问题如何解决
C++ 异常机制问题之捕获异常的问题如何解决
176 2
|
安全 Java 程序员
【C++11】异常知多少
【C++11】异常知多少
146 7
|
C++ 容器
【C++】拷贝构造函数、拷贝赋值函数与析构函数
【C++】拷贝构造函数、拷贝赋值函数与析构函数
372 6
|
安全 编译器 C++
C++一分钟之-构造函数与析构函数
【6月更文挑战第20天】C++中的构造函数初始化对象,析构函数负责资源清理。构造函数有默认、参数化和拷贝形式,需注意异常安全和成员初始化。析构确保资源释放,避免内存泄漏,要防止重复析构。示例代码展示了不同构造函数和析构函数的调用情况。掌握构造和析构是有效管理对象生命周期和资源的关键。
268 2
|
安全 Java 程序员