C++ - 异常
文章目录
- 基本概念
- 异常的抛出与捕获
- 栈展开
- 查找匹配的处理代码
- 异常重新抛出
- 异常安全问题
- 异常规范
- C++98异常规范
- C++11
基本概念
- 异常处理机制允许程序在独立开发的部分之间,就运行时出现的问题进行通信并处理。异常将问题的检测与解决过程分离:一部分程序负责检测问题,另一部分负责处理问题,检测环节无需了解处理模块的具体细节。
- C语言错误处理方式主要通过错误码分类编号处理错误,需额外查询错误信息,较为繁琐。异常抛出一个包含全面信息的异常对象,无需手动查询,传递信息更高效直接。
异常的抛出与捕获
-
程序出现一个问题时,通过抛出(
throw)一个对象来引发一个异常,该对象的类型以及当前的调用链决定了由哪个catch的处理代码来处理异常 -
被选中的处理代码是调用链中与该对象类型匹配且里抛出异常位置最近的一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误
-
当throw执行时,throw后面的语句将不再执行。程序的执行从throw位置跳到与之匹配的catch模块,catch可能是同以一函数中的一个局部的,也可能是调用链中另一个函数的catch,控制权从throw位置转移到了catch位置。这里还有两个重要的含义:
-
沿着调用链的函数可能提早退出
void methodA() {try {methodB();} catch (Exception e) {// 异常在此被捕获} }void methodB() {methodC();// 如果methodC抛出异常,这行代码不会执行 }void methodC() {throw new RuntimeException();// 这行之后的代码不会执行 } -
一旦程序开始执行异常处理程序,沿着调用链创建的对象都将摧毁
void process() {try {Resource r1 = new Resource(); // 创建资源1Resource r2 = new Resource(); // 创建资源2riskyOperation(); // 可能抛出异常r1.close(); // 正常流程会执行r2.close(); // 正常流程会执行} catch (Exception e) {// 如果riskyOperation抛出异常:// 1. r1和r2的close()不会被执行// 2. 但它们的析构函数会被调用(在Java中通过finalize(),但推荐用try-with-resources)} }
-
-
抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝对象会再catch子句后销毁
栈展开
- 抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try内部,如果在则查找匹配的catch子句,如果有匹配的,则跳到catch的地方进行处理
- 如果当前函数中没有try/catch子句,或者有try/catch子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开
- 如果到达main函数,依旧没有找到catch子句,程序会调用标准库的
termianate函数终止程序 - 如果找到匹配的catch子句处理后,catch子句代码会继续执行
#include <iostream>
#include <string>
using namespace std;double Divide(int a, int b)
{try{// 当b == 0时抛出异常if (b == 0){string s("Divide by zero condition!");throw s;}else{return ((double)a / (double)b);}}//用于捕获 int类型的异常,但当前代码中并没有任何地方抛出 //int类型的异常,因此这个 catch 块永远不会被执行。catch (int errid){cout << errid << endl;}return 0;
}
void Func()
{int len, time;cin >> len >> time;try{cout << Divide(len, time) << endl;}//throw s抛出的是 string类型//catch (const char *)只能捕获 const char*类型//这两种类型不兼容,因此这个 catch 块不会捕获到 throw s抛出的异常catch (const char *errmsg){cout << errmsg << endl;}cout << __FUNCTION__ << ":" << __LINE__ << "⾏执⾏" << endl;
}
int main()
{while (1){try{Func();}//// 最终捕获 string 异常catch (const string &errmsg){cout << errmsg << endl;}}return 0;
}

查找匹配的处理代码
- 一般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择位置最近的那个
- 例外的是,允许从非常量向常量的类型转换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数转换成指向函数的指针;允许从派生类向杰基类类型的转换
- 如果匹配到main函数,异常仍然没有被匹配就会终止程序,不是发生严重错误的情况下,是不期望程序终止的,所以一般main函数最后都会使用
catch(...),它可以捕获任意类型的异常,但是不知道异常错误是什么
#include <thread>
#include <iostream>
using namespace std;// 模拟设计⼀个服务的⼏个模块
// 每个模块的继承都是Exception的派⽣类,每个模块可以添加⾃⼰的数据
// 最后捕获时,我们捕获基类就可以
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 SqlException : public Exception
{
public:SqlException(const string &errmsg, int id, const string &sql): Exception(errmsg, id), _sql(sql){}virtual string what() const{string str = "SqlException:";str += _errmsg;str += "->";str += _sql;return str;}private:const string _sql;
};
class CacheException : public Exception
{
public:CacheException(const string &errmsg, int id): Exception(errmsg, id){}virtual string what() const{string str = "CacheException:";str += _errmsg;return str;}
};
class HttpException : public Exception
{
public:HttpException(const string &errmsg, int id, const string &type): Exception(errmsg, id), _type(type){}virtual string what() const{string str = "HttpException:";str += _type;str += ":";str += _errmsg;return str;}private:const string _type;
};
void SQLMgr()
{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();
}
void HttpServer()
{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();}catch (const Exception &e) // 这⾥捕获基类,基类对象和派⽣类对象都可以被捕获{cout << e.what() << endl;}catch (...){cout << "Unkown Exception" << endl;}}return 0;
}

异常重新抛出
有时catch到一个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理。捕获异常后需要重新抛出,直接throw;就可以把捕获的对象直接抛出
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
// 下⾯程序模拟展⽰了聊天时发送消息,发送失败捕获异常,但是可能在
// 电梯地下室等场景⼿机信号不好,则需要多次尝试,如果多次尝试都发
// 送不出去,则就需要捕获异常再重新抛出,其次如果不是⽹络差导致的
// 错误,捕获后也要重新抛出。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 HttpException : public Exception
{
public:HttpException(const string &errmsg, int id, const string &type): Exception(errmsg, id), _type(type){}virtual string what() const{string str = "HttpException:";str += _type;str += ":";str += _errmsg;return str;}private:const string _type;
};void _SeedMsg(const string &s)
{if (rand() % 2 == 0){throw HttpException("⽹络不稳定,发送失败", 102, "put");}else if (rand() % 7 == 0){throw HttpException("你已经不是对象的好友,发送失败", 103, "put");}else{cout << "发送成功" << endl;}
}void SendMsg(const string &s)
{// 发送消息失败,则再重试3次for (size_t i = 0; i < 4; i++){try{_SeedMsg(s);break;}catch (const Exception &e){// 捕获异常,if中是102号错误,⽹络不稳定,则重新发送// 捕获异常,else中不是102号错误,则将异常重新抛出if (e.getid() == 102){// 重试三次以后否失败了,则说明⽹络太差了,重新抛出异常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){cout << e.what() << endl<< endl;}catch (...){cout << "Unkown Exception" << endl;} }return 0;
}

异常安全问题
- 异常抛出后,后面的代码就不再执行,前面申请的资源,本来应该在后面进行释放,但是可能会因为抛异常导致资源没有被释放,这里由于异常就引发了资源泄漏,产生了安全性问题。所以中间需要捕获异常,释放资源后再重新抛出
- 其次在析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要捕获处理,否则后面5个资源就没有释放,造成资源泄漏
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;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 HttpException : public Exception
{
public:HttpException(const string &errmsg, int id, const string &type): Exception(errmsg, id), _type(type){}virtual string what() const{string str = "HttpException:";str += _type;str += ":";str += _errmsg;return str;}private:const string _type;
};double Divide(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Division by zero condition!";}return (double)a / (double)b;
}
void Func()
{// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array没有得到释放。// 所以这⾥捕获异常后并不处理异常,异常还是交给外层处理,这⾥捕获了再// 重新抛出去。int *array = new int[10];try{int len, time;cin >> len >> time;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){cout << errmsg << endl;}catch (const exception &e){cout << e.what() << endl;}catch (...){cout << "Unkown Exception" << endl;}return 0;
}

异常规范
- 对于用户和编译而言,预先知道某个程序会不会抛出异常是很有利的,知道某个函数是否会抛出异常有助于简化调用函数的代码
- C++98中函数参数列表的后⾯接
throw(),表⽰函数不抛异常,函数参数列表的后⾯接throw(类型1,类型2...)表⽰可能会抛出多种类型的异常,可能会抛出的类型⽤逗号分割。 - C++98的⽅式这种⽅式过于复杂,实践中并不好⽤,C++11中进⾏了简化,函数参数列表后⾯加
noexcept表⽰不会抛出异常,啥都不加表⽰可能会抛出异常。 - 编译器并不会在编译时检查
noexcept,也就是说如果⼀个函数⽤noexcept修饰了,但是同时⼜包含了throw语句或者调⽤的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是⼀个声明了noexcept的函数抛出了异常,程序会调⽤ ~ 终⽌程序。 noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,可能会则返回false,不会就返回true。
C++98异常规范
#include <iostream>
#include <cstdlib>
#include <new> // 包含 std::bad_allocusing namespace std;// C++98 异常规范示例// 1. 不抛出任何异常
class Vector {
private:int* data;size_t capacity;public:// 添加默认构造函数,确保成员被初始化,避免在析构时 delete 未初始化指针Vector() : data(NULL), capacity(0) {}// 声明不抛出任何异常size_t getCapacity() const throw() {return capacity;}// 析构函数通常声明为不抛出异常~Vector() throw() {delete[] data;}
};// 2. 可能抛出特定类型的异常
class FileProcessor {
public:// 声明可能抛出 const char* 和 int 类型的异常void processFile(const string& filename) throw(const char*, int) {if (filename.empty()) {throw "文件名不能为空"; // const char* 异常}if (filename.length() > 100) {throw -1; // int 异常(错误码)}// 文件处理逻辑...cout << "处理文件: " << filename << endl;}// 可能抛出 bad_alloc 异常void* allocateMemory(size_t size) throw(std::bad_alloc) {return ::operator new(size); // 使用全局的operator new}
};// 3. 全局操作符重载的异常规范
void* operator new(size_t size) throw(std::bad_alloc) {void* ptr = malloc(size);if (!ptr) {throw std::bad_alloc(); // 内存分配失败时抛出}return ptr;
}void operator delete(void* ptr) throw() {free(ptr); // delete 操作不应该抛出异常
}// 4. 可能抛出任何异常(不写throw规范)
void riskyFunction() { // 可能抛出任何类型的异常throw string("这是一个字符串异常");// 或者 throw 123;// 或者 throw runtime_error("错误");
}int main() {Vector vec;try {// 这个函数承诺不抛出异常cout << "容量: " << vec.getCapacity() << endl;FileProcessor processor;// 这个函数可能抛出 const char* 或 int 异常processor.processFile("test.txt");// 空文件名测试// processor.processFile(""); // 会抛出 const char* 异常} catch (const char* msg) {cout << "捕获到字符串异常: " << msg << endl;} catch (int errorCode) {cout << "捕获到错误码: " << errorCode << endl;} catch (...) {cout << "捕获到未知异常" << endl;}return 0;
}
C++11
#include <iostream>
#include <vector>
#include <stdexcept>
#include <type_traits>using namespace std;// C++11 异常规范示例class Container
{
private:vector<int> data;public:Container() = default;// 1. noexcept 表示不抛出异常size_t size() const noexcept{return data.size();}// 2. 条件性 noexcepttemplate <typename T>void push_back(T &&value) noexcept(noexcept(data.push_back(forward<T>(value)))){data.push_back(forward<T>(value));}// 3. 移动构造函数通常声明为 noexceptContainer(Container &&other) noexcept: data(move(other.data)){}// 4. 移动赋值运算符通常声明为 noexceptContainer &operator=(Container &&other) noexcept{if (this != &other){data = move(other.data);}return *this;}
};// 5. 简单的数学函数 - 声明为 noexcept
double safeDivide(int a, int b) noexcept
{if (b == 0){// 注意:noexcept 函数内部不能抛出异常!// 这里应该返回特殊值或终止程序cerr << "错误:除零操作" << endl;return 0.0;}return static_cast<double>(a) / b;
}// 6. 可能抛出异常的函数
double riskyDivide(int a, int b)
{ // 没有 noexcept,可能抛出异常if (b == 0){throw invalid_argument("除数不能为零");}return static_cast<double>(a) / b;
}// 7. noexcept 运算符示例
void testNoexceptOperator()
{int x = 10, y = 2;cout << boolalpha;cout << "noexcept(safeDivide(10, 2)): " << noexcept(safeDivide(10, 2)) << endl;cout << "noexcept(riskyDivide(10, 2)): " << noexcept(riskyDivide(10, 2)) << endl;cout << "noexcept(x + y): " << noexcept(x + y) << endl;cout << "noexcept(throw 1): " << noexcept(throw 1) << endl;// 测试标准库函数的 noexceptcout << "noexcept(vector<int>().push_back(1)): "<< noexcept(vector<int>().push_back(1)) << endl;
}// 8. 条件性异常规范在模板中的应用
template <typename T>
class SmartPointer
{T *ptr;public:// 如果 T 的析构函数是 noexcept,那么这个也是 noexcept~SmartPointer() noexcept(noexcept(ptr->~T())){delete ptr;}
};// 9. 交换函数通常声明为 noexcept
template <typename T>
void swap(T &a, T &b) noexcept(noexcept(a.swap(b)))
{a.swap(b);
}int main()
{Container cont;// noexcept 检测testNoexceptOperator();cout << "\n=== 异常安全测试 ===" << endl;try{// 安全函数 - 不会抛出异常cout << "安全除法: " << safeDivide(10, 2) << endl;cout << "安全除法(除零): " << safeDivide(10, 0) << endl;// 危险函数 - 可能抛出异常cout << "危险除法: " << riskyDivide(10, 2) << endl;// cout << "危险除法(除零): " << riskyDivide(10, 0) << endl; // 会抛出异常}catch (const exception &e){cout << "捕获到异常: " << e.what() << endl;}// 测试容器的 noexcept 方法cout << "\n容器大小(noexcept): " << cont.size() << endl;// 测试移动操作的 noexceptContainer cont1;Container cont2 = move(cont1); // 移动构造 - noexceptcout << "\n=== 类型特性测试 ===" << endl;cout << "is_nothrow_default_constructible<vector<int>>: "<< is_nothrow_default_constructible<vector<int>>::value << endl;cout << "is_nothrow_move_constructible<vector<int>>: "<< is_nothrow_move_constructible<vector<int>>::value << endl;return 0;
}
