云计算百科
云计算领域专业知识百科平台

深入理解C++系列(21)——异常

⭐️博主: 此生决int-@CSDN博客

 速胜派就是最大的投降派!!!

         🔥热门专栏🔥

      深入理解 C++ 系列 | 算法系列

      快速复习系列 | Java 速通系列


文章目录

    • 上期回顾
    • 1. 异常的概念及使用
      • 1.1 异常的概念
      • 1.2 异常的抛出和捕获
      • 1.3栈展开
        • 局部对象会析构
      • 1.4 查找匹配的处理代码
      • 1.5 异常重新抛出
        • `throw;` 和 `throw e;`的区别
      • 1.6 异常安全问题
      • 1.7 异常规范
    • 2. 标准库的异常
        • ① `logic_error`(逻辑错误)
        • ② `runtime_error`(运行时错误)
        • ③ `invalid_argument`(无效参数)
        • ④ `out_of_range`(越界错误)
        • ⑤ `length_error`(长度错误)
        • ⑥ `domain_error`(定义域错误)
        • ⑦ `overflow_error`(上溢错误)
        • ⑧ `underflow_error`(下溢错误)
        • ⑨ `bad_alloc`(内存分配失败)
        • 小结
    • 下期预告
    • 智能指针
    • 结语

上期回顾

上一篇我们主要学习了 C ++11的一些新特性,今天我们来看看C++的异常处理机制!

1. 异常的概念及使用

1.1 异常的概念

我们常常会遇到这样的问题:

int Find(const vector<int>& v, int x)
{
// 找到了返回下标
// 找不到返回 -1
}

但是,万一-1也有可能是要返回的呢?再说了,你出错返回-1,我写的返回0,所以,要用一种统一的方式来管理这种情况,处理这种异常情况。 于是 C++ 提供了异常机制(Exception Handling)。

正常情况下按照正常流程执行;发生异常时,立即跳出当前正常执行流程,把异常交给能够处理它的地方。

1.2 异常的抛出和捕获

异常就三个关键字:try catch throw: 例如:

int Div(int x, int y)
{
if (y == 0)
{
throw "除数不能为0";//throw的是char* str类型
cout<<"你好"<<endl;//永远不会执行,throw后面可以认为函数就退出了
}
return x / y;
}
int main()
{
try
{
//可能出现异常的代码
int ret = Div(10, 0);
cout << ret << endl;
}
catch (const char* str)//char* str类型,类型匹配,才可以捕捉
{
//发生异常后怎么处理
cout << str << endl;//打印错误信息
}
return 0;
}

注意: 1,类型必须匹配! 那么,异常到底是怎么传出去的呢?栈展开!

1.3栈展开

发生异常后,程序从当前函数开始,一层一层往外退,同时销毁这些函数中的局部对象,直到找到匹配的 catch。

void D()
{
throw 10;//抛出异常,并且没有对应的catch捕获,函数结束,返回上一层C
}

void C()
{
D();//没有对应的catch捕获,函数结束,返回上一层C
}

void B()
{
C();//没有对应的catch捕获,函数结束,返回上一层C
}

void A()
{
B();//没有对应的catch捕获,函数结束,返回上一层C
}

int main()
{
try
{
A();//这里有对应的catch,
}
catch (int e)
{
cout << "捕获异常:" << e << endl;
}
}

这个过程就是栈展开!

局部对象会析构

即,栈展开时,函数栈帧里面创建的局部对象会析构掉(正常,函数结束了,局部对象当然要销毁),这里体现了C++里面的RAII思想。即:

把资源的生命周期交给对象管理,在对象析构时自动释放资源

像下面这种情况就不是交给对象管理

int* ptr=new int[4];
throw 10;
delete[] ptr;//这句不会执行,资源没有释放!

意义就是:即使程序因为异常提前离开作用域,已经构造成功的局部对象依然会被正确析构,从而完成资源释放。

1.4 查找匹配的处理代码

我们上面说了,throw出来的类型要和catch对应 实际代码通常不是只有一个 catch:

try
{
// …异常代码
}
catch (int e)
{
cout << "int异常" << endl;
}
catch (double e)
{
cout << "double异常" << endl;
}
catch (const char* e)
{
cout << "字符串异常" << endl;
}
catch (...)//可以匹配任何类型的异常,匹配规则是从上往下依次匹配,所以,通常放到最后
{
cout << "未知异常" << endl;
}

throw也可以处理自定义类型,继承类和派生类

class Exception
{
};

class FileException : public Exception
{
};

如果:

try
{
throw FileException();
}
catch (const Exception& e)//使用&,避免拷贝,const是因为异常通常不会被修改,建议都这样写

{
cout << "Exception" << endl;
}
catch (const FileException& e)//一般都是先捕捉派生类,再捕捉基类,也就是从具体到抽象!
{
cout << "FileException" << endl;
}

注意:

try
{
throw 10;
}
catch (double e)//虽然,int可以隐式类型转换成double,但是不能捕获
{
}

