一、异常的概念
异常处理机制允许程序中独⽴开发的部分能够在运⾏时就出现的问题进⾏通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的⼀部分负责检测问题的出现,然后解决问题的任务传递给程序的另⼀部分,检测环节⽆须知道问题的处理模块的所有细节 。
C语⾔主要通过错误码的形式处理错误,错误码本质就是对错误信息进⾏分类编号,拿到错误码以后还要去查询错误信息,⽐较⿇烦。异常时抛出⼀个对象,这个对象可以函数更全⾯的各种信息。
还有C语言的assert(断言)也是如果断言的判断条件成立就报错,这个比错误码更不友好,它是会使程序直接终止的,可能是你的程序一****小块部分有问题,不影响正常使用。
所以总体来看c++的异常还是非常好用的1.1异常的语法
异常是要抛出(throw)和捕获(catch),首先你是哪里的代码或函数需要在运行时捕获异常需要包括在try{} 里面,然后在try里面或者是被调用函数里面用throw 抛异常,抛出的异常可以是对象,然后catch(),相当于一个函数,参数类型要和抛出的对象的类型匹配,但可以是非常量对象被常量参数接受,然后执行catch子句
注意: try和catch是配套使用的,在同一个局部域,写了try,就一定要写catch
而且在当前函数栈帧里的catch不匹配当前抛出的对象类型,就会沿着调用链去寻找是否有匹配的catch, 如果找到匹配的catch⼦句处理后,catch⼦句代码会继续执⾏,并且main函数后面写的代码还会正常运行.如果到达main函数,依旧没有找到匹配的catch⼦句,程序会调⽤标准库的terminate函数终止程序
1.2异常的使用
看下面简单的例子能更好理解,如下是除法运算的函数,如果b是0,也就是分母为0的话就抛出string匿名对象并且里面的信息是分母不能为0,当前函数中有try/catch语句,就会先看这里的catch的参数类型与抛出的异常是否匹配,这里是不匹配,然后销毁函数去调用它的函数里再找,f函数中没有,再去mian函数里找,最后找到了,在执行catch子句里的代码
如果是在main函数还没找到,又因为main后面还有其他要执行的函数不想终止,那就在添加个catch(...), 里面三个点表示接受任意类型的对象,所以如果前面都没匹配一定会调用这个,那么后面的代码也会正常运行
cpp
double Divide(double a, double b)
{
try
{
if (b == 0)
{
throw string("分母不能是0!");
}
else
{
return a / b;
}
}
catch(int errid)
{
cout<< errid << endl;
}
}
void f()
{
int a, b;
cin >> a >> b;
cout<<Divide(a,b) << endl;
}
int main()
{
try {
f();
}
catch (const string& s)
{
cout << s << endl;
}
catch (...)
{
cout << "未知的异常" << endl;
}
return 0;
}
1.3查找匹配的处理代码
⼀般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个。
但是也有⼀些例外,允许从⾮常量向常量的类型转换,也就是权限缩⼩;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派⽣类向基类类型的转换,这个点⾮常实⽤,实际中继承体系基本都是⽤这个⽅式设计的。
因为在实际项目中有多个组去负责部分区块业务的代码,如果要写异常处理的话catch就需要写很多类型,就很繁琐,但在项目中一般会有异常写成基类,每个组负责的区块做派生类,catch子句就能用基类对象接受,再通过多态重写的虚函数就能知道是哪个模块出了问题
下面是一些简单实现的例子,下面包括简单的sql模块,网络层接受请求、和缓存区读取请求,三大模块的简单实现
cpp
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();
}
用rand()生成的随机数来模拟判断,这里的srand就是以当前时间生成随机值,this_thread::sleep_for(chrono::seconds(1));这个是执行完异常后停一秒再随机输入随机值去调用
cpp
#include <thread>
int main()
{
srand((unsigned)time(nullptr));
while (1) {
try
{
this_thread::sleep_for(chrono::seconds(1));
HttpServer();
}
catch (const Exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
}
这里只截取了一部分,因为是死循环输入,这里就能看到只要一个catch的子句捕获抛出的异常对象就行,上面显示有?是可能处理异常时的字符集有问题不影响
1.4异常重新抛出
我们如日常生活中给朋友发消息难免会发生网络不好的情况,我们发送信息其实就是向服务器发送了请求然后再通过服务器发给你朋友,但网络不好可能会发送失败,但它不是请求一次不行就失败,而是很多次才发不出去,就像我们的微信发不出消息就会有红色感叹号,也有可鞥是你被你朋友删掉了.
类比到这里就相当于有时catch到⼀个异常对象后,需要对错误进⾏分类,其中的某种异常错误需要进⾏特殊的处理,其他错误则重新抛出异常给外层调⽤链处理。捕获异常后需要重新抛出,重新抛出可以省略对象直接throw; 就可以把捕获的对象直接抛出。,
下⾯程序模拟展⽰了聊天时发送消息,发送失败补货异常,但是可能在电梯地下室等场景⼿机信号不好,则需要多次尝试,如果多次尝试都发
送不出去,则就需要捕获异常再重新抛出,其次如果不是⽹络差导致的
错误,捕获后也要重新抛出。
cpp
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;
}
}
}
}
1.5异常安全问题
只要抛出了异常,就会回调寻找catch子句,在寻找的过程中,当前函数没有catch子句的话会销毁掉释放资源,这样可能可能造成内存泄漏,如果函数体内有向堆上申请空间的话就释放不了,所以我们需要先释放资源再重新抛出,后面可以通过智能指针章节讲的RAII⽅式解决这种问题是
更好的。
cpp
double Divide(double a, double b)
{
try
{
if (b == 0)
{
throw string("分母不能是0!");
}
else
{
return a / b;
}
}
}
void f()
{
int a,b;
cin>>a>>b;
vector<int>* v= new vector<int>();
catch(...)
{
delete[] v;
cout<< delete[] << endl;
throw;//重新抛出异常
}
int main()
{
try {
Divide(a, b);
}
catch (const string& s)
{
cout << s << endl;
}
return 0;
}
1.6异常的规范
• 对于⽤⼾和编译器⽽⾔,预先知道某个程序会不会抛出异常⼤有裨益,知道某个函数是否会抛出异常有助于简化调⽤函数的代码。
• C++98中函数参数列表的后⾯接throw(),表⽰函数不抛异常,函数参数列表的后⾯接throw(类型1,类型2...)表⽰可能会抛出多种类型的异常,可能会抛出的类型⽤逗号分割。
• C++98的⽅式这种⽅式过于复杂,实践中并不好⽤,C++11中进⾏了简化,函数参数列表后⾯加noexcept表⽰不会抛出异常,啥都不加表⽰可能会抛出异常。
• 编译器并不会在编译时检查noexcept,也就是说如果⼀个函数⽤noexcept修饰了,但是同时⼜包含了throw语句或者调⽤的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是⼀个声明了noexcept的函数抛出了异常,程序会调⽤
• noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,可能会则返回false,不会就返回true。
二、智能指针
上面也提到了抛出异常后有些代码不会执行而导致内存泄漏,所以推出了智能指针来管理这样的情况
2.1RAII和智能指针的设计思路
RAII是ResourceAcquisitionIsInitialization的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、⽂件指针、⽹络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
简单来说就是本质就是智能指针是个类,它是个模板能存任意参数类型的对象它能管理你动态开出来的资源,函数栈帧销毁会调用它的析构函数所以不会发生内存泄漏,而且它里面还重载了很多运算符,也能取到里面的值 ,下面是简单的实现
cpp
template<class T>
class SmartPtr
{
public:
// RAII
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete[] " << _ptr << endl;
delete[] _ptr;
}
// 重载运算符,模拟指针的⾏为,⽅便访问资源
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
};
cpp
double Divide(double a, double b)
{
try
{
if (b == 0)
{
throw string("分母不能是0!");
}
else
{
return a / b;
}
}
}
void f()
{
int a,b;
cin>>a>>b;
SmartPtr<vector<int>> v= new vector<int>();
}
int main()
{
try {
Divide(a, b);
}
catch (const string& s)
{
cout << s << endl;
}
return 0;
}
如上图这样改就能函数栈帧销毁前先析构v就不会发生内存泄漏,我感觉就是利用了对象的生命周期是虽局部域一起销毁的而设计的,并且设计的运算符重载使它能和指针一样使用。后面的知识下一篇博客再深入讲库里的智能指针的使用以及底层实现

