C++基础教程面向对象(学习笔记(80))

异常危险和缺点

与几乎所有有益的东西一样,异常也存在一些潜在的缺点。本文并不是全面的,只是指出在使用异常(或决定是否使用它们)时应该考虑的一些主要问题。

清理资源

使用异常时新程序员遇到的最大问题之一是在发生异常时清理资源的问题。请考虑以下示例:

try
{
    openFile(filename);
    writeFile(filename, data);
    closeFile(filename);
}
catch (FileException &exception)
{
    cerr << "Failed to write to file: " << exception.what() << std::endl;
}

如果WriteFile()失败并抛出FileException会发生什么?此时,我们已经打开了文件,现在控制流跳转到FileException处理程序,该处理程序打印错误并退出。请注意,该文件从未关闭!此示例应重写如下:

try
{
    openFile(filename);
    writeFile(filename, data);
    closeFile(filename);
}
catch (FileException &exception)
{
    // 确保文件关闭
    closeFile(filename);
    // 然后写入错误
    cerr << "Failed to write to file: " << exception.what() << std::endl;
}

在处理动态分配的内存时,这种错误通常以另一种形式出现:

try
{
    Person *john = new Person("John", 18, PERSON_MALE);
    processPerson(john);
    delete john;
}
catch (PersonException &exception)
{
    cerr << "Failed to process person: " << exception.what() << '\n';
}

如果processPerson()抛出异常,控制流将跳转到catch处理程序。结果,john永远不会被取消分配!这个例子比前一个更棘手 - 因为john是try块的本地例子,当try块退出时它超出了范围。这意味着异常处理程序根本无法访问john(它已被销毁),因此无法释放内存。

但是,有两种相对简单的方法可以解决这个问题。首先,在try块之外声明john,这样当try块退出时它不会超出范围:

Person *john = NULL;
try
{
    john = new Person("John", 18, PERSON_MALE);
    processPerson(john);
    delete john;
}
catch (PersonException &exception)
{
    delete john;
    cerr << "Failed to process person: " << exception.what() << '\n';
}

因为john是在try块之外声明的,所以在try块和catch处理程序中都可以访问它。这意味着catch处理程序可以正确地进行清理。

第二种方法是使用一个类的局部变量,该类知道如何在超出范围时清理自身(通常称为“智能指针”。标准库提供了一个名为std :: unique_ptr的类,可用于此目的 .std :: unique_ptr是一个包含指针的模板类,当它超出范围时会释放它。)

#include <memory>; // std::unique_ptr
 
try
{
    Person *john = new Person("John", 18, PERSON_MALE);
    unique_ptr<Person> upJohn(john); // upJohn now owns john
 
    ProcessPerson(john);
 
    // 当upJohn超出范围时,它将删除john
}
catch (PersonException &exception)
{
    cerr << "Failed to process person: " << exception.what() << '\n';
}

我们将在下一章中详细讨论智能指针。

异常和破坏

与构造函数不同,抛出异常可能是表明对象创建不成功的有用方法,因此绝不应在析构函数中抛出异常。

在堆栈展开过程中从析构函数抛出异常时会发生此问题。如果发生这种情况,则编译器处于不知道是继续堆栈展开过程还是处理新异常的情况。最终结果是您的程序将立即终止。

因此,最好的做法是完全避免在析构函数中使用异常。请将消息写入日志文件。

性能问题

异常确实需要支付较低的性能价格。它们会增加可执行文件的大小,并且由于必须执行额外的检查,它们也可能导致它运行得更慢。但是,异常的主要性能损失发生在实际抛出异常时。在这种情况下,必须展开堆栈并找到适当的异常处理程序,这是一个相对昂贵的操作。

需要注意的是,一些现代计算机体系结构支持称为零成本异常的异常模型。零成本异常(如果支持)在非错误情况下没有额外的运行时成本(我们最关心的是性能)。但是,在发现异常的情况下,它们会收到到更大的成本。

那我什么时候应该使用异常?

如果满足以下所有条件,则最好使用异常处理:

#正在处理的错误可能很少发生。
#错误很严重,否则执行无法继续。
#无法在发生错误的地方处理错误。
#没有一种好的替代方法可以将错误代码返回给调用者。
作为一个例子,让我们考虑你编写了一个函数,希望用户传入磁盘上文件名的情况。您的函数将打开此文件,读取一些数据,关闭文件,并将一些结果传回给调用者。现在,假设用户传入了一个不存在的文件名或一个空字符串。这是一个异常的好候选人吗?

在这种情况下,上面的前两个点很容易满足 - 这不会经常发生,并且当它没有任何数据可用时,你的函数无法计算结果。该函数也无法处理错误 - 重新提示用户输入新文件名不是函数的作用,这可能甚至不合适,具体取决于程序的设计方式。第四个点是关键 - 是否有一种很好的替代方法可以将错误代码返回给调用者?这取决于您的计划的细节。如果是这样(例如,您可以返回空指针或状态代码来指示失败),那可能是更好的选择。如果没有,那么异常是合理的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章