小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
c++系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
目录
前言
【c++】STL容器-map和set、multimap和multiset的使用和介绍(2.3w字详情解析)——书接上文 详情请点击<——
本文由小编为大家介绍——【c++】c++异常
一、c语言传统的处理错误方式:
处理方式一
终止程序,缺陷用户比较难以接受,例如assert为false的时候会直接终止程序,例如内存错误(数组越界,未初始化/空指针/无效地址的访问,野指针,内存泄露,同一块空间释放多次等等),例如除0错误
- 其它的终止场景可能见的比较多,这里小编只演示比较少见的除0错误
#include <stdio.h>
int main()
{
int a = 3 / 0;
printf("%d", a);
return 0;
}
运行结果如下
处理方式二
返回错误码,缺陷:需要程序员自己查找对应的错误。例如,系统的很多库的接口函数都是通过将错误码放到errno中,表示错误
errno是定义在#include <errno.h>的一个全局变量,使用errno的时候要包头文件
- 举例申请内存过大,内存分配失败返回错误码的情况,当出现内存分配失败,内存不足的时候,返回的错误码是12,对应c语言定义的宏ENOMEM,那么我们就可以以此为根据进行判断
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
int* ptr = (int*)malloc(10000000000000000);
if (ptr == NULL)
{
printf("%d\n", errno);
if (errno == ENOMEM)
{
printf("申请内存不足,返回了错误码\n");
}
}
return 0;
}
运行结果如下
实际中c语言都是通过返回错误码的方式处理错误,部分情况下,使用终止程序处理非常严重的错误
二、c++异常概念
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误的时候就可以抛出异常,让函数直接或间接调用它的调用者处理错误
- throw:当程序出现问题时,会抛出异常,这是通过使用throw关键字实现的
- catch:在你想要处理问题的地方,通过异常处理程序捕获异常。catch关键字可以捕获异常,可以有多个catch捕获异常
- try:try的作用是标识一段可能会抛出异常的代码,当try中的代码遇到问题抛出异常后,抛出的是异常对象,这个异常对象类型决定了后续哪个catch块会被激活并处理该异常
如果有一个块抛出一个异常,捕获异常的方法是使用try和catch关键字,try中放置可能抛异常的代码,catch的函数参数放置与异常对象类型相同的类型,用于捕获异常,在catch的函数体中处理异常。try块中的代码称为保护代码。
- try/catch语句的使用如下
try
{
// 保护的标识代码
}
catch (ExceptionName e1)
{
// catch 块
}
catch (ExceptionName e2)
{
// catch 块
}
catch (ExceptionName eN)
{
// catch 块
}
三、异常的使用
异常的抛出和捕获
异常的抛出和匹配原则
- 异常是通过抛出对象引发的,该对象的类型决定了应该激活哪个catch的处理代码
- 被选中的处理代码是函数调用链中与抛出对象类型匹配且与抛出异常位置距离最近的那一个
- 抛出异常对象后,会生成一个异常对象的拷贝。因为这个抛出的异常对象可能是一个临时对象,临时对象只在它所对应的函数域内有效,出了函数作用域自动销毁,所以要返回异常对象的拷贝,这个返回的异常对象的拷贝的对象会在catch语句结束后销毁(这个过程类似于函数的传值返回)
- catch(…)可以捕获任意类型的对象,但是不知道捕获的异常错误是什么
- 实际中抛出和捕获的类型有个例外,抛出和捕获的类型并不是完全匹配,在实际的应用场景中,可以抛出派生类对象使用基类对象的引用捕获,可以抛出派生类对象的指针使用基类对象的指针捕获,将基类的处理异常的函数定义为虚函数,派生类重写虚函数,那么此时符合多态调用,可以根据实际指向的对象去调用指向对象的异常处理函数去处理异常
在函数调用链中查找与异常对象匹配的catch的语句,栈展开的过程如下
- 首先检查throw本身是否在try块内部,如果在那么再去查找匹配的catch语句。
- 如果有匹配的,则调到catch语句内处理。如果没有匹配则退出当前函数栈,继续在调用当前函数的函数栈中查找匹配的catch。如果到达main函数的栈依旧没有匹配的catch,则终止程序。
- 这侧面说明抛出了异常对象,这个异常对象是一定要被捕获处理的,如果没有捕获处理,那么则会直接终止程序,在实际应用中,这时候为了避免程序终止,即找不到与异常对象类型匹配的catch语句,会在最后加上一个catch(…),这样就可以捕获任意类型的异常对象,那么就会避免程序被终止,缺点是catch(…)无法得知异常错误
- 同时上面的这个沿着函数调用链查找匹配的catch语句的过程称为栈展开
- 在找到匹配的catch语句并处理后,会沿着catch语句后面的语句继续执行
- 下面小编以除零操作抛异常为例进行讲解栈展开的过程
void division(double a, double b)
{
if (b == 0)
{
throw "Division by zero is not allowed";
}
else
{
cout << a / b << endl << endl;
}
}
void fun1()
{
double a, b;
cin >> a >> b;
division(a, b);
}
int main()
{
try
{
fun1();
}
catch (const char* str)
{
cout << str << endl << endl;
}
catch (...)
{
cout << "unknown false" << endl << endl;
}
cout << "继续执行后续语句" << endl;
return 0;
}
运行结果如下
- 当输入的是非除零操作的时候,此时不会抛异常
- 当输入的是除零操作的时候,此时会进行抛异常,会先检查throw自身是否在try语句内,检查不在,那么会去调用当前函数的函数栈中去查找匹配的catch语句,查找没有,那么会去main函数的栈中查找与异常对象类型的匹配的catch语句,查找匹配成功catch语句,被激活的catch语句被激活,会在catch语句中处理异常,处理完成后会继续执行catch语句之后的语句
异常的重新抛出
有时候单个catch不能完全处理异常,在经过一些校正处理后,希望交给更外层的调用链函数来处理,catch则可以通过重新抛出异常,将异常交给更外层的函数处理,使用catch(…)捕获的任意类型的异常对象,可以直接使用throw即可进行异常的重新抛出
- 异常的抛出会导致执行流的跳转到与异常对象类型匹配的catch语句中,这个跳转可能是函数内的,也有可能是函数外的
- 例如当抛出异常进行跳转前,函数中如果new出了一个对象,在当前函数的结尾使用delete对对象进行释放,但是与异常对象类型匹配的catch语句是在调用当前函数的函数的栈中,那么执行流就会跳转到调用当前函数的函数的栈中,那么当前函数中的new出的对象就没有办法得到释放,此时就会发生一个严重的问题,内存泄露,如何处理呢?
- 那么为了避免内存泄露,我们通常是使用多个catch语句,例如使用catch(…)捕获异常后,进行一些矫正处理,即提前使用delete释放对象,再使用throw抛出异常对象,让更外层的调用链函数处理异常
void division(double a, double b)
{
if (b == 0)
{
throw "Division by zero is not allowed";
}
else
{
cout << a / b << endl << endl;
}
}
void fun1()
{
int* c = new int(1);
double a, b;
cin >> a >> b;
try
{
division(a, b);
}
catch (...)
{
cout << "提前释放内存,防止内存泄露" << endl;
delete c;
throw;
}
cout << "正常delete释放内存" << endl << endl;
delete c;
}
int main()
{
try
{
fun1();
}
catch (const char* str)
{
cout << str << endl << endl;
}
catch (...)
{
cout << "unknown false" << endl << endl;
}
cout << "继续执行后续语句" << endl;
return 0;
}
运行结果如下
- 进行非除零操作,那么执行流不会乱跳转,那么会正常释放内存
- 进行除零操作,那么执行流会乱跳转,那么当存在有内存泄露的风险时,此时我们进行捕获异常,提前delete对象,防止内存泄露,并且进行异常的重新抛出,将异常交给外层的调用链函数处理
异常安全
构造函数进行对象的构造和初始化工作,最好不要在构造函数中抛异常,否则可能会导致对象不完整或不完全初始化
析构函数完成对象的资源清理工作,同样的,最好不要在析构函数中抛异常,否则可能会导致资源泄露(例如内存泄露)的问题
c++中的异常经常会导致资源泄露的问题,例如在new和delete中间抛出了异常,导致内存泄露,以及一些其它资源泄露的问题,这些问题的解决方案,请期待后续小编在智能指针文章中进行讲解
异常规范
异常规格的说明是为了让函数的使用者知道函数可能抛出的异常有哪些
- 可以在函数后面接throw(类型),列出这个函数可能抛出的所有异常类型
- 函数后面接throw(),表示函数不抛出异常
- 若无异常接口的声明,则该函数可以抛出任意类型的异常
1 .例如在c++98异常类的定义中,就有其对应的成员函数不会抛出异常throw()的使用
- 例如在c++98中,我们的new调用的operator new就可能会抛出bad_alloc类型的异常,在异常接口中就有关于这种类型的声明
- 例如在c++98中,我们的delete调用的operator delete中不会抛出异常,那么就会在异常接口的声明中就会加入throw()
- 但是上述c++98的异常接口的声明的方式如果这个函数的可能会抛出的异常类型有很多很多,那么就会造成使用者书写繁琐的问题,所以这种方式的使用几乎很少
在c++11中采用了另外一种方式即使用noexcept,使用noexcept跟在函数后面表示该函数不会抛出异常
- 例如在c++11中关于异常类的定义中就有对noexcept的使用
四、自定义异常体系
实际使用中很多公司都会定义一套自己的异常体系用于进行规范的异常管理
- 因为一个项目中如果大家随意抛异常,那么外层的调用者会接收多少类型的异常对象又分别对应多少种catch和相关的异常处理,在一个百万行代码的项目中光进行异常的捕获就要很繁琐,那么对于外层的调用者来讲就几乎没办法实现。
- 所以在实际中都会定义一套继承的规范体系,基类定义虚函数,派生类继承基类,对基类的虚函数进行重写,这样大家抛出的都是派生类对象或派生类对象的指针,那么只需要捕获一个基类的引用或指针就可以了,这样满足多态调用就会对应的调用指向的对象,即派生类的异常处理函数
- 下面是模仿的服务器开发中的异常继承体系,此时仅需要捕获一个基类对象就可以
#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;
};
void SQLMgr()
{
srand(time(0));
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
//throw "xxxxxx";
}
void CacheMgr()
{
srand(time(0));
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw CacheException("数据不存在", 101);
}
SQLMgr();
}
void HttpServer()
{
// ...
srand(time(0));
if (rand() % 3 == 0)
{
throw HttpServerException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw HttpServerException("权限不足", 101, "post");
}
CacheMgr();
}
int main()
{
while (1)
{
Sleep(500);
try {
HttpServer();
}
catch (const Exception& e) // 这里捕获父类对象就可以,
{ //捕获的是异常对象的拷贝,拷贝后该对象一个临时对象
// 多态 //临时对象具有常性所以引用时要加const
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
return 0;
}
运行结果如下
五、c++标准库的异常体系
c++提供了一系列标准异常,定义在#include <exception>,我们可以在程序中使用这些标准异常。它们是以父子层次组织起来的
- 基类为exception
- 派生类为
那么小编挑几个比较重要异常类的简单讲解一下,同时在实际中我们也可以通过继承exception类去实现我们自己的异常类
异常 | 描述 |
---|---|
std::exception | 该异常是所有c++标准异常的父类 |
std::bad_alloc | 该异常可以通过new抛出 |
std::invalid_argument | 当使用了无效参数时,该类型会抛出异常 |
std::out_of_range | 超出范围异常,该类型可以通过方法抛出,例子见下面 |
- | 续:例如std::vector和std::bitset<>::operator[]() |
- 简单使用c++的标准异常
#include <iostream>
#include <vector>
#include <exception>
using namespace std;
int main()
{
try
{
vector<int> v(10, 0);
//内存不足
v.reserve(1000000000000000000);
}
catch (const exception& e)
{
cout << e.what() << endl << endl;
}
catch (...)
{
cout << "unkonwn exception" << endl;
}
try
{
vector<int> v(10, 0);
//越界
v.at(10) = 10;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "unkonwn exception" << endl;
}
return 0;
}
运行结果如下
六、异常的优缺点
异常的优点
- 异常对象定义好了,相比于c语言错误码的方式,可以更为清晰准确的显示出错误的各种信息,甚至可以包含堆栈的调用信息,可以帮助我们更好的定位程序的bug
- c语言返回的错误码的方式,有一个很大的弊端,在函数的调用链中,深层次的函数返回了错误码,那么我们需要层层返回错误码,在最外层才可以拿到错误码,而异常可以直接跳转到最外层,即将执行流跳转到和返回的异常对象类型匹配的catch语句上
- 很多第三方库都提供异常,例如boost,gtest等常用的库,为了使用第三方库,我们需要使用异常
- 部分函数使用异常更好处理,例如T& operator[]这类的函数,一旦pos越界,只能通过异常或终止程序,无法通过返回值表示错误,因为标识错误的返回值的数据要想要返回必须为T类型,那么可能正好是越界访问位置的数据
异常的缺点
- 异常会导致程序的执行流乱跳转,运行时程序抛异常执行流就会乱跳转,这个跳转可以是在函数内也可以是跨函数跳转,不便于我们追踪调试和分析程序
- 异常由于要返回异常对象的拷贝,所以在性能上会有一定的损失,当然现代硬件设备发展很快,这点损失可以忽略
- c++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致资源泄露,例如内存泄露,死锁等问题。使用到智能指针可以很好的处理这一部分的问题
- c++标准库的异常体系定义的并不好,在实际应用中通常都是各自定义自己的异常继承体系
- 异常需要规范使用,一旦随意抛异常,外层进行捕获操作会非常繁琐,所以需要规范异常:
(1) 抛出异常类型都继承自一个基类
(2) 函数是否抛异常使用noexcept的方式规范化
异常总体而言,是利大于弊,在实际的工程应用中可以使用异常去让工程的更规范化,另外面向对象(OO)的语言,例如c++,java,python等基本都是使用异常处理错误
总结
以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!