</p> f2();} catch (const some_exception& e) { // ……
3 u# Q2 d; l; S9 k$ n } - ?+ h4 W' _4 ?6 V* Y; A; }' Y
void f2() { // ……
- T2 J9 z% J1 G a: B: x: o* a/ J f3();}
/ \2 M$ w; j8 b, Z3 i$ B7 |0 n# T void f3() { // …… 0 S! T7 P \& g/ Y6 v5 v; m
f4();}
# l1 ~- K+ E$ ?0 b- L+ I void f4() { // …… 7 h: q3 n ~0 a8 R8 |
throw some_exception();} f4惹祸,f1收场,中间f2和f3只是一脸无辜地把异常“透过去”了(在Java中尚需声明一下)--原因很可能是它们不具备足够的上下文来处理这个异常。于是,我们不用像使用返回值那样,从发生问题的地方开始,到处理问题的地方“之下”,中间每一层都要判断一下,从而写下一层层诸如:view plain x = f();if(x < 0) ) \7 s$ f8 ^5 U& Y% Z
return x;之类的语句。这也有利于逻辑分层。
# o4 n% k% B1 \8 k 值得一提的是,在异常回滚的过程中,栈上已经构造好的对象都会正常析构。当然,这要求程序员在设计类的时候要考虑“异常安全”的因素。
/ T3 { K- m) D 关于异常处理的思想和异常的使用,完全可以讲一本书。更有兴趣的朋友可以去找些相关书籍看看。 * g( s9 }5 W! M1 B m3 p+ o# O
0 s6 L- _, c, Z, n (4)事实上,C++中并非只有抛出异常的new,也有不抛异常的new,即通常所说的“nothrow new”。可以这样使用它:view plain #include // …… 8 H* a6 [& [6 N) c% S' K
T* p = new (std::nothrow) T(/* …… */);其中,nothrow是头文件中定义的一个类型为std::nothrow_t的常量,我们可以直接使用它。这时,如果内存分配失败,p的值将为空(0),且不会有异常抛出,跟C的malloc很像了。 nothrow new实际是标准库中实现的operator new和operator new[]的重载。我们也可以根据需要自己重载operator new/operator new[],可以有全局的,也可以针对某个类重载。但实践中用的不多。 4 `% e/ u; } Q; s8 z
注意,使用nothrow new创建对象时,只能保证不会因为operator new或operator new[]的失败而抛出std::bad_alloc,但难保对象的构造函数不会抛出其它异常,甚至就抛出std::bad_alloc.
+ n, c7 N) v {3 Y (5)说到C++的内存分配,还有必要提一下set_new_handler.它允许你设置一个可以在operator new和operator new[]分配内存失败时可以回调的函数。如果你觉得还有什么办法能释放一些内存的话,这个回调函数就是最后的救命稻草了。 * c' ]$ W0 u+ m" i- `( M H' O7 z
(6)话说回来,多线程程序中,尤其是所谓的worker thread中,在线程函数退出之前使用“catch(……)”捕捉一下所有异常(不止std::bad_alloc)也不是完全没用。别指望能恢复什么,只求不要因为一个线程而挂掉整个程序,同时尽量保证一下数据一致性就好。另外,也别指望catch(……)能捕获一切“问题”或“bug”,没有那么好的事情。它只能捕获C++的异常,其它的问题,比如前面提到的堆栈破坏,再比如野指针访问,哪有那么容易检测得到。
/ T. d! X2 ?" X8 m 通常一个线程crash会导致整个进程crash,有人因为这个原因而更倾向于使用多进程,尤其是在类Unix的环境中。我个人对此虽不反对也不是特别赞同,因为欠债总是要还的,这也包括“技术债务”:有bug迟早还是要解决。不过,使用多进程还有别的好处,因为进程间共享数据比同一个进程的线程之间要麻烦得多,这会“迫使”开发者做出减少共享,从而既能减少并发问题又能提高并发效率的设计。 7 ~% ?/ Y4 h/ E: U+ ]! f
(7)最后,我的另一个好朋友兼同事认为:程序crash没有那么可怕。它可能是多数客户最难以忍受的bug,但那只是源于社会心理,不见得是真正最严重的bug. |