C语言的处理错误方式
- 错误码: 比如在很多C语言系统调用的时候,调用错误都会返回其对应的错误码,这个就很不好找,还要去查询这个错误码对应的意思
- 终止程序: assert,但是内存错误,除0,之后直接就终止程序了
C++异常概念
- throw:出现错误的时候,可以去抛出一个异常,抛出一个对象,(字符串,有更多的信息,整形,浮点型,自定义类型,这些对象都是可以的)
- catch:处理错误的地方,可以去捕获
- try:和catch进行搭配
#include<iostream>
using namespace std;
double Division(int a,int b)
{
if(b==0)
{
throw"division by zero condition";//b是0的话,就出现错误,不让程序终止掉
//抛一个字符串出来
}
else
return ((double)a/(double)b);//直接除,如果发生除0错误,就直接终止掉,我们不想
}
void func()
{
int len,time;
cin>>len>>time;
cout<<Division(len,time)<<endl;
}
void demo1()
{
try{
func();
//如果正常执行的话,下面的catch就不会执行
//如果有异常的话,就直接就跳到了catch的地方了
}
catch(const char* errmsg)//捕获一下,和抛出的类型是要匹配的
{
cout<<errmsg<<endl;
}
catch(...)
{
cout<<"unknown exception"<<endl;
}
}
int main()
{
demo1();
return 0;
}
异常的使用
异常的抛出和捕获
异常的抛出和匹配的原则
- 异常是通过throw抛出对象而引发的,而该对象的类型决定了应该会激活哪一个catch处理代码
double Division(int a,int b)
{
if(b==0)
{
throw"division by zero condition";//b是0的话,就出现错误,不让程序终止掉
//抛一个字符串出来
}
else
return ((double)a/(double)b);//直接除,如果发生除0错误,就直接终止掉,我们不想
}
void func1()
{
int len,time;
cin>>len>>time;
cout<<Division(len,time)<<endl;
}
void func2()
{
int len,time;
cin>>len>>time;
if(time!=0)
throw 1;//抛出一个int类型变量
else
cout<<len<<" "<<time<<endl;
}
void demo1()
{
try{
func1();
//如果正常执行的话,下面的catch就不会执行
//如果有异常的话,就直接就跳到了catch的地方了,
func2();
}
catch(const char* errmsg)//捕获一下,和抛出的类型是要匹配的,抛出异常之后程序就终止了
{
cout<<errmsg<<endl;
}
catch(int errid)//catch是可以有很多个
{
cout<<errid<<endl;
}
catch(...)
{
cout<<"unknown exception"<<endl;
}
}
- 被选择的处理代码是调用链里面和该对象类型匹配且抛出异常位置最近的那个
如果离它近,不匹配也不会去调用
double Division(int a,int b)
{
if(b==0)
{
throw"division by zero condition";//b是0的话,就出现错误,不让程序终止掉
//抛一个字符串出来
}
else
return ((double)a/(double)b);//直接除,如果发生除0错误,就直接终止掉,我们不想
}
void func1()
{
try{
int len,time;
cin>>len>>time;
cout<<Division(len,time)<<endl;
}
catch(const char* errmsg)//这个地方近,就到这里,不到下面的catch
{
cout<<errmsg<<endl;
}
}
void demo1()
{
try{
func1();
//如果正常执行的话,下面的catch就不会执行
//如果有异常的话,就直接就跳到了catch的地方了,
func2();
}
catch(const char* errmsg)//捕获一下,和抛出的类型是要匹配的,抛出异常之后程序就终止了
{
cout<<errmsg<<endl;
}
catch(int errid)//catch是可以有很多个
{
cout<<errid<<endl;
}
catch(...)
{
cout<<"unknown exception"<<endl;
}
}
- 抛出异常对象之后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象(生命周期在catch之后就销毁了)
double Division(int a,int b)
{
if(b==0)
{
string errmsg("/0");
throw errmsg;//抛出这个errmsg,这个errmsg是一个临时对象,
}
else
return ((double)a/(double)b);//直接除,如果发生除0错误,就直接终止掉,我们不想
}
void func1()
{
try{
int len,time;
cin>>len>>time;
cout<<Division(len,time)<<endl;
}
catch(const char* errmsg)//这个地方近,就到这里,不到下面的catch
{
cout<<errmsg<<endl;
}
}
void demo1()
{
try{
func1();
//如果正常执行的话,下面的catch就不会执行
}
catch(const string& errmsg)//捕获一下,和抛出的类型是要匹配的,抛出异常之后程序就终止了
{
//有点像实际参数传给形参
//把对象进行拷贝捕获
cout<<errmsg<<endl;
}
}
- catch(…)可以捕获任意类型的异常,但是不知道异常的错误是什么
catch(...)//这个就可以捕获上面我们没写的位置类型的异常,程序就不会终止了
{
cout<<"unknown exception"<<endl;
}
- 不管在哪里出现了错误,我们捕获都是统一在一层(最外层,网络层进行处理),捕获记录日志,但是我们一般都不抛字符串,整形这些,而是抛出自定义类型,实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,用基类捕获,在实际非常实用
catch(const Exception& e)//用一个基类来进行捕获
{}
throw可以抛出Exception对象,或者Exception的子类对象
- 如果异常被捕获,那么catch后面的语句会继续执行
void func1()
{
try
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
catch (const string &errmsg) //捕获一下,和抛出的类型是要匹配的,抛出异常之后程序就终止了
{
//有点像实际参数传给形参
//把对象进行拷贝捕获
cout << errmsg << endl;
}
func2();//会执行
}
- 异常的重新抛出
void func()
{
int *arr = new int[10]; //可以用只能指针尝试处理
int len, time;
cin >> len >> time;
try
{
cout << Division(len, time) << endl;
}
catch (const string &errmsg) //这里提前把他捕获了,后面也能正常运行
//拦截异常不是要处理异常,而是要正常释放资源
{
cout << "delete []" << arr << endl; //这样就可以了,但是我们希望最外层进行处理
delete[] arr;
throw errmsg; //重复抛出异常
}
catch (...) //这里提前把他捕获了,后面也能正常运行
//拦截异常不是要处理异常,而是要正常释放资源
{
cout << "delete []" << arr << endl; //这样就可以了,但是我们希望最外层进行处理
delete[] arr;
throw; //捕获到什么就抛出什么对象
}
//这个地方抛异常,后面的代码就不会执行了,就会出现内存泄露的问题
//异常安全的问题
cout<<"delete []"<<arr<<endl;//这样就可以了,但是我们希望最外层进行处理
delete[] arr;
}
自定义异常体系
写一个基类异常对象
子类异常对象都去继承它
//异常的继承体系
class Exception //异常的基类对象
{
public:
Exception(const string &errmsg, int &id)
: _errmsg(errmsg), _id(id)
{
}
virtual string what() const //多态
{
return _errmsg;
}
protected:
string _errmsg; //错误的描述
int _id; //错误的编号,区分某种错误,对某种错误进行特殊处理
//比如假如说网络错误的话,我们不能直接反馈信息,而是要重复去尝试10次直到不行,才反馈,所以id就是表示错误的编号
};
class SqlException : public Exception
{
public:
SqlException(const string &errmsg, int id, const string &sql)//记录一下sql语句
: Exception(errmsg, id), _sql(sql) //调用父类的构造函数
{
}
virtual string what() const //多态,子类重写了父类的虚函数
{
string str = "SqlException :"; //前缀
str += _errmsg;
str+="->";
str += _sql;
return str; //这样就行了
}
private:
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:
string _type; //请求类型是什么
};
void SQLMgr()
{
if (rand() % 9 == 0)
{
throw SqlException("权限不足", 100,"select * from name = 'zhansang'"); //但是我们怎么知道是谁的权限不足
}
//但是可能三者都会抛异常
}
void CacheMgr()
{
if (rand() % 3 == 0)
{
throw CacheException("权限不足", 100); //但是我们怎么知道是谁的权限不足
}
if (rand() % 5 == 0)
{
throw CacheException("数据不存在", 101);
}
//...缓存里面再查到数据库
SQLMgr();
}
//我们可以抛任意类型的异常,但是必须要继承父类
void HttpServer()
{
//...数据来了网络先接收,走到缓存里面去
if (rand() % 2 == 0)
{
throw HttpException("请求资源不在", 100, "get");
}
if (rand() % 7 == 0)
{
throw HttpException("权限不足", 101, "post");
}
throw "xxxxxx";//未知异常,程序不会崩
CacheMgr();
}
#include <chrono>
#include <thread>
#include <unistd.h>
void ServerStart()
{
while (1)
{
this_thread::sleep_for(chrono::seconds(1));
try
{
HttpServer();
}
// 这个下面就是异常的捕获
catch (const Exception &e) // const类型的this指针,里面的成员也都要在最后面加const
{
//我们可以用一个多态,
// 1.虚函数的重写 2. 父类的指针或引用去调用,
//这样就可以用了,把他们写到对应的日志库里面就行了
std::cerr << e.what() << endl; //捕获去调用这个对象就是多态
}
catch (...)
{
std::cerr << "unknown Exception" << endl;
}
}
}
void demo3()
{
ServerStart();
}
异常安全
- 资源泄露lock和unlock,malloc和free,new 和delete,fopen和fclose,有异常的话会跳过这些,就会造成内存泄漏
- 构造函数和析构函数最好不要抛异常,没有初始化完整,没有释放完整
异常规范
- 网络规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些,可以在函数的后面接throw(类型),列出这个函数可能抛出的异常的所有类型
//这里表示这个函数会抛出A/B/C/D的某种类型的异常
void fun() throw(A,B,C,D);
- 函数后面接throw(),表示这个函数不会抛异常
//不会抛出任何异常
void* operator delete (std::size_t size,void* ptr)throw();//
- throw后面只有一个,说明只会抛出这个异常
//这里只会抛出bad_alloc的异常
void * operator new (std::size_t size) throw(std::bad_alloc);
∗ ∗ 但是异常规范在实际中很难被执行 ∗ ∗ **但是异常规范在实际中很难被执行** ∗∗但是异常规范在实际中很难被执行∗∗
C++11的noexcept关键字
在后面加上noexcept就说明不会抛异常
class exception {
public:
exception () noexcept;
exception (const exception&) noexcept;
exception& operator= (const exception&) noexcept;
virtual ~exception();
virtual const char* what() const noexcept;
}
异常的优缺点
优点
- 可以更加清晰的表示错误,甚至可以包含堆栈调用信息,更容易去定位bug(只能靠日志去进行分析)
- 如果返回错误码的话,调用链很长的话,底层很难拿到对应的错误码
- 很多第三方库都包含异常,比如gtest,glog等等,所以我们使用的话也要使用异常
- 有的函数使用异常更好处理,而向返回值是int这种,返回的不知道是错误码还是正常的值
缺点
- 执行流乱跳,很混乱,这对我们跟踪调试时以及分析程序时,比较困难
- 异常会有一些性能的开销,但是现在这个影响几乎忽略不计
- C++没有垃圾回收机制,资源要自己管理,有了异常很容易照成内存泄露,死锁等异常安全问题(RAII)(智能指针,lock_guard),出作用域就掉析构函数,学习成本高
- C++标准库的异常体系定义的不好,导致很多人各自定义异常体系
- 异常必须要规范,不然外层的用户就很难受(尽量抛子类对象,尽量对接口函数声明抛异常的规范)