文章目录
- 1. 异常的概念及使用
- 1.1 异常的概念
- 1.2 异常的抛出和捕获
- 1.3 栈展开
- 1.4 查找匹配的处理代码
- 1.5 异常重新抛出
- 1.6 异常安全问题
- 1.7 异常规范
- 2. 标准库的异常
1. 异常的概念及使用
1.1 异常的概念
- 异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分,检测环节无须知道问题的处理模块的所有细节。
- C语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息,比较麻烦。异常时抛出一个对象,这个对象可以函数更全面的各种信息。
1.2 异常的抛出和捕获
- 程序出现问题时,我们通过抛出(
throw
)一个对象来引发一个异常,该对象的类型以及当前的调用链决定了应该由哪个catch
的处理代码来处理该异常。- 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误。
- 当
throw
执行时,throw
后面的语句将不再被执行。程序的执行从throw
位置跳到与之匹配的catch模块,catch
可能是同一函数中的一个局部的catch
,也可能是调用链中另一个函数中的catch
,控制权从throw
位置转移到了catch
位置。这里还有两个重要的含义:
- 1、沿着调用链的函数可能提早退出。
- 2、一旦程序开始执行异常处理程序,沿着调用链创建的对象都将销毁。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝的对象会在
catch
子句后销毁。(这里的处理类似于函数的传值返回)
1.3 栈展开
- 抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的
catch
子句,首先检查throw
本身是否在try
块内部,如果在则查找匹配的catch
语句,如果有匹配的,则跳到catch
的地方进行处理。- 如果当前函数中没有
try/catch
子句,或者有try/catch
子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch
过程被称为栈展开。- 如果到达
main
函数,依旧没有找到匹配的catch
子句,程序会调用标准库的terminate
函数终止程序。- 如果找到匹配的
catch
子句处理后,catch
子句代码会继续执行。
代码1:
// 除法运算函数,返回两数相除的结果
double Divide(int a, int b)
{try{// 检查除数是否为0if (b == 0){// 如果除数为0,创建错误信息并抛出string类型的异常string s("Divide by zero condition!");throw s; // 这个异常会被main函数中的catch块捕获}else{// 正常情况下进行除法运算// 将整数转换为double以进行浮点数除法return ((double)a / (double)b);}}catch (int errid) // 试图捕获 int 类型异常{// 注意:这个catch块永远不会执行,因为上面只抛出string类型异常/*C++中异常的捕获必须类型精确匹配或者符合类型转换规则这里抛出的是string类型,而catch试图捕获int类型string类型不能转换为int类型,所以不会被这个catch块捕获*/cout << errid << endl;}return 0; // 如果发生异常则返回0
}// 获取用户输入并执行除法运算的函数
void Func()
{int len, time;cin >> len >> time; // 从标准输入读取两个数try{// 调用Divide函数并输出结果cout << Divide(len, time) << endl;}//const char* errmsg 是一个指向常量字符的指针catch (const char* errmsg) // 捕获C风格字符串异常{// 注意:这个catch块永远不会执行,因为Divide抛出的是string类型异常cout << errmsg << endl;}// 输出当前函数名和行号// __FUNCTION__和__LINE__是预定义宏cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}int main()
{// 无限循环,持续接收用户输入并处理while (1){try{Func(); // 调用Func函数}catch (const string& errmsg) // 捕获string类型异常{// 这里会捕获Divide函数抛出的除零异常cout << errmsg << endl;}}return 0;
}
这段代码演示了C++异常处理机制的几个重要特点:
异常层级:
- 异常可以从内层函数传播到外层函数
- 如果当前层级没有适当的catch块,异常会继续向上传播
异常类型:
- 示例中使用了string类型的异常
- catch块必须匹配正确的异常类型才能捕获
程序结构:
main() {try {Func() {try {Divide() {try {throw string} catch(int) { // 不匹配}}} catch(const char*) { // 不匹配}}} catch(const string&) { // 匹配并处理} }
异常处理流程:
- 当
b=0
时,Divide
抛出string
异常- 异常跳过
Divide
中的int catch
- 异常跳过
Func
中的const char* catch
- 最终被
main
中的const string& catch
捕获
代码2:捕获异常
// C风格字符串
const char* str1 = "Hello";// C++ string类
string str2 = "Hello";// 它们不能直接互相捕获
try {throw "error"; // 抛出const char*
}
catch (string s) { } // 不会捕获
catch (const char* s) { } // 会捕获try {throw string("error"); // 抛出string
}
catch (const char* s) { } // 不会捕获
catch (string s) { } // 会捕获
代码3:对象的自动销毁(栈展开)
class Resource {
public:Resource(const string& s) : _s(s) {cout << _s << " 构造" << endl;}~Resource() {cout << _s << " 析构" << endl;}
private:string _s;
};void Foo() {Resource r1("r1"); // 创建r1throw "error"; // 抛出异常cout << "Foo continues..."; // 不会执行
}void Bar() {Resource r2("r2"); // 创建r2Foo(); // 调用Foocout << "Bar continues..."; // 不会执行
}int main() {try {Resource r3("r3"); // 创建r3Bar(); // 调用Bar}catch (const char* msg) {cout << msg << endl;}
}
打印:
r3 构造
r2 构造
r1 构造
r1 析构 // 栈展开,按照创建顺序的相反顺序析构
r2 析构
r3 析构
error
1.4 查找匹配的处理代码
- 一般情况下抛出对象和
catch
是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个。- 但是也有一些例外,允许从非常量向常量的类型转换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派生类向基类类型的转换,这个点非常实用,实际中继承体系基本都是用这个方式设计的。
- 如果到
main
函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以一般main
函数中最后都会使用catch(...)
,它可以捕获任意类型的异常,但是是不知道异常错误是什么。
#include<thread> // 用于线程操作// 异常基类:所有模块异常的公共接口
class Exception
{
public:// 构造函数:接收错误信息和错误IDException(const string& errmsg, int id):_errmsg(errmsg), _id(id){}// 虚函数:返回错误信息,允许派生类重写virtual string what() const{return _errmsg;}// 获取错误IDint getid() const{return _id;}protected:string _errmsg; // 错误信息int _id; // 错误ID
};// SQL模块异常类
class SqlException : public Exception
{
public:// 构造函数:额外包含SQL语句信息SqlException(const string& errmsg, int id, const string& sql):Exception(errmsg, id) // 调用基类构造函数, _sql(sql){}// 重写what函数,添加SQL相关信息virtual string what() const{string str = "SqlException:";str += _errmsg;str += "->";str += _sql;return str;}private:const string _sql; // SQL语句
};// 缓存模块异常类
class CacheException : public Exception
{
public:CacheException(const string& errmsg, int id):Exception(errmsg, id){}// 重写what函数,添加Cache标识virtual string what() const{string str = "CacheException:";str += _errmsg;return str;}
};// HTTP模块异常类
class HttpException : public Exception
{
public:// 构造函数:额外包含HTTP请求类型HttpException(const string& errmsg, int id, const string& type):Exception(errmsg, id), _type(type){}// 重写what函数,添加HTTP类型信息virtual string what() const{string str = "HttpException:";str += _type;str += ":";str += _errmsg;return str;}private:const string _type; // HTTP请求类型
};// SQL管理模块
void SQLMgr()
{// 模拟SQL操作可能出现的异常if (rand() % 7 == 0){throw SqlException("权限不足", 100, "select * from name = '张三'");}else{cout << "SQLMgr 调用成功" << endl;}
}// 缓存管理模块
void CacheMgr()
{// 模拟缓存操作的两种可能异常if (rand() % 5 == 0){throw CacheException("权限不足", 100);}else if (rand() % 6 == 0){throw CacheException("数据不存在", 101);}else{cout << "CacheMgr 调用成功" << endl;}SQLMgr(); // 调用SQL模块
}// HTTP服务模块
void HttpServer()
{// 模拟HTTP服务可能的异常if (rand() % 3 == 0){throw HttpException("请求资源不存在", 100, "get");}else if (rand() % 4 == 0){throw HttpException("权限不足", 101, "post");}else{cout << "HttpServer调用成功" << endl;}CacheMgr(); // 调用缓存模块
}int main()
{srand(time(0)); // 初始化随机数种子// 无限循环模拟服务器运行while (1){// 每秒执行一次this_thread::sleep_for(chrono::seconds(1));try {HttpServer(); // 调用HTTP服务}catch (const Exception& e) // 捕获所有继承自Exception的异常{// 通过虚函数机制调用相应派生类的what()cout << e.what() << endl;}catch (...) // 捕获所有其他未知异常{cout << "Unkown Exception" << endl;}}return 0;
}
这个代码展示了异常处理的几个重要概念:
异常继承体系:
- 基类
Exception
定义公共接口- 各模块定义特定的异常类
- 使用虚函数实现多态
异常信息的封装:
- 基本错误信息和
ID
在基类中- 派生类添加特定信息
- 通过
what()
函数提供格式化的错误信息异常的传播:
HttpServer() → CacheMgr() → SQLMgr()
任何一层抛出的异常都会被
main
中的catch
捕获异常的多态处理:
- 使用基类引用捕获所有派生类异常
- 通过虚函数机制调用正确的
what()
版本
1.5 异常重新抛出
有时
catch
到一个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理。捕获异常后需要重新抛出,直接throw
,就可以把捕获的对象直接抛出。
// 下面程序模拟展示了聊天时发送消息,发送失败补货异常,但是可能在电梯地下室等场景手机信号不好,则需要多次尝试,如果多次尝试都发送不出去,则就需要捕获异常再重新抛出,其次如果不是网络差导致的错误,捕获后也要重新抛出。
// 内部发送消息函数,模拟实际的消息发送操作
void _SeedMsg(const string& s)
{// 模拟网络不稳定导致的发送失败(50%概率)if (rand() % 2 == 0){throw HttpException("网络不稳定,发送失败", 102, "put");}// 模拟非好友关系导致的发送失败(约14.3%概率)else if (rand() % 7 == 0){throw HttpException("你已经不是对象的好友,发送失败", 103, "put");}else{// 消息发送成功cout << "发送成功" << endl;}
}// 外部发送消息函数,包含重试机制
void SendMsg(const string& s)
{// 最多尝试4次(初始1次 + 重试3次)for (size_t i = 0; i < 4; i++){try{_SeedMsg(s); // 尝试发送消息break; // 发送成功则退出循环}catch (const Exception& e){// 判断异常类型// 如果是网络不稳定导致的错误(错误码102)if (e.getid() == 102){// 如果已经重试了3次还是失败if (i == 3){throw; // 重新抛出当前异常,表示重试次数已耗尽}cout << "开始第" << i + 1 << "重试" << endl;}else{// 如果是其他类型的错误(如非好友关系),直接重新抛出异常throw;}}}
}int main()
{srand(time(0)); // 初始化随机数种子string str;// 持续接收用户输入的消息while (cin >> str){try{SendMsg(str); // 尝试发送消息}catch (const Exception& e) // 捕获所有Exception类型的异常{// 打印异常信息cout << e.what() << endl << endl;}catch (...) // 捕获所有其他未知类型的异常{cout << "Unkown Exception" << endl;}}return 0;
}
这段代码展示了几个重要的异常处理概念:
异常的重新抛出:
throw; // 重新抛出当前异常
- 保留了原始异常的所有信息
- 用于异常的传播和分层处理
重试机制:
for (size_t i = 0; i < 4; i++) {try { ... }catch { ... } }
- 只对特定类型的错误进行重试
- 限制最大重试次数
- 其他类型错误直接向上传播
异常分类处理:
- 网络错误(错误码102):可重试
- 权限错误(错误码103):直接失败
- 其他未知错误:直接失败
错误恢复策略:
if (e.getid() == 102) // 网络错误 {if (i == 3) // 重试耗尽throw;// 继续重试 } else // 其他错误 {throw; }
程序结构:
main()└─ SendMsg()└─ _SeedMsg()
- 分层的异常处理
- 清晰的职责划分
- 优雅的错误处理
1.6 异常安全问题
- 异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。中间我们需要捕获异常,释放资源后面再重新抛出,当然后面智能指针讲的
RAII
方式解决这种问题是更好的。- 其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要捕获处理,否则后面的5个资源就没释放,也资源泄漏了。《Effctive C++》第8个条款也专门讲了这个问题,别让异常逃离析构函数。
// 除法函数,当除数为0时抛出异常
double Divide(int a, int b)
{// 检查除数是否为0if (b == 0){// 抛出C风格字符串作为异常throw "Division by zero condition!";}// 执行除法运算,将整数转换为double以进行浮点数除法return (double)a / (double)b;
}void Func()
{// 分配动态内存// 注意:这里是异常安全的关键点,如果后续代码抛出异常,需要确保这块内存被释放int* array = new int[10];try {int len, time;cin >> len >> time;// 调用Divide函数,可能抛出异常cout << Divide(len, time) << endl;}catch (...) // 捕获所有类型的异常{// 异常处理块中确保释放内存cout << "delete []" << array << endl;delete[] array;throw; // 重新抛出捕获的异常// 这样可以在清理资源后让外层继续处理异常}// 正常执行路径下的内存释放cout << "delete []" << array << endl;delete[] array;
}int main()
{try{Func(); // 调用可能抛出异常的函数}catch (const char* errmsg) // 捕获C风格字符串异常{cout << errmsg << endl;}catch (const exception& e) // 捕获标准异常{cout << e.what() << endl;}catch (...) // 捕获所有其他类型的异常{cout << "Unkown Exception" << endl;}return 0;
}
这段代码展示了几个重要的异常安全概念:
资源管理和异常安全:
int* array = new int[10]; try {// 可能抛出异常的代码 } catch (...) {delete[] array; // 确保在异常情况下释放资源throw; } delete[] array; // 正常情况下释放资源
异常的重新抛出:
catch (...) {// 清理代码throw; // 重新抛出当前异常 }
- 保持异常的原始信息
- 允许异常继续传播
- 确保资源被正确清理
异常处理层次:
main()└─ Func()└─ Divide()
Divide()
:产生异常Func()
:资源清理main()
:最终异常处理
1.7 异常规范
- 对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,知道某个函数是否会抛出异常有助于简化调用函数的代码。
C++98
中函数参数列表的后面接throw()
,表示函数不抛异常,函数参数列表的后面接throw(类型1,类型2...)
表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。C++98
的方式这种方式过于复杂,实践中并不好用,C++11
中进行了简化,函数参数列表后面加noexcept
表示不会抛出异常,啥都不加表示可能会抛出异常。- 编译器并不会在编译时检查
noexcept
,也就是说如果一个函数用noexcept
修饰了,但是同时又包含了throw
语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是一个声明了noexcept
的函数抛出了异常,程序会调用terminate
终止程序。noexcept(expression)
还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会则返回false
,不会就返回true
。
// C++98 风格的异常说明// throw (std::bad_alloc) 表示这个函数只会抛出 bad_alloc 异常
// 如果抛出其他类型的异常,程序会调用 unexpected 处理函数
void* operator new (std::size_t size) throw (std::bad_alloc);// throw() 表示这个函数不会抛出任何异常
// 如果违反承诺抛出异常,程序会调用 unexpected 处理函数
void* operator delete (std::size_t size, void* ptr) throw();// C++11 风格的异常说明
// noexcept 关键字表示函数不会抛出异常
// 如果违反承诺抛出异常,程序会直接调用 std::terminate 终止程序
size_type size() const noexcept;
iterator begin() noexcept;
const_iterator begin() const noexcept;// 这里声明函数不会抛出异常,但实际实现中可能抛出异常
// 这是一个违反异常说明的例子,如果真的抛出异常会导致程序终止
double Divide(int a, int b) noexcept
{// 当b == 0时抛出异常if (b == 0){// 警告:违反noexcept承诺!throw "Division by zero condition!";}return (double)a / (double)b;
}int main()
{try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;}catch (const char* errmsg) // 捕获C风格字符串异常{cout << errmsg << endl;}catch (...) // 捕获所有其他类型的异常{cout << "Unkown Exception" << endl;}int i = 0;// noexcept运算符:检查表达式是否声明为noexcept// 返回true表示表达式被声明为不会抛出异常,false表示可能抛出异常cout << noexcept(Divide(1,2)) << endl; // 输出1,因为Divide声明为noexceptcout << noexcept(Divide(1,0)) << endl; // 输出1,同上cout << noexcept(++i) << endl; // 输出1,内置类型的操作不抛出异常return 0;
}
关于异常说明的重要概念:
C++98 vs C++11 异常说明:
// C++98 void func() throw(std::exception); // 只能抛出std::exception void func() throw(); // 不能抛出任何异常// C++11 void func() noexcept; // 不能抛出任何异常 void func() noexcept(条件); // 条件为true时不能抛出异常
noexcept
说明符和运算符:// 说明符:用于函数声明 void f() noexcept; // 承诺不抛出异常// 运算符:用于检查表达式 noexcept(表达式) // 检查表达式是否声明为noexcept
违反异常说明的后果:
// C++98: 调用unexpected() // C++11: 调用terminate()
noexcept
的使用场景:
- 析构函数(默认
noexcept
)- 移动构造和移动赋值
- 不会失败的操作
- 性能关键的操作
2. 标准库的异常
- https://legacy.cplusplus.com/reference/exception/exception/
C++
标准库也定义了一套自己的一套异常继承体系库,基类是exception
,所以我们日常写程序,需要在主函数捕获exception
即可,要获取异常信息,调用what
函数,what
是一个虚函数,派生类可以重写。
基类:
std::exception
:所有标准异常的基类直接派生类:
// 内存分配失败 std::bad_alloc {string s(1000000000000); // 可能抛出bad_alloc }// 类型转换失败 std::bad_cast {Base* ptr = new Derived();auto& derived = dynamic_cast<Derived&>(*ptr); // 可能抛出bad_cast }// typeid操作符使用错误 std::bad_typeid {class A { virtual void f() {} };A* ptr = nullptr;typeid(*ptr); // 对空指针使用typeid,抛出bad_typeid }// 异常处理失败 std::bad_exception {// 通常在异常说明违反时抛出 }// 逻辑错误 std::logic_error {vector<int> v;v.at(10); // 抛出out_of_range(logic_error的子类) }// 运行时错误 std::runtime_error {int x = INT_MAX;x += 1; // 可能抛出overflow_error }
- logic_error的子类:
// 参数无效 std::invalid_argument {std::bitset<8> bits("12345"); // 非法字符,抛出invalid_argument }// 域错误(数学运算) std::domain_error {// 例如:对负数求平方根 }// 长度错误 std::length_error {string s;s.resize(s.max_size() + 1); // 抛出length_error }// 越界访问 std::out_of_range {string s = "hello";char c = s.at(10); // 抛出out_of_range }
- runtime_error的子类:
// 算术溢出 std::overflow_error {// 数值计算超出范围 }// 范围错误 std::range_error {// 计算结果超出有效范围 }// 算术下溢 std::underflow_error {// 数值计算结果太小 }
使用示例:
try {vector<int> vec;// 尝试访问不存在的元素vec.at(5); // 抛出 out_of_range } catch (const std::out_of_range& e) {cout << "越界访问: " << e.what() << endl; } catch (const std::logic_error& e) {cout << "逻辑错误: " << e.what() << endl; } catch (const std::exception& e) {cout << "标准异常: " << e.what() << endl; }
这个继承体系的设计原则:
- 所有异常都继承自
std::exception
- 分为逻辑错误和运行时错误两大类
- 具体错误类型进一步细分
- 便于异常的分类处理和向上转换