参考下面的程序:
void processAdoptions(istream& dataSource) {
while (dataSource) { // 如果还有数据
ALA *pa = readALA(dataSource); // 取出下一只动物
pa->processAdoption(); // 处理收养事宜
delete pa; // 删除 readALA 返回的对象
}
}
但如果此时 pa->processAdoption 抛出了一个异常,processAdoption 无法捕捉它,所以这个异常会传播到 processAdoption 的调用端。位于 pa->processAdoption 之后的所有语句都会被跳过,也意味着 pa 不会被删除。
实际上,我们只需要将“一定得执行的清理代码”放到 processAdoption 函数的某个局部对象的 destructor 内即可。因为局部对象总是会在函数结束时被析构,不论函数如何结束(唯一例外就是你调用 longjmp 而结束,正是因为这个缺点 C++ 才推出了 exception)。
在本例中,我们只需用一个“类似指针的对象”取代指针 pa 即可。“行为类似指针(单动作更多)”的对象被我们称为 smart pointer。C++ 标准库提供了一个名为 autp_ptr 的 class template,其行为正是我们所需要的。auto_ptr 看起来像这样(当然 auto_ptr 标准版更为复杂):
templat
class auto_ptr {
public:
auto_ptr(T* p = 0) : ptr(p) { } // 存储对象
~auto_ptr() { delete ptr; } // 删除对象
private:
T* ptr; // 原始指针(指向对象)
};
以 auto_ptr 对象取代原始指针,就不需再担心 heap object 没有被删除 —— 即使在 exception 被抛出的情况下。注意,auto_ptr 不适合取代数组对象的指针。现在 processAdoption 看起来像这样:
void processAdoptions(istream& dataSource) {
while (dataSource) {
auto_ptr pa(readALA(dataSource));
pa->processAdoption();
}
}
隐藏在 auto_ptr 背后的观念:以一个对象存放“必须自动释放的资源”,并依赖该对象的析构函数释放,这也可以对“以指针为本”以外的资源施行
条款 10:在 constructor 内阻止资源泄露
C++ 只会析构已构造完成的对象,对象只有在其 constructor 执行完毕才算是完全构造妥当。所以如果程序打算产生一个局部性的对象时:
void testBookEntryClass() {
BookEntry b("xxx", "xxx");
...
}
而 exception 在 b 的构造过程中被抛出,b 的 destructor 就不会被调用。
如果 destructor 被调用于一个尚未完全构造好的对象身上,这个 destructor 并不会知道该做哪些事,因为它并不知道 constructor 做到了哪一步。如果需要指示 constructor 进行到了什么程度,那么就会降低 constructor 的速度。
由于 C++ 不自动清理那些构造期间抛出 exception 的对象,所以你必须设计你的 constructor,使他们在那种情况下可以进行自我清理。
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 Image(audioClipFileName);
}
} catch (...) { // 捕捉所有的 exception
delete theImage;
delete theAudioClip;
throw; // 继续传播这个 exception
}
}
BookEntry 的 catch 语句块内的动作和其 destructor 内的动作相同,我们可以将重复代码抽出放进一个 private 的辅助函数内,然后让二者调用它。
现在思考这样一种状况,如果 theImage 和 theAudioClip 是常量指针时:
class BookEntry {
private:
Image* const theImage;
AudioClip* const theAudioClip;
};
这样的指针必须通过 BookEntry 构造函数的成员初始值列表加以初始化,因为没有其他方法可以给予 const 指针一个值。但是我们没有办法将 try 和 catch 放到一个成员初始值列表之中,一个可能的地点就是放到某些 private member function 内,让 theImage 和 theAudioClip 在其中获得初始值:
class BookEntry {
private:
...
Image* initImage(const string& imageFileName);
AudioClip* initAudioClip(const string& audioClipFileName);
};
BookEntry::BookEntry(const string& name, const string& address,
const string& imageFileName, const string& audioClipFileName)
: theName(name), theAddress(address),
theImage(initImage(imageFileName)),
theAudioClip(initAudioClip(audioClipFileName)) { }
// theImage 被首先初始化,所以即使这个初始化失败也不用担心资源泄漏
Image* BookEntry::initImage(const string& imageFileName) {
if (imageFileName != "") return new Image(imageFileName);
else return 0;
}
// theAudioClip 第二个被初始化, 所以如果在它初始化过程中抛出异常,它必须确保 theImage 的资源被释放。因此这个函数使用 try...catch
AudioClip* BookEntry::initAudioClip(const string& audioClipFileName) {
try {
if (audioClipFileName != "") {
return new AudioClip(audioClipFileName);
}
else return 0;
}
catch (...) {
delete theImage;
throw;
}
}
上述方法是一个解决方案,但是更好的解答是,接受条款 09 的忠告,将 theImage 和 theAudioClip 所指对象视为资源,交给局部对象来管理。所以可以将 theImage 和 theAudioClip 的原始指针类型改为 auto_ptr。
结论如果你以 auto_ptr 对象来取代 pointer class membe,你便免除了 exception 出现时发生泄漏的危机,不需要再 destructor 内亲自动手释放资源,并允许 const member pointer 得以和 non-const member pointer 有着一样优雅的处理方式。
条款 11:禁止异常流出 destructor 之外
有两种情况下会调用析构函数。第一种,是当对象在正常状态下被销毁;第二种,是由异常处理机制销毁。
当 destuctor 被调用时,可能有(也可能没有)一个 exception 正在作用(可以通过 uncaught_exception 查询)。所以你必须假设当时有个 exception 正在作用,并撰写你的 destructor。因为如果控制权基于 exception 的因素离开析构函数,而此时正有另一个 exception 处于作用状态,C++ 会调用 terminate 函数,直接结束程序,甚至不等局部对象被销毁。
参考下面例子:
Session::~Session() {
logDestruction(this);
}
如果 logDestruction 抛出一个异常,这个异常并不会被 Session 析构函数捕捉,所以它会被传播到析构函数的调用端。但是万一这个析构函数本身是因为其他某个异常而被调用的,那么 terminate 函数会被自动调用。阻止 logDestruction 抛出的异常传出 Session 析构函数之外的唯一方法就是使用 try/catch 语句块。
让 exception 传出 destructor 之外,之所以不好,还有第二个理由。如果异常从析构函数内抛出,而没有在当地被捕捉,哪个析构函数便没有执行完全。如果析构函数执行不全,就是没有完成它应该完成的每一件事。
总结有两个理由支持我们“权力阻止 exception 传出 destructor 之外”。第一,它可以避免 terminate 函数在 exception 传播过程的栈展开机制中被调用;第二,它可以协助确保 destructor 完成其应该完成的所有事情。
条款 12:了解 “抛出一个 exception” 与 “传递一个参数” 或 “调用一个虚函数” 之间的差异
函数参数的声明语法,和 catch 自居的声明语法十分类似:
void f(Widget w);
catch (Widget w) ...
从抛出端传递一个 exception 到 catch 字句,与从函数调用端传递一个自变量到函数参数,有相同之处,也有不同之处。
相同点函数参数和异常的传递方式都有三种:by value,by reference,by pointer。
不同点然而视你所传递的是参数还是异常,发生的事情可能完全不同。
对象从“调用端或抛出端”被转移到“参数或 catch 字句” 做法的差异首先,是控制权:当你调用一个函数,控制权最终会回到调用端(除非函数失败以至于无法返回),但是当你抛出一个异常时,控制权就不会再回到抛出端。
其次,是拷贝形式:参考下面例子
// 此函数从一个 stream 中读值到一个 Widget
istream operator>> (istream& s, Widget& w);
void passAndThrowWidget() {
Widget localWidget;
cin >> localWidget; // 传递 localWidget 到 operator>>
throw localWidget; // 抛出 localWidget 异常
}
当 localWidget 被交到 operator>> 函数中,并没有发生复制行为,而是 operator>> 内的 w 引用被绑定到 localWidget 身上。
而对于一个 exception 则有所不同,不论被捕捉的 exception 是通过值传递还是引用传递(不可能是 by pointer 方式传递 —— 那将造成类型不吻合),都会发生 localWidget 的复制行为,而交到 catch 字句手上的正是那个副本。因为一旦控制权离开 passAndThrowWidget,localWidget 便离开其生存空间,于是 localWidget 的析构函数就会被调用,此时如果传递一个引用给 catch,此字句收到的是一个被析构的 Widget。
一个对象被抛出作为异常时,总是会发生复制行为(throw by pointer 不会)。即使被抛出的对象并没有被析构的风险,复制行为还是会发生。
当对象被复制当作一个 exception,复制行为是由对象的 copy constructor 执行的,这个 copy constructor 相当于该对象的静态类型而非动态类型。例如:
class Widget { ... };
class SpecialWidget : public Widget { ... };
void passAndThrowWidget() {
SpecialWidget localSpecialWidget;
...
Widget &rw = localSpecialWidget;
throw rw; // 抛出一个类型为 Widget 的异常
}
复制动作永远是以对象的静态类型为本(条款 25,会使用一种技术,使得复制以对象的动态类型为本)。上述事实(异常会发生复制行为)也解释了传递参数和抛出异常之间的另一个不同:后者常常比前者慢(见条款 15)。
一个被抛出的对象(必为临时对象)可以简单地用 by reference 的方式捕捉,不需要以 by reference-to-const 的方式捕捉。函数调用过程中将一个临时对象传递一个 non-const reference 参数是不允许的(见条款 19),但对异常则属合法。
参考下面的例子:
catch (Widget& w) {
...
throw; // 重写抛出此异常,使它继续传播
}
catch (Widget& w) {
...
throw w; // 传播被捕捉的异常的一个副本
}
这两个 catch 语句块之间的差异就是,前者重写抛出当前的 exception,后者抛出的是当前 exception 的副本。
但是额外的复制行为除了带来性能的成本因素,还有其他的差别。第一个语句块重新抛出当前的异常,如果最初抛出的异常是 SpecialWidget,第一个语句块会传播一个 SpecialWidget 异常 —— 即使 w 的静态类型是 Widget。这是因为当此异常被重新抛出时并没有发生复制行为。第二个 catch 语句块则抛出一个新的异常,其类型总是 Widget,因为那时 w 的静态类型。
事实上 throw by pointer 相当于 pass by pointer,二者都传递指针副本。必须特别注意的是,千万不要抛出一个指向局部对象的指针,因为该局部对象会在 exception 传离其范围时被销毁
“调用者或抛出者”和“被调用者或捕捉者”之间所存在的类型吻合规则考虑下面的例子:
double sqrt(double); // 标准库函数
int i;
double sqrtOfi = sqrt(i);
由于 C++ 允许隐式转换,所以上述代码将 int 转换为 double。但是一般而言,如此的转换并不发生于 exception 与 catch 子句匹配的过程中:
void f(int value) {
try {
if (Fcn()) {
throw value; // 抛出一个 int
}
}
catch (double d) { // 捕获 double 类型的异常
...
}
}
try 语句中抛出的 int 异常,绝不会被用来捕获 double 异常的 catch 子句捕获到。
在 exception 与 catch 子句匹配的过程中,仅有两种转换可以发生。第一种,是继承架构中的类转换(一个针对 base class exception 而写的 catch 子句,可以处理类型为 derived class 的 exception)。这个规则可适用于 by value,by reference,by pointer 三种形式。
第二种,是从一个有型指针转为无型指针,所以一个针对 const void* 指针而设计的 catch 子句,可捕捉任何指针类型的异常:
catch (const void*) ... // 可捕捉任何指针类型的异常
catch 子句总是以出现顺序做匹配做匹配尝试
考虑下面的代码:
try {
...
}
catch (logic_error& ex) {
... // 这个语句块将捕获所有的 logic_error 异常, 包括它的派生类
}
catch (invalid_argument& ex) {
... // 这个块永远不会被执行因为所有的 invalid_argument 异常都被上面的 catch 子句捕获
}
与虚函数的比较
可以将此行为和调用虚函数时所发生的事情对比。虚函数采用的时 best fit(最佳吻合)策略,而 exception 处理机制遵循的是 first fit(最先吻合)策略。
总结传递对象到函数去,或是以对象调用虚函数和将对象抛出成一个 exception 之间,有 3 个主要的差异。第一,exception 对象总是会被复制,如果以 by value 方式捕捉,它们甚至被复制两次。至于传递给函数参数的对象则不一定得复制;第二,被排除成异常的对象,其被允许的类型转换动作,比被传递到函数去的对象少;第三,catch 子句以其出现于源代码的顺序被比那一其检验比对,其中第一个匹配成功者便执行,而我们以某队想调用一个虚函数,被选中执行的是那个于对象类型最佳吻合的函数,不论它是不是源代码所列的第一个。
条款 13:以 by reference 方式捕捉 exception
写一个 catch 子句时,必须指明异常对象是如何被传递到这个子句的,传递方法有:by value,by reference,by pointer。
by pointer理论上,by pinter 应该是最有效的做法,因为 throw by pinter 是唯一在搬移异常相关信息时不需复制对象的一种做法(见条款 12)。但是程序员必须有办法让 exception object 在控制权离开那个抛出指针的函数之后依然存在。global 对象以及 static 对象都没问题,但是程序员很容易忘记这项约束。
如果忘记了,catch 子句收到的指针会指向不存在的对象。另一种做法时抛出一个指针,指向一个新的 heap object:
void someFunction() {
...
throw new exception;
...
}
这避免了上述问题,但是会遇到一个更难缠的问题:它们应该删除它们获得的指针吗?如果 exception object 被分配于 heap,则必须删除,否则会造成资源泄漏。如果 exception object 没有被分配于 heap,它们就不必删除,否则会招致为未定义的行为。
此外,catch-by-pointer 和语言本身建立起来的管理所有矛盾。4 个标准的 exception 都是对象,不是对象指针。所以你无论如何必须以 by value 或 by reference 的方式捕捉它们。
by valueby value 可以消除上述 “exception 是否需要删除” 及 “与标准的 exception 不一致” 等问题。但是此时,每次 exception object 被抛出,就得复制两次(见条款 12)。此外他也会引起切个问题,因为 derived class exception object 被捕获并被视为 base class exception 者,就失去其派生成分(当虚函数被调用时,会被解析为 base class 的虚函数,而不是 derived class 的虚函数)。
by referencecatch-by-reference 不会蒙受上述的任何问题,它不像 catch-by-pointer,不会发生对象删除问题,因此也就不难捕捉标准的 exception。它和 catch-by-value 也不同,所以没有切割问题,而且 exception object 之会被复制一次。
条款 14:明智运用 exception specification
exception specification 不但是一种文档式的辅助,也是一种实践式的机制,用来规范 exception 的运用。
如果函数抛出了一个并未列于其 exception specification 的 exception,这个错误会在运行使其被检测出来,于是特殊函数 unexpected 会被自动调用。unexpected 的默认行为是调用 terminate,而 terminate 的默认行为是调用 abort,所以程序如果违反 exception specification,默认结果就是程序被终止。
考虑下述 f1 函数声明,它没有 exception specification。此函数可以抛出任何一种 exception:
exception void f1(); // 可以抛出任何东西
在考虑函数 f2,它通过它的 exception specification 声称,之抛出类型为 int 的异常:
void f2() throw(int);
在 C++ 中,f2 调用 f1 绝对合法,即使 f1 可能抛出一个异常,而该异常违反了 f2 的 exception specification:
void f2() throw(int) {
f1(); // 合法,甚至即使 f1 可能抛出 int 以外的 exception
}
避免 unexpected 的方法
为解决调用可能违反当前函数本身的异常规范的函数,将这种不一致性降至最低的一个好方式就是避免将 exception specification 放在需要类型自变量的 template 身上。参考下面的例子:
// 一个不良的 template 设计,因为它带有 exception specification
template
bool operator==(const T& lhs, const T& rhs) throw() {
return &lhs == &rhs;
}
上述 template 有一个异常规范,指明此 template 不会抛出任何异常,但是有可能 operator& 已经被某些类型重载了,而此时 operator& 就可能会抛出一个异常。这样就违反了 exception specification。
避免 unexpected 的第二个方法是:如果 A 函数内调用了 B 函数,而 B 函数没有 exception specification,那么 A 函数本身也不要设定 exception specification。
void f1(); // 没有 exception specification
void f2() throw(); // 有 exception specification
void f2() throw(int) {
f1(); // 合法,但是不知道 f1 会抛出什么错误
}
避免 unexpected 的第三个方法是:处理系统可能抛出的 exception。其中最常见的就是 bad_alloc,那是在内存分配失败时由 operator new 和 operator new[] 抛出的。
直接处理非预期的 exception有时候直接处理非预期的 exception,反而比事先预防来得简单的多。换句话说,阻止非预期的 exception 发生是不切实际的。
C++ 允许你一不同类型的 exception 取代非预期的 exception。举个例子,假设你希望所有非预期的 exception 都以 UnexpectedException object 取代:
class UnexpectedException { }
void convertUnexpected() {
throw UnexpectedException(); // 如果有一个非预期的异常被抛出,便调用此函数
}
并以 convertUnexpected 取代默认的 unexpected 函数:
set_unexpected(convertUnexpected);
一旦完成部署,任何非预期的 exception 便会导致 convertUnexpected 函数被调用,于是非预期的 exception 被一个类型为 convertUnexpected 的异常取代。
将非预期的异常转换为也给已知类型的另一个做法就是:如果非预期函数的替代者重新抛出当前异常,该异常会被标准类型 bad_exception 取代。
void convertUnexpected() {
throw UnexpectedException();
}
set_unexpected(convertUnexpected);
如果你做了上述安排,并且每一个 exception specification 都含有 bad_exception(或其基类,也就是标准类 exception),你就再也不必担心程序会在遇上非预期的 exception 时终止执行。任何非预期的 exception 都会被一个 bad_exception 取代并继续传播。
exception specification 的缺点编译器只为 exception specification 执行局部性检验,所以它们很容易被违反,当它们被违反时,往往会导致程序草草终止。
exception specification 还有另一个缺点:它们会造成当一个较高层次的调用者已经准备好要处理发生的 exception 时,unexpected 函数却被调用的现象。
条款 15:了解异常处理的成本
即使从未使用任何 exception 处理机制,也必须付出的最低消费就是,你必须付出一些空间,放置某些数据结构;你必须付出一些时间,随时保持那些数据结构的正确性。如果编译过程中加上对 exception 的支持,程序就比较大,执行时也比较慢。
exception 处理机制带来的第二种成本来自 try 语句块。只要你用上一个,也就是说一旦你决定捕捉 exception,你就得付出那样的成本。
不论 exception 处理过程需要多少成本,你都不应该付出比你该付出的部分更多。为了让 exception 的相关成本最小化,只要能够不支持 exception,编译器便不支持;请将你对 try 语句块和 exception specification 的使用限制用于非用不可的地方,并且在真正异常的情况下才抛出 exception。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)