C++11异常特性
1.写在前面
在学习了C++11的诸多新特性之后我们也到了C++11新特性学习的尾声,异常,我们从介绍概念+举例子来熟悉异常。
目录
1.写在前面
2.异常的概念
3.异常的抛出与捕获
4.栈展开
5.查找匹配的处理代码
6.异常的重新抛出
7.异常安全问题
8.异常的规范
2.异常的概念
总说:异常就是运行时出现问题作出相应处理。而不是让程序崩溃掉,可以这么说原来我们代码出现数组越界等错误会直接崩掉,但是用异常捕获就不会崩掉,而是抛出异常,这样应用在日常中。当程序运行中发生意外错误时,通过 throw
和 try-catch
机制优雅地处理错误,而不是让程序崩溃。
3.异常的抛出与捕获
程序出现问题时,我们可以用throw来抛出一个对象来引发一个异常。该对象的类型以及当前对象的调用链决定了它被哪个catch捕获。
被选中的代码是距离异常最近且类型匹配的,根据抛出对象的类型和内容,程序抛出异常的部分会告知发生了什么错误。
当throw执行后,throw之后的代码不再执行,程序的执行从throw跳到catch为止,catch可能是同一函数中的,也可能是沿着调用链向上的,这里保证了当代码出现错误提早退出,一旦程序开始执行异常程序,沿着调用链的栈都会被销毁。
抛出异常后,它抛出的是异常对象的拷贝而不是本身,借助传值传参理解,我们传值传参只是拷贝了一分值过去,其实两个值并不一样。
4.栈展开
抛出异常后,程序暂停当前程序的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的调到catch子句处理。
如果当前函数没有catch,try语句,或者有但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述的查找的catch过程叫做栈展开。
如果到达main函数依旧没有找到,程序挂掉。
如果找到catch语句,catch之后的代码会继续执行。
这里注意throw必须在try内部,才会匹配catch语句,这里我们的代码和运行结果可以看出确实是捕获之后继续执行。throw之后的代码暂停执行。
void fun1(){string s("666");throw s;}int main(){try{cout << "继续执行。" << endl;fun1();cout << "继续执行。"<<endl;}catch (const string&s){cout << s;}cout << "继续执行。";return 0;}
5.查找匹配的处理代码
一般情况下抛出的对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置最近的。
但是类型匹配也不是完全严格的,比如我们可以权限缩小,非常量向常量转化,数组转向指针,函数转向函数指针,派生类向基类转化。
如果到main函数,异常没有被捕获,不是发生严重错误的情况下我们不希望程序直接挂掉,所以我们用catch(...)来表示捕获任意异常,这个捕获不知道异常对象的类型,所以不知道异常错误是什么,是最后一道防线。
#include<thread>
// ⼀般⼤型项⽬程序才会使⽤异常,下⾯我们模拟设计⼀个服务的⼏个模块
// 每个模块的继承都是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;}
6.异常的重新抛出
有时catch捕获到一个异常对象后,需要对错误进行分类,其中的某种异常需要进行特殊处理,其它错误则重新抛出给外层处理,捕获异常后需要重新抛出,直接throw意思是捕获到什么异常抛出什么异常,不会发生拷贝,如果throw后跟对象的话又会拷贝,这里又会涉及到派生类和基类的传值拷贝传引用拷贝的区别了,在上述代码中,我们用派生类去重新重写基类中的纯虚函数了,但是纯虚函数无法实例化出对象,如果我们传值拷贝,对象无法实例化就无法拷贝了,其次派生类对象传值传参传给基类还会发生切片。原因如下:
传引用未发生切片实际上是它本身就是没有产生新的对象,它是对原有对象进行操作的。传值传参发生切片是因为它会创建一个新的对象去调用基类的拷贝函数去拷贝,导致了派生类部分的缺失,传引用就是给对象起了个别名,不会去产生新的对象,不会去调基类的拷贝函数去搞,所以不会发生切片。
这里举个例子,我们用微信在发送消息时,它会转圈圈,当转圈圈的时候,实际上这次消息发送已经失败了,是在重新尝试发送,当我尝试到一定程度还不成功后会告诉你失败原因。这里就需要重新抛出对象。
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;
}
解析一下这段代码什么意思:
这里我们发送一条信息,如果发送成功就不会去catch,而是直接到break结束。
如果发生失败,我们会throw异常对象,去匹配catch,如果异常对象是102就会进入循环再次发生,如果不是又会把异常对象抛出去匹配main函数中的catch,catch(...)用来捕获未知的异常。
这段代码我们就运用了上面学习到的异常知识。学习致用。
7.异常安全问题
在上面的学习中,我们知道了如果我们throw之后会跳到catch,throw之后的代码不再执行,但是如果我们在throw之前申请了空间呢?后面的释放空间步骤直接被跳过不会造成内存泄漏吗?
这里就涉及到我们的异常安全问题了。
其次析构函数中我们谨慎使用异常,同样的因为我们手写析构一般都有资源的释放,如果我们抛异常会有很大可能造成内存泄漏的问题呢!
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;
}
这里要注意:在cpp中我们申请空间和释放空间都有可能抛异常,这里我们只申请了一次空间,我们可以去一次捕获,但是如果我们多申请几次呢?每次申请我们都要去进行异常的捕获检查,代码的复杂度上升,可读性降低。后续我们用智能指针来解决这个问题会更好。
8.异常的规范
对于用户来说,知道某个程序有没有可能抛异常是很重要的。
这里我们c++11就规定了,用nocxcept修饰的函数是不可能抛异常的,其它未用noexceot修饰的函数都可能抛异常。
// C++98
// 这⾥表⽰这个函数只会抛出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
size_type size() const noexcept;
iterator begin() noexcept;
const_iterator begin() const noexcept;
double Divide(int a, int b) noexcept
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
int main()
{
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
int i = 0;
cout << noexcept(Divide(1,2)) << endl;
cout << noexcept(Divide(1,0)) << endl;
cout << noexcept(++i) << endl;
return 0;
}