目录
C语言的异常处理方式
C++的异常处理方式
异常的抛出与捕获
抛出与捕获原则
自定义异常体系
异常安全
异常规范
异常的优缺点
优点
缺点
C语言的异常处理方式
1、终止程序
常见形式:assert
缺陷:太过强硬,如果发生内存错误,或者除0语法错误等就会直接终止程序
2、返回错误码
缺陷:需要程序员自己去查找对应的错误,如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误
C语言中基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误
C++的异常处理方式
基本概念:抛异常是编程中的一种机制(c++ / python等),用于在程序遇到错误或异常情况时中断正常的程序流,并将控制权转移到预定的异常处理逻辑中。异常处理使得程序能够优雅地处理错误,而不是直接崩溃(异常是一种运行时错误,可以中断程序的正常流程,常见的异常包括除零错误、文件未找到、网络连接失败等)
- 抛出(Throwing): 使用
throw
关键字将异常对象传递出去 - 捕获(Catching): 使用
try...catch
块来捕获并处理异常
try{// 可能会出现异常的代码
}
//根据类型抛出异常的类型进行捕获
catch( ExceptionName e1 ){//处理方式}
catch( ExceptionName e2 ){//处理方式}
....
- 传播(Propagation):如果异常未被捕获,它会向调用栈的上传递,直到找到一个处理器,如果找不到一个处理器就会使程序终止
- 异常对象:该对象包含了错误的详细信息以及上下文,用于捕获和处理异常
优点:
- 提高代码的健壮性,使得程序能够优雅地处理异常情况
- 提供了错误传播和处理的标准机制
缺点:
- 可能会使代码变得复杂,其中的栈展开机制会打乱正常的执行流
- break、continue、return等虽然也会破坏正常执行流,但并会跨越多个函数
- 不当的异常处理可能会掩盖程序中的逻辑错误
异常的抛出与捕获
抛出与捕获原则
#include<iostream>
using namespace std;
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
throw "Division by zero condition!";
else
return ((double)a / (double)b);//否则返回a / b后的结果
}
void fxx()
{
int i = 0;
cin >> i;
if (i % 2 == 0)//出现偶数时抛异常
{
throw 1;
}
}
void Func()
{
int a, b;
cin >> a >> b;
cout << Division(a, b) << endl;
try
{
fxx();//可能出异常的代码
}
catch (int x)
{
cout <<__LINE__<<"捕获异常:" << x << endl;
}
cout << "=====================" << endl;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (int x)
{
cout << __LINE__ <<"捕获异常:"<< x << endl;
}
cout << "~~~~~~~~~~~~~~~~~~~~~~~~~~~" << endl;
return 0;
}
1、抛出异常的对象的类型决定了会调用哪个catch中的异常处理代码
2、进行异常处理的代码应该是离抛异常位置最近的那一个(就近原则)
3、找到匹配的catch子句并处理后,会继续执行catch子句后的非try...catch块中的内容。不会执行原函数中的内容了
4、抛出异常对象后,会生成一个拷贝该异常对象得到的临时对象(右值),这个临时对象会在被catch后销毁,可以调用移动构造从而省去拷贝构造这一过程
void fxx()
{
int i = 0;
cin >> i;
if (i % 2 == 0)
{
string s("出现偶数");
throw s;
}
}
5、catch(...)可以捕获任意类型的异常对象,用于捕获未知异常对象的情况,是异常捕获的最后一道防线必须加上,防止出现在规定的异常对象外的情况而捕捉不到的情况
6、实际上,抛出对象的类型与捕获的类型并不一定都是匹配的,大多数情况下会抛出派生类对象,使用基类捕获
自定义异常体系
基本概念:实际中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异常,那么外层的调用者基本就没办法使用了,所以实际中都会定义一套基础的规范体系,这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了
#include <iostream>
#include <windows.h>
using namespace std;
// 服务器开发中通常使用的异常继承体系
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
, _id(id)
{}
virtual string what() const
{
return _errmsg;
}
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 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;
};
class A
{
public:
A()
{
cout << "A()" << endl;
_ptr1 = new int;
_ptr2 = new int;
}
~A()
{
cout << "~A()" << endl;
delete _ptr1;
delete _ptr2;
}
private:
int* _ptr1;
int* _ptr2;
};
void SQLMgr()
{
srand(time(0));
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
cout << "!!!!!!!!!!!!!!!!!!!!!!!!运行成功" << endl;
}
void CacheMgr()
{
srand(time(0));
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw CacheException("数据不存在", 101);
}
SQLMgr();
}
void HttpServer()
{
A aa;
srand(time(0));
if (rand() % 3 == 0)
{
throw HttpServerException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw HttpServerException("权限不足", 101, "post");
}
CacheMgr();
}
int main()
{
srand(time(0));
while (1)
{
Sleep(1000);
try
{
HttpServer(); // io
}
catch (const Exception& e) // 这里捕获父类对象就可以
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;//未知捕获
}
}
return 0;
}
-
基类
Exception
:- 用于处理所有异常的基类
- 包含错误消息 (
_errmsg
) 和错误编号 (_id
) - 提供
what()
方法,返回错误描述
-
派生类
SqlException
:- 专门处理与SQL相关的异常
- 添加了 SQL 语句的字段
_sql
- 重写
what()
方法以提供包含 SQL 语句的错误信息
-
派生类
CacheException
:- 处理缓存相关的异常
what()
方法返回基于缓存错误的信息
-
派生类
HttpServerException
:- 用于处理HTTP服务器的异常
- 包括一个请求类型字段
_type
what()
方法返回包括请求类型的详细错误信息
结论:出异常时都是抛出相应的派生类对象,且SQLMgr、CacheMgr、HttpServer三个函数都没有解决抛异常的try...catch块,栈展开后都会回到main函数进行异常处理(描述的可能有误,建议调试查看)
异常安全
1、最好不要在构造函数中抛出异常,否则可能导致对象不完整或者没有完全初始化
2、最好不要在析构函数中抛出异常,析构函数主要用于资源的清理,抛异常可能导致资源泄漏
//可能出问题的代码
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
throw "Division by zero condition!";
else
return ((double)a / (double)b);
}
class A
{
public:
A()
{
cout << "A()" << endl;
_ptr1 = new int;
//在构造函数中设计抛异常
int x, y;
cin >> x >> y;
Division(x, y);//如果此时抛异常了,那么就会进行异常处理,然后直接执行catch子句后的内容,_ptr2未正常初始化1
_ptr2 = new int;
}
~A()
{
cout << "~A()" << endl;
delete _ptr1;
//在析构函数中设计抛异常
int x, y;
cin >> x >> y;
Division(x, y);//如果此时抛异常了,那么就会进行异常处理,然后直接执行catch子句后的内容,_ptr2未释放
delete _ptr2;
}
private:
int* _ptr1;
int* _ptr2;
};
3、在C++异常中经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RALL来解决上述问题
异常规范
基本概念:C++98时对异常抛出有一些建议性的规范,但这并不是强制的,当然实际上这一规范也没能解决使用异常时会出现的一些问题:
- 函数后接throw(),表示此函数不会抛异常
- 函数后接throw(?),表示此函数可以抛出?类型的异常
- 函数后接throw(? / *...),表示此函数可以抛出?或*等类型的异常
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
但是这样子可能会导致兼容性差等的问题
补充:C++11新增的noexcpt关键字,表示也用于表示一个函数不会抛异常
// C++11 中新增的noexcept,表示不会抛异常 thread() noexcept; thread (thread&& x) noexcept;
结论:可能会抛异常时,函数后什么都不加,抛异常的操作在函数内直接写就行,如果缺定该函数不会抛异常,那么就在该函数后加noexcept关键字
异常的优缺点
优点
1、异常对象的处理方式定义好后,相比于错误码可以更加清晰准确的显示出错误的各种信息,甚至可以包含堆栈的调用信息,可以帮助我们更快的定位程序的bug
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
, _id(id)
{}
virtual string what() const
{
return _errmsg;
}
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;
};
...
2、错误码需要层层返回错误,最外层才能拿到错误,异常可以直接跳转,不用层层处理
3、很多第三方库都支持异常,所以我们应该也要去使用
4、部分函数使用异常更好处理,比如构造函数没有返回值,不便使用错误码,比如T& operator这样的函数,如果pos越界了之呢使用异常或终止程序进行处理,没法通过返回值表示错误
缺点
1、异常不仅会导致程序的执行流乱跳,且运行时出错导致的抛异常也会使得程序看起来很混乱,这导致我们跟踪调试以及分析程序时,比较困难
2、C++没有垃圾回收机制,资源需要自己管理,有了异常很容易内存泄漏、死锁等异常安全问题,这需要使用RAII机制来处理资源的管理问题,学习成本高
3、C+标准库的异常体系定义的不好,导致各公司各自定义自己的异常体系,十分混乱
~over~