异常匹配并不是按照普通函数调用/赋值时的所有隐式转换规则来进行匹配的,所以,不要依赖数值类型之间的转换来设计异常匹配;让 throw 的类型和 catch 的类型明确对应。 ,另外,如果异常一直没有合适的匹配, 那么程序会调用:

std::terminate()

最终通常导致程序异常终止。

1.5 异常重新抛出

void Func()
{
try
{
throw 10;
}
catch (int e)
{
cout << "Func捕获异常:" << e << endl;//只是出错错误信息,当前还不想处理这个异常,所以,要抛出给外面,并且是,原封不动的抛出
throw;
}
}
int main()
{
try
{
Func();
}
catch (int e)
{
cout << "main再次捕获:" << e << endl;
}
}

输出:

Func捕获异常:10
main再次捕获:10

throw; 和 throw e;的区别

throw 抛出当前异常!原封不动 throw e; 抛出异常e 例子:

class Exception
{
public:
virtual const char* what() const
{
return "Exception";
}
};
class FileException : public Exception//继承
{
public:
const char* what() const override
{
return "FileException";
}
};
try
{
throw FileException();//抛出派生类异常
}
catch (const Exception& e)//基类接收
{
cout << e.what() << endl;
throw;//抛出的还是派生类而不是基类!!!
//throw e抛出的异常是基类,会导致派生类信息丢失,也就是对象切片
}

throw; vs throw e; 总结

写法含义是否保持原异常类型常见用途
throw; 重新抛出当前异常 保持 向上层继续传递异常
throw e; 抛出 e 这个对象 可能发生切片 确实需要重新构造/改变异常对象时
throw xxx; 抛出一个新的异常对象 取决于 xxx 抛出新的异常

1.6 异常安全问题

程序发生异常以后,不能因为异常导致资源泄漏、数据损坏、对象处于不可用状态等问题。

void Func()
{
int* p = new int[100];
// 中间发生异常
throw 10;
delete[] p;//这里不会执行,导致资源泄漏
//所以,不要这么搞,要让对像管理资源
void Func()
{
vector<int> v(100);

throw 10;
}
}

1.7 异常规范

这个概念主要是用来描述:

一个函数可能抛出什么异常,或者保证自己不抛异常。

不过这里一定要注意:**C++历史上有两套不同的异常规范机制。**旧的就不学了,直接看新的 关键字: noexcept,保证不会发生异常

写法含义现代 C++中的情况
void Func() 没有 noexcept 承诺 可以抛异常
void Func() throw(int) 允许特定类型异常 旧式机制,不推荐使用
void Func() throw() 旧式“不抛异常” 已被 noexcept 取代
void Func() noexcept 保证不抛异常 现代 C++常用
void Func() noexcept(false) 不保证不抛异常 可以抛
noexcept(Func()) 判断表达式是否不抛异常 模板/STL中常见

2. 标准库的异常

虽然我们可以直接定义异常的名字,但是库库里面给我们设计好了一些异常类型: 在这里插入图片描述 在这里插入图片描述

① logic_error(逻辑错误)

程序本身的逻辑/使用方式存在问题。 常见:

invalid_argument
out_of_range
length_error
domain_error

② runtime_error(运行时错误)

程序运行过程中发生了无法预先避免的错误。 常见:

range_error
overflow_error
underflow_error

③ invalid_argument(无效参数)

函数接收到一个不合法的参数。参数类型没问题,但参数的值不符合要求。

④ out_of_range(越界错误)

访问的下标、位置等超出了允许的范围。

例如:

vector<int> v = {1, 2, 3};
cout << v.at(10) << endl;

⑤ length_error(长度错误)

对象的长度超过了允许的范围。

string s;
s.resize(s.max_size() + 1); // 长度超过允许范围

⑥ domain_error(定义域错误)

参数虽然类型正确,但不属于这个操作允许的数学/逻辑定义域。

⑦ overflow_error(上溢错误)

计算结果超出了能够表示的范围。

⑧ underflow_error(下溢错误)

计算结果出现了下溢。

⑨ bad_alloc(内存分配失败)

动态内存分配失败。

int* p = new int[100000000000000];

小结
异常含义典型场景
exception 所有标准异常的基类之一 统一捕获
logic_error 逻辑/使用方式相关错误 程序逻辑问题
invalid_argument 参数不合法 函数收到不符合要求的参数
out_of_range 超出合法范围 vector::at() 越界
length_error 长度超过允许范围 容器长度请求过大
domain_error 超出合法定义域 数学/逻辑定义域问题
runtime_error 运行过程中发生的问题 运行环境/运行条件导致的问题
overflow_error 数值溢出 计算结果过大
underflow_error 数值下溢 计算结果过小
bad_alloc 内存分配失败 new 分配失败

下期预告

智能指针

结语

  本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。   也欢迎订阅我的 深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++ 算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线 快速复习系列:知识梳理、查漏补缺,考前冲刺必备 Java 速通系列:已学 C 语言,快速上手 Java,轻松备战期末考试


  愿每一次敲下键盘,都比昨天更进一步!

  愿每一行代码落下,都让未来多一种可能!

赞(0)
未经允许不得转载:网硕互联帮助中心 » 深入理解C++系列(21)——异常
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!