当前位置: 首页 > news >正文

More Effective C++:异常

异常处理的重要性:

强制性:异常不能被忽略,这与传统的错误代码不同,后者可能被忽略。

安全性:异常确保了错误状态被正确处理,而非让程序继续运行在一个不确定的状态下。

异常安全编程:

异常安全的程序不是偶然形成的,而是通过精心设计得到的。就像一个多线程环境下的程序需要专门设计一样,异常安全的程序也需要特别考虑。

(1)确保资源正确释放,即使在发生异常的情况下也是如此。

(2)在异常被抛出后,程序应该能够保持一致的行为,即要么完全成功,要么完全回滚到之前的状态。

Item M9:使用析构函数防止资源泄漏

使用裸指针来管理动态分配的资源可能会导致资源泄漏,尤其是在异常抛出的情况下。为了避免这种情况,可以利用对象的生命周期和析构函数来自动化资源的清理过程。这种技术的核心在于资源获取即初始化(RAII),它确保资源在其管理对象的生命周期内被妥善管理和释放。

例子场景

基类 ALA(动物)和两个派生类 Puppy(小狗)和 Kitten(小猫),每个类都有一个processAdoption 方法来处理领养流程。在处理领养过程中,如果使用裸指针来管理动态创建的对象,那么当 processAdoption 方法抛出异常时,可能会导致内存泄漏,因为后续的 delete 操作会被跳过。

解决方案

使用try-catch块:可以在调用 processAdoption 时包裹一个try-catch块,确保即使抛出异常也能正确释放资源。

