C++11: 自定义异常&&标准异常体系&&回顾c异常处理方式
摘要:本文系统讲解了C++异常处理机制,重点分析了try-catch捕获原理、栈展开过程及执行流跳跃问题。通过自定义异常体系示例,展示了如何构建多态异常类层次结构。文章指出异常处理相比错误码的优势在于自动展开调用栈,但存在内存泄漏风险,建议结合RAII机制使用。同时对比了C++标准异常体系,并以vector的at()和operator[]为例说明异常安全性差异。最后强调在生产环境中,异常处理与日志

前言
如下面几张图,带你迅速理解,系统编程中获取异常终止信息,用户自定义退出码,来获取退出信息
系统编程获取进程异常终止信息

用户自定义退出码,避免进程真正执行异常代码 导致异常退出

问题是 ? 不标准,不够用
嗯... 当访问无效内存,或者调用终止abrt等等产生信号让进程退出信息,但是大部分我们能拿到的是退出码,又或者用户自定义退出码依然是一个数字,要么查表,要么用户也可以打印一下在异常位置判断不要真正让进程执行异常,但是这依旧是不够用。
1. 不灵活 2. 不够标准 大家都拿着自己的退出码 自己的方式来规避异常退出,不优雅,在很多大型项目里面,根据提前规避异常的位置,比如/0 或者空指针等等我们都进来规避异常的发送,因为一旦发送异常退出很多有效信息是可能被我们无法追踪的,所以提前规避!是很有必要的: 总结一下思路:
C++11也好又或者其他的语言 本质上异常的核心就如下情况 : 必要让x/0真正发生了导致进程异常退出,而是提前规避,这就是异常机制,只不过下面这样的写法如退出码以及使用方式不够规划,所以c++11 对于异常机制有一套面向对象的方式。
对了实际生产中: 日志和异常都是很重要的,只不过异常也有自己的缺点哈(跳跃执行流) 但是对于定为错误,避免进程异常退出还是很重要的哈!
int divide(int x,int y){
if(y==0){
print("除0错误");// 除0错误打印一下
exit(-1);// 让进程退出返回退出码为-1
//
}
return x/y;
}
一. try 和throw 以及catch关键字基本使用
可以参考如下代码进行迅速上手c++的异常类型,我这里做补充:
在c++中 你的throw关键字可以抛出任意类型的对象们可以是自定义类型,也可以是内置类型如 int const char* 等等 ,但是要注意,这跟异常体系是这样的:
try{ //// 内部抛出你的异常体系 }
catch(捕获类型1 ) { //// }
catch(捕获类型2 ) { //// } ....
如果一种所有当前try类型都不会不到你抛出的很有可能就会直接终止,因为如果throw没有人捕获他,他就调用abrt()函数 终止进程(还有可能调用栈展开也可能捕获哦!)
所以可以用这跟
catch(...) {} 三个点 表示可以捕获任意类型
class myclass {
public:
void test()const {
cout << "我是一个测试异常类" << endl;
}
int _a;
string _b;
};
void test_use1() {
//throw; 空抛出直接报错哈
// 这样要注意throw只会在他的一个try -catch模块中
// 不会去别人的try-catch模块 如果没有人捕获他就直接报错了
try {
//受到检测的代码 这里主动抛异常
//主动调用 throw +基类类型或者自定义类型
//throw 'a'; 表示我抛出一个异常对象 类型是 char
throw myclass();// 抛出一个匿名对象 类型是 myclass
}
catch (int a) {
// 表示处理捕获到别人throw的 int
}
catch (char b)
{
// 表示处理捕获到别人throw的 char
}
catch (const myclass& obj) {
// 捕获 一个myclass的类引用 const的 注意我们可以传引用哈
// 前面的内置类型我都是值拷贝的哈
// 捕获到之后你要是不要再对外抛出throw就表示处理完成
obj.test();
}
catch (...) {
cout << "捕获都无效类型 " << endl;
exit(-1);
}
}
int main() {
test_use1();
return 0;
}
catch(...) 表示捕获任意类型!

退出码

二. 抛异常的原理以及调用栈展开
基本使用已经提到了,就是c++编译器三个新的关键字 ,try{ } - catch{ } 模块 在try模块中 throw 任意 类型,然后在try 所有一一对应的类型进行就近匹配原则,捕获到了然后进行处理,如果在catch模块中没有继续向外抛出就没有关系啦 .
2.1 同一try下不能在catch相同类型

try {
throw myclass();
}
catch (myclass&obj) {
// 捕获到了我们不处理直接抛出
throw;// 直接抛出表示将捕获到的对象直接抛出
}
// 同一try下捕获相同类型会报错
catch (int) {
//捕获int 不处理
}
catch (int) {
// 在封装一个捕获int对象
}
2.2 try模块下栈展开
在异常体系中抛出的异常对象是由throw决定的,要始终记住,在当前的try-catch中只会被捕获一次,如果当前try -catch中你无法捕获会触发栈展开,他会回到你的调用栈中,注意后面的代码都不会执行了,这就是执行流跳跃,然后检测调用栈检测也没有try -catch如果在你的调用栈能捕获那就捕获报错,否则报错。
2.2.1 图解栈展开
图解异常的栈展开: 如下图: 当我们现在有三个函数层层嵌套的函数调用,最终在第三个函数throw了一个函数,但是很离谱的是,他自己当前的try-catch模块无法捕获他自己的抛出的异常对象,2. 所以他只能跳跃执行流注意是跳跃,直接回到调用当前函数的调用函数栈 func2 ,也就是下面的代码直接不执行了,就好像"我就倔,我今天非要解决你这问题",然后回到func2 ,但是注意哈来到func2 ,必须保证调用func3的函数调用也在一个try -catch中
然后在这个try-catch中寻找,最后发现没有解决,有展开调用栈最后回到func1 ,func1带哦有的func2的try-catch最终捕获了。
我图中说如果func1没有捕获到就直接报错了是不严谨的哈,因为func1可能也被其他函数调用,最终栈展开哈,回到主函数! 如果主函数都没有捕获这个异常对象,最终异常处理机制就会调用abrt()终止当前进程。

2.2.2 throw 直接原地抛出当前对象
如下代码: 简单来说,当我们这里抛出了myclass对象当前try-catch确实捕获了,但是他直接throw 这就表示对当前捕获的对象不处理原地抛出
goood 所以再次栈展开最终输出结果你猜猜,下文有输出:
class myclass {
public:
void test()const {
cout << "我是一个测试异常类" << endl;
}
int _a;
string _b;
};
void test_use2() {
try {
throw myclass(); // 注意抛出int类型
}
catch (myclass&obj) {
// 捕获到了我们不处理直接抛出
throw;// 直接抛出表示将捕获到的对象直接抛出
}
catch (string str) {
cout << "hello i catch you:" << str;
}
}
int main() {
try {
test_use2();
}
catch(myclass&obj){
cout << "在main中捕获到obj" << endl;
}
catch (...) {
cout << "在主函数捕获到异常类型" << endl;
}
return 0;
}

2.2.3 c++异常导致的执行流跳跃
异常处理千般好,执行流跳跃,同如大坑的goto语句,执行流跳跃问题很明显:
倘若一不小心那就是内存泄露: 如下场景 : 当函数mytest2()抛出异常,后面原本要析构当前函数栈帧动态开辟的内存,delete可是你直接越过了,那太好了,直接内存泄露,呜呼哀哉,然后又展开调用栈,捕获到了,问题就是内存已经泄露了,这就是执行流跳跃的坏处。
所以为了规避这样的详细,对应动态开辟的内存,c++中的思路是RAII ,具体来说就是使用智能指针来管理这样的情况。
class myclass {
public:
void test()const {
cout << "我是一个测试异常类" << endl;
}
int _a;
string _b;
};
void mytest2() {
try {
string str = "hello wrold";
myclass* c1 = new myclass;
int *pa = new int[5] ;
throw "出问题了";
delete pa; // 这里就不会被调用了
delete c1;// 这里不会执行 导致内存泄露
}
catch (int a) {
}
}
void mytest1() {
try{
mytest2();
}
catch(string str){
}
int main(){
mytest1();
return 0;
}
}
三. 自定义异常规划&&C++11的异常规范标准
3.1 自定义异常体系结构

如上我们简单花了一个自定义异常体系的类图,原因是因为,在捕获的时候,catch(基类) 是可以捕获子类元素的,这就是多态嘛,这是很关键的一步,所以我们通常在很多公司都会自定义异常体系结构,基类大概率是一个纯虚函数,内部封装一个waht方法,然后定义相关子类,我们来模拟写一个自定义的异常体系吧。
样例体系代码:
#include<string>
#include<thread>
#include <chrono>
#include<Windows.h>
// 自定义异常体系结构
class Myexception {
public:
Myexception(const string error_message, int id)
:_error_message(error_message)
, _id(id) {
}
virtual string what()const {
return _error_message+":"+to_string(_id);
}
protected:
int _id;// 表示错误id号
string _error_message;
};
class memory_exception :public Myexception{
public:
memory_exception(const string error_message, int id,const string&memory_type)
:Myexception(error_message,id),
_memory_type(memory_type)
{}
string what() const override {
return _error_message + ":" + to_string(_id) + _memory_type;
}
protected:
string _memory_type;
};
class HttpServer_Exception :public Myexception {
public:
// 错误码 如 404
HttpServer_Exception(const string& error_message, int id, const int erro) :
Myexception(error_message,id) ,
_erro(erro){
}
// 重写
string what()const override {
return _error_message + to_string(_id) + ":" + to_string(_erro);
}
protected:
int _erro;
};
// 我不在函数写try-catch模块 我最终在主函数 等着用基类捕获你们
void Test_http() throw(HttpServer_Exception){
while (1) {
// 模拟工作 然后突然出问题
std::this_thread::sleep_for(std::chrono::seconds(2));
// 休眠后抛异常
throw HttpServer_Exception("这里发生httpsever异常", -1, 404);
}
}
// throw(类型1,类型2) 如果是 throw()空括号表示
// 声明当前函数不会抛出异常,否则自动调用abrt()终止进程
void Test_1() throw(){
// .... 啥也不不做
}
void Test_memory()throw(memory_exception, HttpServer_Exception) {
//模拟 工作 休眠1s
std::this_thread::sleep_for(std::chrono::seconds(2));
throw memory_exception("这里发生内存异常", -1,"空指针访问");
}
int main() {
/// 在主函数调用 在主函数 包上try-catch即可 然后基类捕获他们
try {
Test_http();
}
catch (const Myexception& e) {
cout << e.what()<<endl;// 在这直接捕获即可 然后输出异常信息
}
try {
Test_memory();
}
catch (const Myexception& e) {
cout << e.what() << endl;// 在这直接捕获即可 然后输出异常信息
}
//std::this_thread::sleep_for(std::chrono::seconds(2));
cout << "hello;";
//Sleep(10000);// windows 的是 毫秒级别的 跨平台的c++的线程库也有
cout << "hello;";
}

3.2 C++标准异常体系结构
基类的what字段

标准异常类图结构


理解
// 简单来说 c++的异常基类里面也包含了一个 what
// 还有一些错误信息的字段
class exception{
public:
exception(const std::string&data,int id){
_data = data;
_id = id;
}
virtrul std::string what(){
return _data;
}
protected:
std::string _data;
std:: int _id;
}
3.2 简单的使用&&简单的stl容器的异常处理
像c++的异常有基类,其实我们也可以继承他的基类然后写自己的异常体系结构,但是生产实践当中大家都不乐意,其实c++的异常体系结构执行流的跳跃有很大的好处,就是 他会不断的展开调用栈知道回到main函数,那么我们只会可以尽量在主函数用基类捕获就就行了,也不会除太大的问题,这也就不用像错误码那样的方式要不停的自主判断调用返回。
这也的方式也算不错,但是执行流的跳跃也会有内存的泄露问题,但是有了智能指针,其实也不算差,对于动态的内存又有异常的情况下,动态内存尽量用智能指针来指向这是一个不错的注意。
当然你要是直接用c++异常的,那你得看人家有些容器的方法也没有保护异常判断字段,就比如vector的operator[] 就没有异常啊 如下代码:
void test_vector_e() {
vector<int> v = {1,2,3,4,5};
//v[10] = 2;// 这个[]方法内部没有封装 cpu检测到你越界 直接就挂了
v.at(10); // at方法内部封装了 但是要注意在main()函数要保证异常哈
// 不然人家at抛出去你都不捕获。。。
}
int main() {
try {
test_vector_e();
}
catch (const exception& e) {
cout << e.what() << endl;
}
return 0;
}

更多推荐


所有评论(0)