自学内容网 自学内容网

C/C++语言基础--C++异常看这一篇就够了

本专栏目的

  • 更新C/C++的基础语法,包括C++的一些新特性

前言

  • 通过前面几节课,我们学习了抽象、封装、继承、多态等相关的概念,接下来我们将讲解异常,异常是专门处理错误的;
  • 这一次加了不少图标,希望大家喜欢;
  • C语言后面也会继续更新知识点,如内联汇编;
  • 欢迎收藏 + 关注,本人将会持续更新。

👀 先看问题

我们先来看这一段代码:

void _div(double a, double b) 
{
    std::cout << a / b << std::endl;
}

这段代码很简单,就是实现一个除法运算,但是这个代码有一个很大的bug🍴🍴🍴,就是没有判断b!=0的情况,所以很多人写代码的时候是这样写的:

void _div(double a, double b) 
{
    if(b == 0) {
        std::cout << "除数不能为0" << std::endl;
        return;
    }
    std::cout << a / b << std::endl;
}

🚫 🚫🚫 ​ 但是如果我们忘记了写判断,而这一段代码在一个程序中无非就是一个小部分,如果我们忘记写这个判断了,程序就会引发中断,而这个是逻辑错误,不是代码错误,编译器是不会提示的,所以这个时候我们就需要一个一个去找bug的位置了🐛🐛🐛🐛🐛🐛,那有什么更好的解决方法呢?

当然有💪,这就是异常

❓ 什么是异常?

异常是程序在执行期间产生的问题,也就是指在程序运行时发生的特殊情况,比如我们上面的尝试除以零的操作。


为什么异常可以这样解决程序中产生的问题呢?

概念:异常处理提供了一种可以使程序执行的某点将控制流和信息转移到与执行先前经过的某点相关联的处理代码的方法,换言之,异常处理就是将控制权沿调用栈向上转移

🔑 关键:

  • 将程序运行的的PC指针沿着栈向上移动
  • 换句话说:就是将程序出现异常的时候那个位置移动到另外一个位置。

4️⃣ C++处理异常API

C++ 异常处理涉及到四个关键字:**try、catch、throw、noexcept **。

  • throw::程序会抛出一个异常。

  • catch: 通过异常处理程序捕获异常

  • try: try中存放是否需要检查有异常的的代码段,它后面通常跟着一个或多个 catch 块。

    • try
      {
         // 保护代码 
      }catch( ExceptionName e1 )  // ExceptionName 异常类型名字
      {
         // catch 块
      }catch( ExceptionName e2 )
      {
         // catch 块
      }catch( ExceptionName eN )
      {
         // catch 块
      }
      
  • **noexcept :**用于描述函数不会抛出异常,一旦有异常抛出,会立刻终止程序,它可以阻止异常的传播与扩散;

    • 扩展:noexcept可以带一个“常量表达式作为参数,常量表达式为true,表示不会抛出异常,否则代表可以抛出异常,如:noexpect(true) (也是默认的),noexpect(false)
    int show() noexpect  // 声明这个函数不会抛出异常
    {
        throw 0;
    }
    
    int show() noexpect(false)  // 声明这个函数*** 会 *** 抛出异常
    {
        throw 0;
    }
    

↗️ 抛出异常

我们上面可以知道,跑出异常的关键字是throw,那具体怎么使用呢?我们这里以解决我们上面的案例为例❗️❗️

double _div(double a, double b)
{
   if( b == 0 )  // 除数为0,错误,抛出异常
   {
      throw "Division by zero condition!";    
   }
   return (a / b);
}

🐱 捕获异常

catch 块跟在 try 块后面,用于捕获异常。我们可以指定想要捕捉的异常类型,通过指定 catch 关键字即可。

double _div(double a, double b)
{
try
{
if (b == 0)  // 除数为0,错误,抛出异常
{
throw std::runtime_error("Division by zero condition!");
}
return (a / b);
}
catch (std::exception& err) {
std::cout << err.what() << std::endl;
}
}

上面的代码会捕获一个类型为 std::runtime_error的异常,catch存放处理异常方法,这样就不会发送中断了,运如图:

在这里插入图片描述


抛出异常代码模板结构大概如下:

try
{
   // 保护代码
}catch(...)
{
  // 能处理任何异常的代码
}

当然,catch还可以多个,多次匹配不同的问题:,如下代码🕶🕶🕶

double _div(double a, double b)
{
try
{
if (b == 0)  // 除数为0,错误,抛出异常
{
throw 0;
}
return (a / b);
}
catch (const char* err) {
std::cout << err << std::endl;
}
catch (int& err) {
std::cout << 1 << std::endl;
}
}

运行结果:

在这里插入图片描述

