More Effective C++读书笔记---异常

异常
如果你需要一个方法,能够通知不可被忽略的异常状态,并且搜索栈空间以便找到异常处理代码时,你还得确保局部对象的析构函数必须被调用,这时你就需要使用C++的异常处理
九、条款9--使用析构函数防止资源泄漏
1.ALA* pa = readALA( dataResource );
pa->processAdoption();
delete pa;
如果processAdoption()抛出异常,processAdoption()没有捕获异常,所以异常将传递给processAdoption的调用者。delete pa会被跳过,造成资源泄漏
堵塞很容易:
ALA* pa = readALA( dataResource );
try {
pa->processAdoption();
}
catch(...){
delete pa;
throw;
}
delete pa;
但这样的双份清除代码让人心烦且难于维护。应该用pointer-like对象(smart pointer)
auto_ptr<ALA> pa(readALA( dataResource ) );
pa->processAdoption();
2.资源应该被封装在一个对象里,遵循这个规则,你通常就能避免在存在异常环境里发生资源泄漏
十、条款10--在构造函数中防止资源泄漏
1.C++仅仅能删除被完全构造的对象(full constructed objects),只有一个对象的构造函数完全运行完毕,这个对象才被完全地构造。C++拒绝为没有完成构造操作的对象调用析构函数是有一些原因的:如果为没有完成构造操作的对象调用析构函数,析构函数如何去做?仅有的办法是在每个对象里加入一些字节来指示构造函数执行了多少步?然后让析构函数检测这些字节并判断执行哪些操作。这样的记录会减慢析构函数的运行速度,并使得对象的尺寸变大。C++避免了这种开销,但是代价是不能自动删除被部分构造的对象
2.经常用的方法是捕获所有的异常,然后执行一些清除代码,最后再重新抛出异常让它继续传递
3.SomeClass* const mptr;类似于这们的成员指针必须通过构造函数的成员初始化列表来初始化,因为再也没有其他地方可以给const指针赋值
4.如果你用对应的auto_ptr对象替代指针成员变量,就可以防止构造函数存在异常时发生资源泄露,你也不用手工在析构函数中释放资源,并且你还能象以前使用非const指针一样使用const指针,给其赋值
十一、条款11--禁止异常信息(exceptions)传递到析构函数外
1.在两种情况下会调用析构函数,一是在正常情况下删除一个对象,例如对象超出了作用域或被显式地delete;二是异常传递的堆栈辗转开解(stack-unwinding)过程中,由异常处理系统删除一个对象
2.在上述两种情况下,调用析构函数时异常可能处于激活状态也可能处于没有激活状态。遗憾的是没有办法在析构函数内部区分出这两种情况。因此写析构函数时你必须保守地假设有异常被激活。因为如果在一个异常被激活的同时,析构函数也抛出异常,并导致程序控制权转移到析构函数外,C++将调用terminate函数。这个函数的作用正如其名字所表示的:它终止你的程序的运行,而且是立即终止,甚至连局部对象都没有释放
3.不允许异常传递到析构函数外面的第二个原因:如果一个异常被析构函数抛出而没有在函数内部捕获住,那么析构函数就不会完全运行(它会停在抛出异常的那个地方上)
十二、条款12--理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
1.第一个差异:调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方
2.C++规范要求被做为异常抛出的对象必须被复制(防止超出作用域被释放),即使被抛出的对象不会被释放(如static的),也会进行拷贝操作。这表示,即使通过引用来捕获异常,也不能在cacth块中修改原对象,仅仅修改的是原对象的拷贝
3.第二个差异:抛出异常运行速度比传递参数要慢
4.在函数调用中不允许传递一个临时对象到一个非const引用类型的参数里,但是异常中却被允许
5.当抛出一个异常时,系统构造的(以后会析构掉)被抛出对象的拷贝数比以相同对象做为参数传递给函数时构造的拷贝数要多一个
6.通过指针抛出异常与通过指针传递参数是相同的。不论哪种方法都是一个指针的拷贝被传递。但,你不能认为抛出的指针是一个指向局部对象的指针,因为当异常离开局部变量的生存空间时,该局部变量已经被释放。catch子句将获得一个指向已经不存在的对象的指针。这种行为在设计时应该予以避免(注:也就是说,必须是全局的或堆中的)
7.第三个差异:在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同
如 double sqrt( double ); // from <cmath> or <math.h>
我们能这样计算一个整数的平方根:
int i;
double sqrt0fi = sqrt( i );
毫无疑问,C++允许进行从int到double的隐式类型转换。一般来说,catch子句匹配异常类型时不会进行这样的转换,如:
void f( int value )
{
 try{
  if( someFunction() ) {
   throw value;
   ...
  }
 }
 catch( double d ) {
  ...
 }
}
在try块中抛出的int异常不会被处理double异常的catch子句捕获。该子句只能捕获类型真真正正为double的异常,不进行类型转换
8.catch子句中进行异常匹配时可以进行两种类型转换:第一种是继承类与基类间的转换;第二种允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常
9.最后一点差异:catch子句匹配顺序总是取决于它们在程序中出现的顺序。与这种行为相反,当你调用一个虚拟函数时,被调用的函数位于与发出函数调用的动态类型最相近的类里。你可以这样说虚拟函数采用最优适合法,而异常处理采用最先适合法。如果一个处理派生类异常的catch子句位于处理基类异常的catch子句后面,编译器会发出警告(这样的代码在C++里通常是不合法的)。不过你最好做好预先防范:不要把处理基类异常的catch子句放在处理派生类异常的catch子句的前面
十三、条款13--通过引用(reference)捕获异常
1.通过指针方式捕获异常是最高效的(能够不拷贝对象)。但如果catch子句接收到的指针,不是全局或静态的或堆的,那简直非常糟糕。就算是建立的一个堆对象,但catch子句无法判断是否应该删除该指针?如果是在堆中建立的,那必须删除,否则会资源泄漏。如果不是在堆中建立的异常对象,他们绝对不能删除它,否则程序的行为将不可预测。这是不可能知道的。而且通过指针捕获异常也不符合C++语言本身的规范,所以最好避开它
2.通过值捕获异常时系统将对异常对象拷贝两次(一次建立临时对象,一次拷贝--条款12),而且它会产生slicing problem,即派生类的异常对象被做为基类异常对象捕获时,那它的派生类的行为就被切掉了(sliced off),这样的sliced对象实际上是一个基类对象(当一个对象通过传值方式传递给函数,也会发生一样的情况)
3.通过引用捕获异常,就不会为删除异常对象而烦恼;能够避开slicing异常对象;能够捕获标准异常类型;减少对象需要被拷贝的数目
十四、条款14--审慎使用异常规格(exception specifiations)
1.异常规格明确的描述了一个函数可以抛出什么样的异常。但是它不只是一个有趣的注释,编译器在编译时有时能够检测到异常规格的不一致。而且如果一个函数抛出一个不在异常规格范围里的异常,系统在运行时能够检测出这个错误,然后一个特殊的函数unexcepted将被调用,此函数的缺省行为是调用函数terminate,而terminate的缺省行为是调用函数abort
2.避免调用unexcepted的第一个方法:避免在带有类型参数的模板内使用异常规格
3.避免调用unexcepted的第二个方法:如果在一个函数内调用其他没有异常规格的函数时应该去除这个函数的异常规格
4.避免调用unexcepted的第三个方法:处理系统本身抛出的异常
十五、条款15--了解异常处理的系统开销
1.在理论上,异常是C++的一部分,C++编译器必须支持异常
2.为了使你的异常开销最小化,只要可能就尽量彩不支持异常的方法编译程序,把使用try块和异常规格限制在你确实需要它们的地方,并且只有在确为异常的情况下才抛出异常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值