异常
条款9:利用destructors避免泄露资源
对于下面的函数,如果pa->processAdoption();
发生异常时,异常会传播到processAdoptions
的调用端,这意为着pa不会被删除
void processAdotions(istream& dataSource)
{
while(dataSource)
{
ALA *pa = readALA(dataSource);
pa->processAdotion();
delete pa;
}
}
为了避免这种情况,可以通过捕获异常的方法,避免资源泄露。
void processAdotions(istream& dataSource)
{
while(dataSource)
{
ALA *pa = readALA(dataSource);
try
{
pa->processAdotion();
}
catch(...)
{
delete pa;
throw;
}
delete pa;
}
}
这样的代码被try和catch语句搞得乱七八糟,既不美观,而且还有撰写可被正常和异常路线清理的代码(比如上面的两个delete pa)。可以通过将“一定得执行的清理代码”移动到函数的某个局部对象的析构函数内即可,因为局部对象总是会在函数结束时被析构,通常的解决办法是以一个“类似指针的对象”取代指针pa。如此这个类似指针的对象被销毁时,我们可以在其析构中调用delete,这个行为类似指针的对象,我们一般称其为智能指针。
template<class T>
class auto_ptr {
public:
auto_ptr(T *p = 0): ptr(p) {}
~auto_ptr() { delete ptr; }
private:
T * ptr;
};
//以auto_ptr取代原始指针后,processAdoptions看起来像这样
void processAdotions(istream& dataSource)
{
while(dataSource)
{
autor_ptr<ALA>(readALA(dataSource));
pa->processAdotion();
}
}
隐藏在auto_ptr背后的观念是以一个对象存放“必须自动释放的资源”,并依赖该对象的析构释放。只要坚持把资源封装在对象内部,通常便可在exceptions出现时避免资源泄露。
条款10:在constructors内阻止资源泄露
在一个对象的构造函数中,如果存在多个new的行为,当后面的new或其他行为引发异常时,会造成资源泄露。这是因为c++只会析构已经完成构造的对象,如果是在对象的构造函数中发生的异常,程序不会调用其析构函数。
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
:theName(name), theAddress(address), theImage(0), theAudioClip(0)
{
if(imageFileName != "")
{
theImage = new Image(imageFileName);
}
if(audioClipFileName != "")
{
theAudioClip = new AudioClip(audioClipFileName);
}
}
可以通过对构造函数中可能引发异常的函数添加try catch,并销毁需要处理的资源,然后再抛出对应的异常。
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
:theName(name), theAddress(address), theImage(0), theAudioClip(0)
{
try
{
if(imageFileName != "")
{
theImage = new Image(imageFileName);
}
if(audioClipFileName != "")
{
theAudioClip = new AudioClip(audioClipFileName);
}
}
catch(...)
{
delete theImage;
delete theAudioClip;
throw;
}
}
但当需要处理的资源变为常量指针则会出现问题,因为常量指针成员变量只能通过构造函数的成员初值列表初始化(也可以通过默认成员初始化器来初始化),这时的try catch就无法用了,因为成员初值列表只支持表达式,而try catch是语句。处理这个问题的思路是将资源的构造放到对象的私有成员函数中,不过这也会带来维护上的困难,因为构造函数的动作散落各处。
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
:theName(name), theAddress(address), theImage(initImage(imageFileName)), theAudioClip(initAudioClip(audioClipFileName))
{
}
Image* BookEntry::initImage(const std::string& imageFileName)
{
if(imageFileName != "")
return new Image(imageFileName);
return 0;
}
//这里要将theImage的资源释放掉(至于谁去释放谁,要看定义成员变量的顺序,后面的释放前面的,和初始化列表顺序无关)
AudioClip* BookEntry::initAudioClip(const std::string& audioClipFileName)
{
try
{
if(audioClipFileName != "")
return new AudioClip(audioClipFileName);
return 0;
}
catch(...)
{
delete theImage;
throw;
}
}
一个更好的办法是,将theImage和theAudioClip所指对象视为资源,交给局部对象来管理。就算theAudioClip初始化期间有任何异常抛出,theImage也是个完整的构造好的对象,所以他会自动销毁。
条款11:禁止异常流出destructors之外
有两种情况下析构函数会被调用。第一种是对象在正常状态下被销毁,也就是当它离开了它的生存空间或者是被明确的删除了;第二种情况是当对象被异常处理机制---也就是异常传播过程中的栈展开机制销毁。当析构函数被调用时,可能(也可能不)有一个异常正在作用之中,如果控制权基于异常的因素离开析构,而此时正有另一个异常作用状态,c++会调用terminate。(就是说异常触发的析构,如果再抛异常,就会终止程序)。
可以通过在析构函数中加入try catch的方法避免析构函数向外界传播异常。
禁止异常流出destructors之外也是为了确保析构函数完成其该完成的事情。
条款12:了解“抛出一个exception”与“传递一个参数”或“调用一个虚函数”之间的差异
函数参数和异常传递方式有3种:值传递、引用传递、指针传递。然而情况却完全不同,因为调用函数时,控制权最终会回到调用端,但抛异常控制权则不会回到抛出端。无论被捕获的异常是以值传递还是以引用传递,都会发生复制行为,这就是为什么C++规定,一个对象被抛出作为exception时,总会发生复制,即使是静态变量。当对象被复制当作一个exception,复制行为是由对象的拷贝构造器执行的,这个拷贝构造相应的是对象的“静态类型”而非“动态类型”
class Widget { ... };
class SpecialWidget : pulic Widget { ... };
void passAndThrowWidget()
{
SpecialWidget localSpecialWidget;
Widget& rw = localSpecialWidget;
throw rw;//这里会抛出一个Widget类型对象(即使强转也没有SpecialWidget的数据)
}
exceptions对象是其他对象的副本,会让你觉得下面的行为是相同的。前者是重新抛出当前的异常,后者则是抛出当前异常的副本。
catch(Widget& w)
{
...
throw;
}
catch(Widget& w)
{
...
throw w;
}
区别在于第一个语句重新抛出的异常,不论气类型为何,例如最初抛出的类型是specialWidget的话,依旧抛出specialWidget,而第二个因为发生了静态类型的复制,所以其抛出的是新的Widget类型的exception。所以通常选第一个。
函数参数和exception的区别还在于,函数调用过程不允许将临时对象传递给一个非常量引用参数,但exception是合法的。千万不要抛出一个指向局部对象的指针,这是因为局部对象在exception传离其scope时被销毁。
函数参数和exception的区别还在于类型匹配,int类型的exception绝对不会用来捕获double类型的exception。exception与catch子句相匹配的过程中,仅有两种转换可以发生,一是“继承架构中的类转换”,一个针对base class exception而编写的catch子句,可以捕获类型为derived class类型的exception。第二种则是,允许发生转换是从一个**“有型指针”转为“无型指针”**,一个针对const void*指针设计的catch子句,可以捕获任意指针类型的exception。
函数参数和exception的区别还在于,catch子句总是依据出现顺序做匹配尝试,也就是base class exception的catch子句在前面的话会被直接调用,这可能会导致后面的derived class类型的exception的子句无法调用到,而虚函数遵循的是最优原则,即找到一个最吻合的。
条款13:以by reference方式捕获exceptions
通过指针传递异常应该是最有效率的一种方法,因为它不需要复制exceptions对象,这只适用于全局或者静态对象,并不适用于局部对象。或者抛出一个指向新的heap object的指针,但这会有一个更难缠的问题,就是是否删除获得的指针?而标准的exceptions都是对象,无论如何只能通过by value或者by reference的方式传递。by value的缺点在于抛出异常时的两次复制,此外还会引起切割问题。因此只剩下一个选择了,就是by reference。
by reference可以避开对象删除问题;可以避开exception objects的切割问题;可以保留捕捉标准exception的能力;约束了exception objects需要复制的次数;
条款14:明智运用exception specifications
exception specifications可以让代码更容易被理解,因为它明确指出了一个函数可以抛出什么样的exceptions。虽然编译器会检查函数抛出的异常类型是否与specifications相匹配,但是编译器只做局部检查,
extern void f1();//可能抛出任何异常
void f2() throw(int)//只抛出int类型异常
{
f1();//是合法的
}
为了避免exception specifications的不一致性
第一是不要将templates和exception specifications混合使用,因为没有任何一个办法知道模板参数类型可能抛出什么异常。
第二是如果函数A调用了B,B函数是没有exception specifications的,那么A函数也不要设定exception specifications。
第三是处理系统可能抛出的exceptions。比如在operator new可能会抛出bad_alloc,则在函数内使用new要有心理准备。
C++允许你以不同类型的exceptions,取代非预期的exceptions。可以使用set_unexpected函数进行设置。(C++17移除)
条款15:了解异常处理(exception handling)的成本
为了能够在运行时处理exception,程序需要做大量的笔记工作,包括对每一个执行点,必须确认如果发生异常,哪些对象需要析构,针对于每个try语句要记录子句能够处理的类型。即使从未使用任何exception也要付出一些空间,放置某些数据结构(比如记录哪些对象已被完全构造);付出一些时间,随时保证那些数据结构的正确性。不同编译器对try语句块的处理各不相同,通常会使代码膨胀5%~10%。所以应将try catch和exception specifications局限于非要用不可的场景。
Member discussion