void processAdoptions(istream& dataSource) {while (dataSource) {ALA *pa = readALA(dataSource);try {pa->processAdoption();} catch (...) {delete pa; // 避免内存泄漏throw; // 重新抛出异常}delete pa; // 正常情况下的资源释放}
}

使用智能指针:使用C++标准库提供的 std::auto_ptr 或者更现代的 std::unique_ptr 来自动管理资源。这些智能指针会在其生命周期结束时自动调用 delete,从而防止资源泄漏。

#include <memory>  // 引入 std::unique_ptr
void processAdoptions(istream& dataSource) {while (dataSource) {std::unique_ptr<ALA> pa(readALA(dataSource));pa->processAdoption();  // 不需要额外的 try-catch 或 delete}
}

RAII技术的应用:除了内存管理外,RAII还可以应用于其他类型的资源,如窗口句柄。可以定义一个类来封装窗口的创建和销毁。

class WindowHandle {
public:explicit WindowHandle(WINDOW_HANDLE handle) : w(handle) {}~WindowHandle() { destroyWindow(w); }operator WINDOW_HANDLE() const {return w;}
private:WINDOW_HANDLE w;
};
void displayInfo(const Information& info) {WindowHandle w(createWindow());// 在 w 对应的窗口中显示信息
}

这种方式同样确保了即使在异常情况下,窗口资源也会被正确地销毁。通过使用智能指针和RAII模式,我们可以有效地防止由于异常导致的资源泄漏。

Item M10:在构造函数中防止资源泄漏

在C++中,构造函数可能抛出异常,导致对象的部分构造。这种情况下,C++不会自动调用析构函数来清理已分配的资源,从而可能导致资源泄漏。为了解决这个问题,需要在构造函数内部适当地管理资源,确保即使在异常情况下也能正确释放资源。

假设我们正在开发一个多媒体通讯录程序,其中每个条目可以包含文字信息(如姓名、地址)、照片和声音片段。我们定义了以下类:

Image:用于处理图像数据。

AudioClip:用于处理声音数据。

PhoneNumber:用于存储电话号码。

BookEntry:通讯录中的条目,包含姓名、地址、电话号码、照片和声音片段。

初始的 BookEntry 构造函数和析构函数如下:

class BookEntry {
public:BookEntry(const string& name,const string& address = "",const string& imageFileName = "",const string& audioClipFileName = "");~BookEntry();void addPhoneNumber(const PhoneNumber& number);
private:string theName;string theAddress;list<PhoneNumber> thePhones;Image* theImage;AudioClip* theAudioClip;
};
BookEntry::BookEntry(const string& name,const string& address,const string& imageFileName,const string& audioClipFileName): theName(name), theAddress(address), theImage(nullptr), theAudioClip(nullptr) {if (imageFileName != "") {theImage = new Image(imageFileName);}if (audioClipFileName != "") {theAudioClip = new AudioClip(audioClipFileName);}
}
BookEntry::~BookEntry() {delete theImage;delete theAudioClip;
}

如果在构造函数中抛出异常(例如,new 失败或 Image/AudioClip 构造函数抛出异常),BookEntry 对象将不会完全构造,析构函数不会被调用,从而导致资源泄漏。

解决方案

使用 try-catch 块

在构造函数中使用 try-catch 块来捕获异常,并在捕获异常后手动释放资源,然后再重新抛出异常。

BookEntry::BookEntry(const string& name,const string& address,const string& imageFileName,const string& audioClipFileName): theName(name), theAddress(address), theImage(nullptr), theAudioClip(nullptr) {try {if (imageFileName != "") {theImage = new Image(imageFileName);}if (audioClipFileName != "") {theAudioClip = new AudioClip(audioClipFileName);}} catch (...) {delete theImage;delete theAudioClip;throw;}
}

使用辅助函数

将资源释放的代码提取到一个私有辅助函数 cleanup 中,供构造函数和析构函数共同使用。

class BookEntry {
public:BookEntry(const string& name,const string& address = "",const string& imageFileName = "",const string& audioClipFileName = "");~BookEntry();void addPhoneNumber(const PhoneNumber& number);
private:string theName; string theAddress;list<PhoneNumber>thePhones;Image* theImage;AudioClip* theAudioClip;void cleanup();
};
void BookEntry::cleanup() {delete theImage;delete theAudioClip;
}
BookEntry::BookEntry(const string& name,const string& address,const string& imageFileName,const string& audioClipFileName): theName(name), theAddress(address), theImage(nullptr), theAudioClip(nullptr) {try {if (imageFileName != "") {theImage = new Image(imageFileName);}if (audioClipFileName != "") {theAudioClip = new AudioClip(audioClipFileName);}} catch (...) {cleanup();throw;}
}
BookEntry::~BookEntry() {cleanup();
}

使用智能指针

使用 std::auto_ptr 或 std::unique_ptr 来管理资源,从而自动处理资源的释放。

#include <memory>
class BookEntry {
public:BookEntry(const string& name,const string& address = "",const string& imageFileName = "",const string& audioClipFileName = "");~BookEntry();void addPhoneNumber(const PhoneNumber& number);
private:string theName;string theAddress;list<PhoneNumber> thePhones;std::unique_ptr<Image> theImage;std::unique_ptr<AudioClip> theAudioClip;
};
BookEntry::BookEntry(const string& name,const string& address,const string& imageFileName,const string& audioClipFileName): theName(name),theAddress(address),theImage(imageFileName != "" ? new Image(imageFileName) : nullptr),theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName) : nullptr) {
}
BookEntry::~BookEntry() {// 无需手动删除,智能指针会自动处理
}

通过在构造函数中使用 try-catch 块和智能指针,我们可以确保即使在异常情况下也能正确释放资源。智能指针(如 std::unique_ptr)提供了一种更加简洁和安全的方式来管理动态分配的资源,避免了手动管理指针带来的复杂性和潜在的错误。

Item M11:禁止异常信息传递到析构函数外

析构函数在两种情况下会被调用:一是对象正常离开作用域或被显式删除时;二是在异常处理过程中,异常堆栈展开。

析构函数可能会在异常激活的状态下调用。如果析构函数本身抛出异常,程序将调用 std::terminate 函数,这会导致程序立即终止,而不执行任何进一步的清理工作。析构函数不应抛出异常,以确保程序的健壮性和资源的正确释放。

假设有一个 Session 类,用于跟踪在线会话的创建和销毁时间。需要记录会话的创建和销毁信息。

初始的 Session 类及其析构函数如下

#include<iostream>
class Session {
public:Session();~Session();
private:static void logCreation(Session *objAddr);static void logDestruction(Session *objAddr);
};
Session::Session() {logCreation(this);
}
Session::~Session() {logDestruction(this);
}
void Session::logCreation(Session *objAddr) {// 记录会话创建std::cout << "Session created at address: " << objAddr << std::endl;
}
void Session::logDestruction(Session *objAddr) {// 记录会话销毁std::cout << "Session destroyed at address: " << objAddr << std::endl;
}

如果 logDestruction 抛出异常,异常将传递到 Session 的析构函数外部。如果此时正处于异常堆栈展开过程中,std::terminate 将被调用,程序将立即终止,导致资源泄漏或其他未定义行为。

解决方案

使用 try-catch 块捕获异常

在析构函数中使用 try-catch 块来捕获并处理可能的异常,确保异常不会传递到析构函数外部。

Session::~Session() {try {logDestruction(this);} catch (...) {// 忽略异常,防止异常传递到析构函数外部}
}

确保析构函数完成所有必要的清理工作。如果析构函数中有多个操作,确保这些操作都能完成,即使其中一个操作抛出异常。

#include <iostream>
class Session {
public:Session();~Session(); //其他成员函数
private:static void logCreation(Session *objAddr);static void logDestruction(Session *objAddr);void startTransaction();void endTransaction();
};
Session::Session() {logCreation(this);startTransaction();
}
Session::~Session() {try {logDestruction(this);endTransaction();} catch (...) {// 忽略异常,防止异常传递到析构函数外部}
}

使用 try-catch 块:

在析构函数中使用 try-catch 块捕获并处理可能的异常,确保异常不会传递到析构函数外部。这样可以防止 std::terminate 被调用,从而避免程序立即终止。

确保所有操作完成:

在析构函数中,如果有多个操作需要完成,确保这些操作都能完成,即使其中一个操作抛出异常。通过这种方式,即使 logDestruction 或 endTransaction 抛出异常,析构函数也能完成必要的清理工作。

Item M12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异

在C++中,抛出异常、传递参数和调用虚函数虽然在语法上看起来有些相似,但它们在实际操作过程中有着显著的区别。这些区别主要体现在以下几个方面:对象拷贝、类型转换以及匹配规则。

相同点:无论是传递参数还是抛出异常,都可以通过值、引用或指针来进行。两者都可以使用引用或指针来避免不必要的拷贝。

(这条没有看懂)

差异

对象拷贝

传递参数:当通过值传递参数时,会创建参数的一个副本。通过引用或指针传递则不会创建副本。

抛出异常:抛出异常时,总是会创建异常对象的一个副本,即使通过引用捕获也是如此。这是因为异常对象在其抛出点可能已经离开了作用域,需要确保在捕获点有一个有效的副本。

当在C++中抛出异常时,系统会创建异常对象的一个副本,即使在捕获异常时使用的是引用。这样做的目的是确保在异常传播过程中,即使原始异常对象已经离开其作用域,仍然有一个有效的异常对象副本可以被处理。

#include <iostream>
#include <stdexcept>
class MyException : public std::exception {
public:MyException(const char* msg) : message(msg) {std::cout << "MyException constructor called" << std::endl;}MyException(const MyException& other) : message(other.message) {std::cout << "MyException copy constructor called" << std::endl;}const char* what() const noexcept override {return message;}
private:const char* message;
};
void throwException() {MyException e("An error occurred");std::cout << "Throwing exception" << std::endl;throw e;  // 抛出异常
}
int main() {try {throwException();} catch (const MyException& e) {  // 通过引用捕获异常std::cout << "Caught exception: " << e.what() << std::endl;}return 0;
}

自定义异常类MyException 类继承自 std::exception,并重写了 what() 方法。MyException 有一个构造函数和一个拷贝构造函数,用于输出构造和拷贝构造的调用信息。

抛出异常:throwException 函数中,创建了一个 MyException 对象 e。

通过 throw e; 语句抛出异常。这时,系统会调用 MyException 的拷贝构造函数,创建 e 的一个副本,并将这个副本传递给异常处理机制。

捕获异常:

main 函数中的 try 块调用了 throwException 函数。当 throwException 抛出异常时,控制权会转移到 catch 块。catch (const MyException& e) 通过引用捕获异常。尽管这里使用的是引用,但实际上捕获的是在抛出点创建的副本。

类型转换

传递参数:C++允许隐式的类型转换,例如从int到double。

抛出异常:异常捕获时不允许隐式类型转换。唯一的例外是基类和派生类之间的转换,以及从类型化的指针到const void*的转换。

匹配规则

传递参数:虚函数调用遵循动态绑定,即根据对象的实际类型调用相应的虚函数。

抛出异常:异常捕获遵循顺序匹配规则,即按照catch子句在代码中出现的顺序进行匹配,第一个匹配成功的catch子句将被调用。

#include <iostream>
#include <stdexcept>
#include <cmath>
class Widget {
public:Widget() { std::cout << "Widget constructed\n"; }Widget(const Widget&) { std::cout << "Widget copy constructed\n"; }~Widget() { std::cout << "Widget destructed\n"; }
};
class SpecialWidget : public Widget {
public:SpecialWidget() { std::cout << "SpecialWidget constructed\n"; }SpecialWidget(const SpecialWidget&) { std::cout << "SpecialWidget copy constructed\n"; }~SpecialWidget() { std::cout << "SpecialWidget destructed\n"; }
};
void passAndThrowWidget() {Widget localWidget;std::cin >> localWidget; // 传递 localWidget 到 operator>>throw localWidget; // 抛出 localWidget 异常
}
void passAndThrowSpecialWidget() {SpecialWidget localSpecialWidget;Widget& rw = localSpecialWidget; // rw 引用 SpecialWidgetthrow rw; // 抛出一个类型为 Widget 的异常
}
void testExceptionHandling() {try {passAndThrowWidget();} catch (Widget& w) {std::cout << "Caught Widget by reference\n";}try {passAndThrowSpecialWidget();} catch (Widget& w) {std::cout << "Caught Widget by reference\n";}
}
void testRethrow() {try {throw std::runtime_error("An error occurred");} catch (std::exception& e) {std::cout << "Caught an exception: " << e.what() << "\n";throw; // 重新抛出当前异常}
}
int main() {testExceptionHandling();testRethrow();return 0;
}

对象拷贝

passAndThrowWidget函数中,localWidget被抛出时,会创建一个副本。即使通过引用捕获异常,也会创建一个副本。在passAndThrowSpecialWidget函数中,尽管rw引用的是SpecialWidget,但抛出的是Widget类型的异常。这是因为rw的静态类型是Widget。

类型转换

在testExceptionHandling函数中,passAndThrowSpecialWidget抛出的Widget异常可以被Widget&的catch子句捕获,但不能被SpecialWidget&的catch子句捕获,除非明确指定。

通过const void*可以捕获任何指针类型的异常。

匹配规则

在testRethrow函数中,throw;重新抛出当前异常,保持其原始类型。而throw e;会创建一个新的异常对象,其类型为e的静态类型。

(1)对象拷贝:异常抛出时总是会创建副本,而参数传递可以根据传递方式决定是否创建副本。

(2)类型转换:异常捕获时不允许隐式类型转换,只有基类与派生类之间的转换以及从类型化的指针到const void*的转换是允许的。

(3)匹配规则:异常捕获遵循顺序匹配规则,而虚函数调用遵循动态绑定。

Item M13:通过引用(reference)捕获异常

通过指针捕获异常:这种方式看似高效,因为它避免了对象的拷贝。如果抛出的是局部对象的指针,那么当控制流离开抛出异常的函数时,该对象会被销毁,导致指针悬空。如果抛出的是动态分配的对象指针,那么需要在catch块中负责清理(删除)该对象,这容易被遗忘,造成内存泄漏。

标准异常类型(如std::bad_alloc等)不是指针,因此不能通过指针来捕获。

(1)通过值捕获异常:这种方式解决了通过指针捕获的问题,但是它会导致异常对象被拷贝两次,并且如果抛出的是派生类对象而捕获的是基类,则会发生对象切割(slicing),丢失派生类特有的部分。

(2)通过引用捕获异常:这是推荐的做法,因为它避免了对象的拷贝,不会发生对象切割,可以正确地处理继承关系,并且可以直接捕获标准异常类型。

//基类异常
class MyException : public std::exception {
public:virtual const char* what() const throw() {return "MyException occurred";}
};
//派生类异常
class DerivedException : public MyException {
public:const char* what() const throw() override {return "DerivedException occurred";}
};

//抛出异常的函数
void someFunction(bool shouldThrow) {if (shouldThrow) {throw DerivedException();}
}
//主函数
int main() {try {someFunction(true); // 抛出异常} catch (const MyException& e) { // 通过引用捕获异常std::cerr << e.what() << '\n'; // 输出派生类的 what()}return 0;
}

someFunction函数会在特定条件下抛出一个DerivedException对象。在main函数中,我们通过catch (const MyException& e)语句来捕获异常。由于我们使用了引用,所以即使捕获的是基类MyException,实际调用的还是DerivedException的what()方法,这样就避免了对象切割的问题。同时,异常对象只被拷贝了一次,提高了性能。

Item M14:审慎使用异常规格(exception specifications)

C++中异常规格的使用及其潜在的问题。异常规格是一种在函数声明中指定函数可能抛出哪些异常类型的机制。虽然异常规格看起来很有吸引力,因为它提供了关于函数行为的额外文档,但实际上它们可能会引发一系列问题。

异常规格的主要问题

(1)意外的程序终止:如果一个函数抛出了不在其异常规格中列出的异常,系统会调用unexpected函数,该函数默认会调用terminate,最终可能导致程序非正常终止,而不会释放栈上的局部变量。

(2)编译器检测有限:编译器只能部分检测异常规格的一致性。如果一个函数调用另一个函数,而后者抛出的异常不在前者的异常规格之内,编译器可能不会报错,但程序在运行时会出错。

(3)模板与异常规格的兼容性:由于模板实例化时类型参数的不确定性,很难为模板函数提供有意义的异常规格。

unexpected的处理:即使高阶的调用者准备好处理某些异常,如果底层函数违反了异常规格,unexpected仍然会被调用,从而可能中断异常处理流程。

示例 1:违反异常规格的情况

extern void f1(); // 可以抛出任意的异常
void f2() throw(int) {// 即使 f1 可能抛出不是 int 类型的异常,这也是合法的f1();
}

f2函数声明了它可以抛出int类型的异常,但它调用了f1,而f1可能抛出任何类型的异常。如果f1抛出了非int类型的异常,那么f2的异常规格就被违反了。

在C++中,throw(int) 是一个异常规格(exception specification),它出现在函数声明或定义的末尾,用来指定该函数可能抛出的异常类型。具体来说,throw(int) 表示这个函数只能抛出 int 类型的异常,或者不抛出任何异常。

异常规格的语法是在函数声明或定义的参数列表之后加上 throw 关键字,后面跟着一个括号,括号内可以列出函数可能抛出的异常类型,多个类型之间用逗号分隔。如果没有列出任何类型,即 throw(),则表示这个函数承诺不会抛出任何异常。

下面是一个使用 throw(int) 异常规格的函数示例:

void someFunction() throw(int) {// 函数体if (/* 某些条件 */) {throw 7; // 抛出一个整数异常}// 其他代码
}

someFunction 函数声明了它只会抛出 int 类型的异常。如果这个函数试图抛出其他类型的异常,那么程序的行为将是未定义的,通常会调用 std::unexpected 函数,这可能导致程序终止。

异常规格在C++11及以后的标准中已经被标记为弃用(deprecated),取而代之的是 noexcept 关键字。noexcept 提供了一种更简洁的方式来指定函数是否可以抛出异常。例如:

noexcept 表示函数不会抛出任何异常。

noexcept(false) 表示函数可能会抛出异常。

noexcept(expression) 表示函数是否会抛出异常取决于表达式 expression 的结果。

Item M15:异常处理的系统开销

基本开销:

即使不使用 try, throw, 或 catch,编译器也需要维护一些数据结构来支持异常处理。这些数据结构用来跟踪对象的构造状态、对象的生命周期以及异常处理的相关信息。这些开销通常很小,但如果程序完全不使用异常处理,编译器通常可以生成更小、更快的代码。

编译器支持:

理论上,C++编译器必须支持异常处理,即使程序中没有任何地方使用异常。实际上,许多编译器提供了选项来禁用异常处理的支持。如果你确定程序和所有链接的库都不使用异常,可以通过禁用异常处理来优化程序的大小和性能。

try 块的开销:

使用 try 块会增加额外的代码和运行时开销。不同的编译器实现方式不同,但通常会增加 5% 到 10% 的代码尺寸和相应的运行时开销。为了避免不必要的开销,应仅在真正需要捕获异常的地方使用 try 块。

抛出异常的开销:

抛出异常的开销通常比正常函数返回大得多,可能慢几个数量级。由于异常通常是罕见的情况,这些开销通常不会显著影响整体性能。但如果频繁抛出异常,会对性能产生严重影响。应避免使用异常来表示常见的情况,如控制流的结束或循环的退出。

优化异常处理开销的方法

禁用异常处理:如果确定程序和所有链接的库都不使用异常,可以在编译时禁用异常处理。这可以显著减小程序的大小并提高性能。

最小化 try 块的使用:

只在确实需要捕获异常的地方使用 try 块,避免无用的 try 块。例如,如果某个函数内部的操作不太可能抛出异常,就不必将其包裹在 try 块中。谨慎使用异常规格:避免使用旧式的异常规格(如 throw(int)),改用 noexcept。例如使用 noexcept 来指定函数不会抛出异常,或者使用 noexcept(false) 来表示函数可能会抛出异常。仅在异常情况下抛出异常:异常应该是罕见的、异常的情况,而不是常见的控制流手段。例如,不要用异常来控制循环的退出或表示函数的正常返回。


http://www.mrgr.cn/news/70577.html

相关文章:

  • The 3rd Universal CupStage 15: Chengdu, November 2-3, 2024(2024ICPC 成都)
  • Git - Think in Git
  • Numpy入门及进阶(三)
  • c语言中孤立位(loner)的使用
  • 华为交换机配置默认路由
  • [C++]内联函数和nullptr
  • C++builder中的人工智能(21):Barabási–Albert model(BA)模型
  • Snipaste截图软件直接运行
  • 【Web前端】从回调到现代Promise与Async/Await
  • Windows网络常见操作应用
  • Flink介绍
  • 干部管理系统:打造 “实、全、活” 的干部画像
  • 基于Matlab 口罩识别
  • 【高等数学学习记录】连续函数的运算与初等函数的连续性
  • 【软件测试】系统测试
  • 一文读懂【CSR社会责任报告】
  • 24/11/11 算法笔记 泊松融合
  • 什么是Stream流?
  • Centos7 安装RabbitMQ以及web管理插件
  • C++生成随机数
  • 数据结构与算法:二分搜索/二分查找的python实现与相关力扣题35.搜索插入位置、74.搜索二维矩阵
  • 衍射光学理解
  • 组件间通信(组件间传递数据)
  • 双十一”买买买!法官告诉你注意这些法律问题
  • 别再为视频转文字烦恼啦!这10个转换工具帮你一键搞定。
  • STM32系统的GPIO原理与结构