C++异常处理详解 看这一篇就够了

前言

在程序运行的过程中,我们不可能保证我们的程序百分百不出现异常和错误,那么出现异常时该怎么报错,让我们知道是哪个地方错误了呢?
C++中就提供了异常处理的机制。

异常处理的主要关键字

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
  • try:try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块(至少要跟一个)。

当被try的括号包围的代码块(我们可称之为保护代码)出现异常时,就会抛出异常,catch块的代码就会捕获异常并执行catch包围的代码块。因为异常也可以有不同的种类,所以我们可以在try后面跟着多个catch块,捕获不同的异常,如下:

try
{
    
    
   // 保护代码
}
catch( ExceptionName1 e1 )//第一种异常
{
    
    
   // catch 块
}
catch( ExceptionName2 e2 )//第二种异常
{
    
    
   // catch 块
}
catch( ExceptionName3 eN )//第三种异常
{
    
    
   // catch 块
}

抛出异常

使用 throw 语句可以在代码块中的任何地方抛出异常。
throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。
如下:

#include <iostream>
using namespace std;
double division(double a, double b)
{
    
    
    if (b == 0)
    {
    
    
        throw "出现了除数为0的错误";//throw关键字抛出错误
    }
    return (a/ b);
}
int main()
{
    
    
    cout << division(1, 0);
}

如果我们在Visual Studio2019中运行这段代码,那么它就会在throw的地方停止程序并报错,
如果我们在Visual Studio2019中运行这段代码,那么它就会在throw的地方报错

可以看到,我们抛出的是一串字符串,所以编译器报错的信息说异常类型是char(其实是const char*,但它简写了)。
实际我们编程的时候,我们肯定不希望编译器就只给我说异常类型是char*就完了。我们还希望把它打印出来,让我们看得到"出现了除数为0的错误",这就需要捕获异常。

捕获异常

我们把会抛出异常的代码放到try代码块里面,再用catch关键字捕获异常。如下:

#include <iostream>
using namespace std;
double division(double a, double b)
{
    
    
    try
    {
    
    
        if (b == 0)
        {
    
    
            throw "出现了除数为0的错误";//throw关键字抛出错误类型 const char*
        }
    }
    catch (const char* error_string)//捕获const char*类型的错误
    {
    
    
        cout << error_string << endl;//打印它
    }
    return (a / b);
}
int main()
{
    
    
    cout << division(1, 0);
}

在Visual Studio2019中运行它,就会看到:
输出结果:

出现了除数为0的错误
inf

打印了const char*类型的异常
可见,有了try和catch后,程序不会在遇到错误时崩溃终止,而是会处理它,然后继续运行。
所以这个程序依然能输出计算结果inf(无限大),但是在此之前也打印了报错信息。
总之,利用异常处理机制,我们就可以在程序运行时处理异常,大大减少程序崩溃的概率。

你在生活中经常遇到软件闪退的问题,十有八九没有做好异常处理。

C++标准库自带的异常类型

C++ 提供了一系列标准的异常,定义在 头文件exception 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:
图片来自于菜鸟教程
下表是对上面层次结构中出现的每个异常的说明:

  • std::exception 该异常是所有标准 C++ 异常的父类。 std::bad_alloc 该异常可以通过 new 抛出。
  • std::bad_cast 该异常可以通过 dynamic_cast 抛出。 std::bad_typeid 该异常可以通过 typeid
    抛出。 std::bad_exception 这在处理 C++ 程序中无法预期的异常时非常有用。
  • 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 当发生数学下溢时,会抛出该异常。

异常规格说明

throw关键字

异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的声明中列出这个函数可能抛掷的所有异常类型。例如:

void fun() throw(A,B,C,D);

throw后面的括号内,必须写出会抛出的异常的类型,如果括号里面是空的,说明这个函数不会抛出任何异常。
如果你不声明throw(A,B,C,D),那么编译器就会认为这个函数什么异常都可能会抛出。
异常规范声明可以让编译器知道可能会抛出的异常有哪些,就可以提升编译速度和给编译器更多的优化空间,让你的代码性能更高,也可以让其他程序员了解你这段代码可能会产生的异常,提高代码可读性。

noexcept关键字

在C++11中新增了noexcept关键字以表示这个函数不会抛出某种异常。并且可以阻止异常的传播。

无条件的noexcept关键字

当我们声明的noexcept关键字无条件时,表示这个函数中所的所有代码都不会产生异常,如下:

void fun() noexcept; //C++11
void fun() noexcept(); //也可以写成这样,等价的
void fun() noexcept(...); //也可以写成这样,等价的
void fun() noexcept(true); //也可以写成这样,等价的

有条件的noexcept关键字

void fun(Type& x, Type& y) noexcept(noexcept(noexcept(fun1()),noexcept(fun2()));
//表示fun函数内会调用到的fun1函数和fun2函数都不会抛出异常,但不保证其他代码不会抛出异常

什么时候我们需要noexcept关键字?

使用noexcept表明函数或操作不会发生异常,会给编译器更大的优化空间。然而,并不是加上noexcept就能提高效率。
以下情形鼓励使用noexcept:

  • 移动构造函数(move constructor)
  • 移动分配函数(move assignment)
  • 析构函数(destructor)。这里提一句,在新版本的编译器中,析构函数是默认加上关键字noexcept的。下面代码可以检测编译器是否给析构函数加上关键字noexcept。
  • 叶子函数(Leaf Function)。叶子函数是指在函数内部不分配栈空间,也不调用其它函数,也不存储非易失性寄存器,也不处理异常。
    最后强调一句,在不是以上情况或者没把握的情况下,不要轻易使用noexception。

定义新的异常类型

只有C++标准库自带的异常类型肯定是不够用的,我们实际工作中还需要根据项目需求定义新的异常类型。
在C++中,异常类型都有个共同的基类(父类)——exception 类,我们定义新的异常时最好继承exception 类,exception 类在头文件exception中定义,你需要定义新的异常时需要#include它,如下:

#include <iostream>
#include <exception>
using namespace std;
class DivisionZeroException :public exception//基于exception类定义新的异常类型除以0导致的异常类
{
    
    
public:
    //这里重载了父类的虚函数what()
    //throw ()的括号里面没有东西,这表示这个函数不会抛出任何异常
    //const 是常量的关键字,常量在定义后无法被修改
    const char* what() const throw ()
    {
    
    
        return "出现了除以0的错误\n";
    }
};
double division(double a, double b)
{
    
    
    try
    {
    
    
        if (b == 0)
        {
    
    
            DivisionZeroException e;
            throw e;//throw关键字抛出错误类型DivisionZeroException
        }
    }
    catch (DivisionZeroException e)//捕获const char*类型的错误
    {
    
    
        cout<<e.what();//打印什么异常了
    }
    return (a / b);
}
int main()
{
    
    
    cout << division(1, 0);
}

输出结果:

出现了除数为0的错误
inf

异常类也可以被继承,所以在实际软件开发中,我们可以把异常分为几个大类,然后继承出许多小类来处理千变万化的异常类型。

猜你喜欢

转载自blog.csdn.net/lifesize/article/details/128656010