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

⭐️博主: 此生决int-@CSDN博客
** 速胜派就是最大的投降派!!!**

**         🔥热门专栏🔥**

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

      快速复习系列Java 速通系列


文章目录

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

上期回顾

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

1. 异常的概念及使用

1.1 异常的概念

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

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

但是,万一-1也有可能是要返回的呢?再说了,你出错返回-1,我写的返回0,所以,要用一种统一的方式来管理这种情况,处理这种异常情况。

于是 C++ 提供了异常机制(Exception Handling)

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

1.2 异常的抛出和捕获

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

例如:

cpp 复制代码
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

cpp 复制代码
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思想。即:

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

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

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

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

1.4 查找匹配的处理代码

我们上面说了,throw出来的类型要和catch对应

实际代码通常不是只有一个 catch

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

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

cpp 复制代码
class Exception
{
};

class FileException : public Exception
{
};

如果:

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

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

注意:

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

异常匹配并不是按照普通函数调用/赋值时的所有隐式转换规则来进行匹配的 ,所以,不要依赖数值类型之间的转换来设计异常匹配;让 throw 的类型和 catch 的类型明确对应。

,另外,如果异常一直没有合适的匹配,

那么程序会调用:

cpp 复制代码
std::terminate()

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

1.5 异常重新抛出

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

输出:

text 复制代码
Func捕获异常:10
main再次捕获:10
throw;throw e;的区别

throw 抛出当前异常!原封不动

throw e; 抛出异常e

例子:

cpp 复制代码
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 异常安全问题

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

cpp 复制代码
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(逻辑错误)

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

常见:

cpp 复制代码
invalid_argument
out_of_range
length_error
domain_error
runtime_error(运行时错误)

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

常见:

cpp 复制代码
range_error
overflow_error
underflow_error
invalid_argument(无效参数)

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

out_of_range(越界错误)

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

例如:

cpp 复制代码
vector<int> v = {1, 2, 3};
cout << v.at(10) << endl;
length_error(长度错误)

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

cpp 复制代码
string s; 
s.resize(s.max_size() + 1); // 长度超过允许范围
domain_error(定义域错误)

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

overflow_error(上溢错误)

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

underflow_error(下溢错误)

计算结果出现了下溢。

bad_alloc(内存分配失败)

动态内存分配失败。

cpp 复制代码
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,轻松备战期末考试


**  愿每一次敲下键盘,都比昨天更进一步!** **  愿每一行代码落下,都让未来多一种可能!**

相关推荐
曼巴UE51 小时前
UE5 客户端 需要的网络同步概念总结(3 )-事件同步 RPC 以及三种调用方式
网络·c++·网络协议·学习·rpc·ue5
乌暮1 小时前
Java 抽象类与接口详解:半成品图纸 vs 能力合同
java·开发语言·后端·学习
All for pursuit.1 小时前
【栈-5】84.柱状图中最大的矩形
数据结构·c++·算法·leetcode
01二进制代码漫游日记1 小时前
C++之类和对象(上)02
c++
乐观的Terry1 小时前
ShipDesk 完整使用教程:从新建项目到 SSH 自动发布
开发语言
渡我白衣2 小时前
HttpRequest与HttpResponse的实现
服务器·数据结构·c++·人工智能·tcp/ip·机器学习·caffe
刃神太酷啦2 小时前
前端入门第一课:HTML 基础语法 + 常用标签 + 实战全解
服务器·c语言·前端·javascript·css·c++·html
蒸蒸yyyyzwd2 小时前
八股学习笔记 day42
c++·八股
凤年徐2 小时前
C++ 仿 muduo 高并发服务器:用户态缓冲区 Buffer 模块实现
linux·c++