我不怕练过一万种腿法的对手,就怕将一种腿法 练一万次的对手。
什么是C++的异常
在C++中,异常处理通常使用try-catch块来实现。try块用于包含可能会抛出异常的代码,而catch块用于捕获并处理异常。当异常被抛出时,程序会跳过try块中未执行的代码,并在catch块中执行适当的处理操作。如果没有抛出异常,则catch块将被跳过。
以下是一个简单的C++程序,演示了如何使用异常处理:
#include <iostream> int main() { try { // 尝试执行下面的代码块,如果发生异常,则跳转到catch块处理异常 int x = 10; int y = 0; if (y == 0) { // 如果y等于0,抛出一个异常 throw "Division by zero!"; // 抛出一个字符串类型的异常对象,内容为"Division by zero!" } int result = x / y; std::cout << "Result: " << result << std::endl; } catch (const char* error) { // 捕获一个字符串类型的异常对象,将异常对象赋值给变量error std::cerr << "Error: " << error << std::endl; // 输出错误消息,内容为"Error: Division by zero!" } return 0; } /* 在上面的程序中,try块包含了可能会抛出异常的代码, 包括将变量y赋值为0和使用除法运算符计算x除以y的结果。 如果y等于0,程序会抛出一个异常,内容为"Division by zero!"。 然后,catch块用于捕获并处理该异常,输出错误消息"Error: Division by zero!"。 */
异常对象
- 异常对象是一种特殊的对象。编译器依据异常抛出表达式构造异常对象(即异常对象总是被拷贝)。对象的类型是由表达式所表示对象的静态编译类型决定的。如Parent& rObj = Child; throw rObj;时会抛出Parent类型的异常对象。
- 异常对象存放在内存特殊位置,该位置既不是栈也不是堆,在Windows中是放在线程信息TIB中。该对象由异常机制负责创建和释放!(g++和vc下存储区域处理略有差异)。
- 异常对象不同于函数的局部对象,局部对象在函数调用结束后就被自动销毁,而异常对象将驻留在所有可能激活的catch语句都能访问到的内存空间中。当异常对象与catch语句成功匹配后,在该catch语句的结束处被自动析构。
- 在函数中返回局部变量的指针或引用几乎肯定会造成错误。同理,在throw语句中抛出局部变量的指针或引用也几乎是错误的。
// 捕获异常对象 (值,引用,指针) #include <iostream> #include <string> using namespace std; class MyException { public: MyException() { cout << "MyException():" << this << endl; } MyException(const MyException&) { cout << "MyException(const MyException&):" << this << endl; } ~MyException() { cout << "~MyException():" << this << endl; } void what() { cout << "MyException: this = " << this << endl; } }; class MyChildExcept : public MyException { public: MyChildExcept() { cout << "MyChildExcept():" << this << endl; } MyChildExcept(const MyChildExcept&) { cout << "MyChildExcept(const MyChildExcept&):" << this << endl; } ~MyChildExcept() { cout << "~MyChildExcept():" << this << endl; } void what() { cout << "MyChildExcept: this = " << this << endl; } }; void func_local() { // throw 局部对象 MyException localEx; throw localEx; //尽管localEx是个局部对象,但这里会将其复制构造出一个异常对象,并存储在TIB中。而不是真正的将局部对象抛出去! } void func_temp() { //throw 临时对象 MyException(); //临时对象1 throw MyException(); //编译器会将这个临时对象直接存储在线程TIB中,成为异常对象(注意与临时对象1存储位置一般相距较远!) } void func_ptr() { //throw 指针 throw new MyException(); //注意:异常对象是复制的堆对象而来的指针(存在内存泄漏风险!!!) } void func_again() { MyChildExcept child; MyException& re = child; //注意抛出的是re的静态类型的异常对象,即MyException,而不是MyChildExcept; throw re; } int main() { cout << "----------------------------------catch by value------------------------------" << endl; //按值捕获 try { func_local(); //throw MyExecption() } catch (MyException e) { //复制异常对象,须额外进行一次拷贝! cout << "catch (MyException e)" << endl; e.what(); } cout << "--------------------------------catch by reference----------------------------" << endl; //按引用捕获 try { func_temp(); } catch (MyException& e) { //直接引用异常对象,无须拷贝 cout << "catch (MyException& e)" << endl; e.what(); } cout << "---------------------------------catch by pointer-----------------------------" << endl; //按指针捕获 try { func_ptr(); } catch (MyException* e) { //按指针捕获(可能造成内存泄漏) cout << "catch (MyException* e)" << endl; e->what(); delete e; //释放堆对象,防止内存泄漏 } cout << "------------------------------------throw again-------------------------------" << endl; //二次抛异常 try { try { func_again(); } catch (MyException& e) { e.what(); //注意以下两种方式不同 //1. 在throw后指定异常对象为e //throw e; //e会继续复制一份,并抛出复制的异常对象而e本身会被释放! //2.throw后不指定任何对象,只要是在catch中捕获的,一律抛出去。 throw; //此时,e本身再被抛出去。不会另外构造异常对象。 } } catch (MyException& e) { e.what(); } return 0; }
(参考博客:https://www.cnblogs.com/5iedu/p/11270922.html)
异常的抛出和捕获
异常的抛出和匹配原则
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
- 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
- catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。
- 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,抛出的派生类对象,可以使用基类捕获,这个在实际中非常实用。
在函数调用链中异常栈展开匹配原则
- 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
- 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
- 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
- 4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
//栈展开的测试 #include <iostream> // 自定义异常类,继承自std::exception类 class MyException : public std::exception { public: // 重写what()方法以返回异常信息字符串 const char* what() const noexcept override { return "MyException: Something went wrong!"; } }; // 函数func3,抛出一个MyException异常 void func3() { std::cout << "func3: throwing MyException" << std::endl; throw MyException(); // 抛出一个MyException异常 std::cout << "func3: return" << std::endl; //如果抛出异常,这里就不会执行 } // 函数func2,调用函数func3 void func2() { std::cout << "func2: calling func3" << std::endl; func3(); // 调用函数func3 std::cout << "func2: return" << std::endl; } // 函数func1,调用函数func2 void func1() { std::cout << "func1: calling func2" << std::endl; func2(); // 调用函数func2 std::cout << "func1: return" << std::endl; } int main() { try { std::cout << "main: calling func1" << std::endl; func1(); // 调用函数func1 } catch (const std::exception& e) { // 捕获一个std::exception类型的异常对象,将异常对象赋值给变量e std::cerr << "Exception caught: " << e.what() << std::endl; // 输出错误消息,内容为捕获的异常信息 } return 0; // 程序结束 }
异常的重新抛出
有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。
namespace skate{ // 服务器开发中通常使用的异常继承体系 class Exception{ public: Exception(const string& errmsg, int id) :_errmsg(errmsg) , _id(id){ } virtual string what() const{ return _errmsg; } int getid() const{ return _id; } protected: string _errmsg; // 描述错误信息 int _id; // 错误编码 // 堆栈信息 }; class HttpServerException : public Exception{ public: HttpServerException(const string& errmsg, int id, const string& type) :Exception(errmsg, id) , _type(type){ } virtual string what() const{ string str = "HttpServerException:"; str += _type; str += ":"; str += _errmsg; return str; } private: const string _type; }; void SeedMsg(const string& str){ if (rand() < (RAND_MAX /4)*3){ throw HttpServerException("SeedMsg::网络错误", 2, "put"); } else if (rand() < RAND_MAX /3){ throw HttpServerException("SeedMsg::你已经不是对方好友", 1, "post"); } else{ cout << "消息发送成功!->" << str << endl; } } } int main() { srand(time(0)); while (1) { ::Sleep(1000); try { //skate::HttpServer(); // 发送出现网络错误,要求重试3次 // 权限错误就直接报错 for (size_t i = 0; i < 3; ++i) { try { skate::SeedMsg("你好啊!今晚一起看电影怎么样?"); break; } catch (const skate::Exception& e) { if (e.getid() == 2) // 异常编码的价值,针对某个错误进行特殊处理 { cout << "网络错误,重试发消息第" <<i+1<<"次"<< endl; //特殊处理 if (2 == i) cout << "=======网络错误===发送失败======" << endl; //异常直接被捕获 不重新抛出 而是尝试重试 continue; //网络错误,尝试重新发送 / } else // 其他错误 { //break; //发送失败,直接重新抛出 throw e; // 异常重新抛出 } } } } catch (const skate::Exception& e) // 这里捕获父类对象就可以 { // 多态 cout << e.what() << endl; //此处已经捕获不到 网络错误,因为网络错误没有重新抛出,已经被特殊处理了 } catch (const std::exception& e) // 这里捕获父类对象就可以 { // 多态 cout << e.what() << endl; } catch (...) { cout << "Unkown Exception" << endl; } } return 0; }
《C++Primer》关于重新抛出
关于异常安全
- 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不 完整或没有完全初始化
- 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
- C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄 漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题。
补充关于构造函数与try语句块
关于异常规范(C++11 noexcept 声明)
C++0x与C++11异常规格声明方式的不同
- void func() throw() { ... } // throw()声明该函数不会产生异常(C++0x)
- void func() throw(int, double) { ... } //可能产生int或double类型异常(C++0x)
- void func() noexcept { ... } // noexcept声明该函数不会产生异常(C++11)
- void func() noexcept(常量表达式) { ... } //由表达式决定是否产生异常(C++11)
ps: 这里学习noexcept关键字的关键主要为了看得懂官方文档的声明,具体细节就做过多介绍了
-end