由于我们抛出了一个类型为 int 的异常,在捕获异常的时候,他会匹配不同抛出异常的类型,我们抛出的是整形

所以C++会自动寻找相对应位置。

如果throw的类型,在catch中没有找到会怎么样子呢?这个就留给读者思考吧🤔🤔🤔🤔

⭐️ C++ 标准的异常

C++ 提供了一系列标准的异常,定义在 中,这些异常是我们程序中容易犯错的结果,我们可以在程序中使用这些标准的异常,异常家族👪👪👪结构图如下(一部分):

在这里插入图片描述

下表是对上面层次结构中出现的每个异常的说明:

异常描述
std::exception该异常是所有标准 C++ 异常的父类
std::bad_alloc该异常可以通过 new 抛出。
std::bad_cast该异常可以通过 dynamic_cast 抛出。
std::bad_exception这在处理 C++ 程序中无法预期的异常时非常有用。
std::bad_typeid该异常可以通过 typeid 抛出。
std::logic_error理论上可以通过读取代码来检测到的异常。
std::domain_error当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument当使用了无效的参数时,会抛出该异常。
std::length_error当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator
std::runtime_error用于表示那些在程序运行时可能发生的错误.
std::overflow_error当发生数学上溢时,会抛出该异常。
std::range_error当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error当发生数学下溢时,会抛出该异常。

结合我们之前学过的面向对象,封装、继承、多态,我们可以发现,这些异常都是继承exception类,所以,对于这些标准异常的捕获处理,我们就可以得到如下代码结构

try {
    throw 标准库中含有的异常;
}
catch(std::exception& err){
    
} 

🆕 定义新的异常(有头文件)

您可以通过继承和重载 exception 类来定义新的异常。下面的实例演示了如何使用 std::exception 类来实现自己的异常:

#include <iostream>
#include <exception>
using namespace std;
 
struct MyException : public exception//exception 在std命名空间里面
{
  const char * what () const
  {
    return "C++ Exception";
  }
};
 
int main()
{
  try
  {
    throw MyException();
  }
  catch(MyException& e)
  {
    std::cout << "MyException caught" << std::endl;
    std::cout << e.what() << std::endl;
  }
  catch(std::exception& e)
  {
    //其他的错误
  }
}

这将产生以下结果:

MyException caught
C++ Exception

🉑 万能接收异常

C++考虑的很全面,提供了一个能够捕获万能捕获异常的方法,...,代码结构如下:

try {
    throw "hello";
} catch (...) {
    
}

我们将案例用这个方法捕获:

double _div(double a, double b)
{
try
{
if (b == 0)  // 除数为0,错误,抛出异常
{
throw 0;
}
return (a / b);
}
catch (...) {
std::cout << __FUNCTION__ << " 代码有问题" << std::endl;
}
}

结果:

在这里插入图片描述

⏹ 抑制new抛异常

当使用new申请内存时,如果内存申请失败,会抛出std::bad_alloc异常,如果像让他不发生异常则需要如下处理(学习github某一位老师的笔记)。

  • 测试的时候,需要换成 x86 环境下(2g),x64 理论上无限内存
try
{
while (true)
{
new char[1024];
}
}
catch (const std::bad_alloc& e)
{
cout << "has exception "<<e.what() << endl;
}

如果想根据返回的指针来判断,就需要抑制new抛出异常。

double* p = nullptr;
do
{
 p = new(std::nothrow) double[1024];      //声明让 new 不抛出异常
} while (p);

🙈 函数中捕获异常注意点

当在函数中没有匹配处理该抛出异常的操作,这个时候会他会到函数调用的地方去寻找匹配,如下:

double _div(double a, double b)
{
try
{
if (b == 0)  // 除数为0,错误,抛出异常,  *** 但是没有匹配 0 的代码 ***
{
throw 0;
}
return (a / b);
}
catch (const char* msg) {  // 匹配字符串类型的
std::cout << __FUNCTION__ << " 代码有问题" << std::endl;
}
}

// 这个时候回到函数调用的地方找寻
int main()
{
try {
_div(10, 0);
}
catch (...) {  // 这个地方找
std::cout << "main" << std::endl;
}


}

运行结果图如下:

在这里插入图片描述

⚛️ 异常注意点:

  • 类的构造函数不抛出异常

  • 异常不能乱用,C++一般用的不多,不像java那样,动不动就抛一个异常,以下是使用异常的一些标准

    1. 如果程序是逻辑错误,则不应该抛出异常,应该解决他

    2. 如果后面的代码,依赖这个结果,那么这个有异常情况,则可以抛出异常

    3. 如果后面的代码不依赖这个结果,则不应该抛出异常

    4. 异常不能用if……else代替


原文地址:https://blog.csdn.net/weixin_74085818/article/details/142965956

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!