首页 > 其他分享 >16.异常处理

16.异常处理

时间:2024-08-31 14:14:09浏览次数:2  
标签:16 处理 抛出 try catch include 异常 throw

16. 异常处理

16.1 引言

异常处理使您能够创建可以处理(即解决)异常的应用程序,并在发生无法或不应处理的异常时执行适当的清理。





16.2 C++ throw(抛出异常用法详解)

异常处理是许多现代编程语言中不可或缺的一部分,C++ 也不例外。通过使用 throw、try、和 catch 关键字,C++ 为程序员提供了强大的异常处理机制。

16.2.1 throw的基础用法

throw 是 C++ 异常处理机制中的一个关键字,用于在检测到异常情况时触发异常,语法格式如下:

throw 异常信息

异常信息可以是一个变量,也可以是一个表达式,表示要抛出的异常对象。

throw 1; // 抛出一个整数的异常
throw “abced”; // 抛出一个字符串的异常
throw int; // 错误,异常信息不能是类型,必须是具体的值

当在代码中检测到一个异常情况(如非法输入、资源耗尽等)时,可以使用 throw 关键字来抛出一个异常。这通常会中断当前函数的执行,并将控制权转交给最近的 catch 块。

下列是一个完整的示例:

#include <iostream>

void testFunction(int choice) {
    if (choice == 1) {
        throw 42;
    }
    else if (choice == 2) {
        throw "String type exception";
    }
    else {
        throw 1.23;
    }
}

int main() {
    try {
        testFunction(2);
    }
    catch (int e) {
        std::cout << "Caught an integer exception: " << e << std::endl;
    }
    catch (const char* e) {
        std::cout << "Caught a string exception: " << e << std::endl;
    }
    catch (const double& e) {
        std::cout << "Caught a standard exception: " << e << std::endl;
    }

    return 0;
}

16.2.2 C++异常类

throw 抛出的异常值,可以是基本数据类型,也可以是类类型。C++ 标准库中提供了一些常见的异常类,它们定义在<stdexcept>头文件中。

如下是从<stdexcept> 头文件中摘录的一部分异常类:

namespace std
{
    class logic_error; // logic_error: 表示程序逻辑错误的基类。
    class domain_error; // 当数学函数的输入参数不在函数的定义域内时抛出。
    class invalid_argument; // 当函数的一个参数无效时抛出。
    class length_error; //当操作导致容器超过其最大允许大小时抛出。
    class out_of_range; // 当数组或其他数据结构访问越界时抛出。
   
    class runtime_error;// 表示运行时错误的基类。
    class range_error; // 当数学计算的结果不可表示时抛出。
    class overflow_error; // 当算术运算导致溢出时抛出。
    class underflow_error; // 当算术运算导致下溢时抛出。
}
#include <iostream>
#include <stdexcept>

void divideNumbers(double num1, double num2) {
    if (num2 == 0) {
        throw std::invalid_argument("Denominator cannot be zero");
    }
    std::cout << "Result: " << num1 / num2 << std::endl;
}

int main() {
    try {
        divideNumbers(10, 2);
        divideNumbers(10, 0);
    } catch (const std::invalid_argument& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

    return 0;
}
image-20231022145640506

在上述代码中,divideNumbers() 函数接受两个浮点数,并尝试进行除法。如果除数(denominator)是 0,则通过 throw 关键字抛出一个 std::invalid_argument 异常。这个异常会被 main() 函数中的 catch 块捕获,并输出相应的错误消息。

16.3 C++异常处理(try和catch)

程序运行时常会碰到一些错误,例如除数为 0、年龄为负数、数组下标越界等,这些错误如果不能发现并加以处理,很可能会导致程序崩溃。C++ 异常处理机制就可以让我们捕获并处理这些错误,然后我们可以让程序沿着一条不会出错的路径继续执行,或者不得不结束程序,但在结束前可以做一些必要的工作,例如将内存中的数据写入文件、关闭打开的文件、释放分配的内存等。

运行时错误如果放任不管,系统就会执行默认的操作,终止程序运行,也就是我们常说的程序崩溃(Crash)。

一个发生运行时错误的程序:

#include <iostream>
#include <string>
using namespace std;

int main(){
    string str = "http://c.biancheng.net";
    char ch1 = str[100];  //下标越界,ch1为垃圾值
    cout<<ch1<<endl;
    char ch2 = str.at(100);  //下标越界,抛出异常
    cout<<ch2<<endl;
    return 0;
}

运行代码,在控制台输出 ch1 的值后程序崩溃。下面我们来分析一下原因。

at() 是 string 类的一个成员函数,它会根据下标来返回字符串的一个字符。与[ ]不同,at() 会检查下标是否越界,如果越界就抛出一个异常;而[ ]不做检查,不管下标是多少都会照常访问。

所谓抛出异常,就是报告一个运行时错误,程序员可以根据错误信息来进一步处理。

上面的代码中,下标 100 显然超出了字符串 str 的长度。由于第 6 行代码不会检查下标越界,虽然有逻辑错误,但是程序能够正常运行。而第 8 行代码则不同,at() 函数检测到下标越界会抛出一个异常,这个异常可以由程序员处理,但是我们在代码中并没有处理,所以系统只能执行默认的操作,也即终止程序执行。

16.3.1 捕获异常

我们可以借助 C++ 异常机制来捕获上面的异常,避免程序崩溃。捕获异常的语法为:

try{
    //可能抛出异常的语句
}
catch(exceptionType variable){
    //处理异常的语句
}

trycatch都是 C++ 中的关键字,后跟语句块,不能省略{ }。try 中包含可能会抛出异常的语句,一旦有异常抛出就会被后面的 catch 捕获。从 try 的意思可以看出,它只是“检测”语句块有没有异常,如果没有发生异常,它就“检测”不到。catch 是“抓住”的意思,用来捕获并处理 try 检测到的异常;如果 try 语句块没有检测到异常(没有异常抛出),那么就不会执行 catch 中的语句。

catch 关键字后面的exceptionType variable指明了当前 catch 可以处理的异常类型,以及具体的出错信息。稍后再对异常类型展开讲解,当务之急是演示一下 try-catch 的用法,先有一个整体上的认识。

#include <iostream>
#include <string>
#include <exception>
using namespace std;

int main(){
    string str = "http://c.biancheng.net";
  
    try{
        char ch1 = str[100];
        cout<<ch1<<endl;
    }catch(exception e){
        cout<<"[1]out of bound!"<<endl;
    }

    try{
        char ch2 = str.at(100);
        cout<<ch2<<endl;
    }catch(exception &e){  //exception类位于<exception>头文件中
        cout<<"[2]out of bound!"<<endl;
    }

    return 0;
}

可以看出,第一个 try 没有捕获到异常,输出了一个没有意义的字符(垃圾值)。因为[ ]不会检查下标越界,不会抛出异常,所以即使有错误,try 也检测不到。换句话说,发生异常时必须将异常明确地抛出,try 才能检测到;如果不抛出来,即使有异常 try 也检测不到。所谓抛出异常,就是明确地告诉程序发生了什么错误。

第二个 try 检测到了异常,并交给 catch 处理,执行 catch 中的语句。需要说明的是,异常一旦抛出,会立刻被 try 检测到,并且不会再执行异常点(异常发生位置)后面的语句。本例中抛出异常的位置是第 17 行的 at() 函数,它后面的 cout 语句就不会再被执行,所以看不到它的输出。

说得直接一点,检测到异常后程序的执行流会发生跳转,从异常点跳转到 catch 所在的位置,位于异常点之后的、并且在当前 try 块内的语句就都不会再执行了;即使 catch 语句成功地处理了错误,程序的执行流也不会再回退到异常点,所以这些语句永远都没有执行的机会了。本例中,第 18 行代码就是被跳过的代码。

执行完 catch 块所包含的代码后,程序会继续执行 catch 块后面的代码,就恢复了正常的执行流。

为了演示「不明确地抛出异常就检测不到异常」,大家不妨将第 10 行代码改为char ch1 = str[100000000];,访问第 100 个字符可能不会发生异常,但是访问第 1 亿个字符肯定会发生异常了,这个异常就是内存访问错误。运行更改后的程序,会发现第 10 行代码产生了异常,导致程序崩溃了,这说明 try-catch 并没有捕获到这个异常。

16.3.2 发生异常的位置

异常可以发生在当前的 try 块中,也可以发生在 try 块所调用的某个函数中,或者是所调用的函数又调用了另外的一个函数,这个另外的函数中发生了异常。这些异常,都可以被 try 检测到。

下述代码为try块中直接发生的异常:

#include <iostream>
#include <string>
#include <exception>
using namespace std;

int main(){
    try{
        throw "Unknown Exception";  //抛出异常
        cout<<"This statement will not be executed."<<endl;
    }catch(const char* &e){
        cout<<e<<endl;
    }

    return 0;
}

下述代码为try块中调用的某个函数发生了异常:

#include <iostream>
#include <string>
#include <exception>
using namespace std;

void func(){
    throw "Unknown Exception";  //抛出异常
    cout<<"[1]This statement will not be executed."<<endl;
}

int main(){
    try{
        func();
        cout<<"[2]This statement will not be executed."<<endl;
    }catch(const char* &e){
        cout<<e<<endl;
    }

    return 0;
}

func() 在 try 块中被调用,它抛出的异常会被 try 检测到,进而被 catch 捕获。从运行结果可以看出,func() 中的 cout 和 try 中的 cout 都没有被执行。

下述代码为 try 块中调用了某个函数,该函数又调用了另外的一个函数,这个另外的函数抛出了异常:

#include <iostream>
#include <string>
#include <exception>
using namespace std;

void func_inner(){
    throw "Unknown Exception";  //抛出异常
    cout<<"[1]This statement will not be executed."<<endl;
}

void func_outer(){
    func_inner();
    cout<<"[2]This statement will not be executed."<<endl;
}

int main(){
    try{
        func_outer();
        cout<<"[3]This statement will not be executed."<<endl;
    }catch(const char* &e){
        cout<<e<<endl;
    }

    return 0;
}

发生异常后,程序的执行流会沿着函数的调用链往前回退,直到遇见 try 才停止。在这个回退过程中,调用链中剩下的代码(所有函数中未被执行的代码)都会被跳过,没有执行的机会了。

exceptionType是异常类型,它指明了当前的 catch 可以处理什么类型的异常;variable是一个变量,用来接收异常信息。当程序抛出异常时,会创建一份数据,这份数据包含了错误信息,程序员可以根据这些信息来判断到底出了什么问题,接下来怎么处理。

异常既然是一份数据,那么就应该有数据类型。C++规定,异常类型可以是 int、char、float、bool 等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型。C++ 语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的异常。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象。

exceptionType variable和函数的形参非常类似,当异常发生后,会将异常数据传递给 variable 这个变量,这和函数传参的过程类似。当然,只有跟 exceptionType 类型匹配的异常数据才会被传递给 variable,否则 catch 不会接收这份异常数据,也不会执行 catch 块中的语句。换句话说,catch 不会处理当前的异常。

我们可以将 catch 看做一个没有返回值的函数,当异常发生后 catch 会被调用,并且会接收实参(异常数据)。

但是 catch 和真正的函数调用又有区别:

  • 真正的函数调用,形参和实参的类型必须要匹配,或者可以自动转换,否则在编译阶段就报错了。
  • 而对于 catch,异常是在运行阶段产生的,它可以是任何类型,没法提前预测,所以不能在编译阶段判断类型是否正确,只能等到程序运行后,真的抛出异常了,再将异常类型和 catch 能处理的类型进行匹配,匹配成功的话就“调用”当前的 catch,否则就忽略当前的 catch。

总起来说,catch 和真正的函数调用相比,多了一个「在运行阶段将实参和形参匹配」的过程。另外需要注意的是,如果不希望 catch 处理异常数据,也可以将 variable 省略掉,也即写作:

try{
    // 可能抛出异常的语句
}catch(exceptionType){
    // 处理异常的语句
}

这样只会将异常类型和 catch 所能处理的类型进行匹配,不会传递异常数据了。

16.3.3 多级catch

try{
    //可能抛出异常的语句
}catch (exception_type_1 e){
    //处理异常的语句
}catch (exception_type_2 e){
    //处理异常的语句
}
//其他的catch
catch (exception_type_n e){
    //处理异常的语句
}

当异常发生时,程序会按照从上到下的顺序,将异常类型和 catch 所能接收的类型逐个匹配。一旦找到类型匹配的 catch 就停止检索,并将异常交给当前的 catch 处理(其他的 catch 不会被执行)。如果最终也没有找到匹配的 catch,就只能交给系统处理,终止程序的运行。

#include <iostream>
#include <string>
using namespace std;

class Base{ };
class Derived: public Base{ };

int main(){
    try{
        throw Derived();  //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象
        cout<<"This statement will not be executed."<<endl;
    }catch(int){
        cout<<"Exception type: int"<<endl;
    }catch(char *){
        cout<<"Exception type: cahr *"<<endl;
    }catch(Base){  //匹配成功(向上转型)
        cout<<"Exception type: Base"<<endl;
    }catch(Derived){
        cout<<"Exception type: Derived"<<endl;
    }

    return 0;
}

在 catch 中,我们只给出了异常类型,没有给出接收异常信息的变量。

本例中,我们定义了一个基类 Base,又从 Base 派生类出了 Derived。抛出异常时,我们创建了一个 Derived 类的匿名对象,也就是说,异常的类型是 Derived。

我们期望的是,异常被catch(Derived)​捕获,但是从输出结果可以看出,异常提前被catch(Base)​捕获了,这说明 catch 在匹配异常类型时发生了向上转型(Upcasting)。

16.3.4 catch 在匹配过程中的类型转换

C/C++ 中存在多种多样的类型转换,以普通函数(非模板函数)为例,发生函数调用时,如果实参和形参的类型不是严格匹配,那么会将实参的类型进行适当的转换,以适应形参的类型,这些转换包括:

  • 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
  • 向上转型:也就是派生类向基类的转换,请猛击《C++向上转型(将派生类赋值给基类)》了解详情。
  • const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
  • 数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
  • 用户自定的类型转换。

catch 在匹配异常类型的过程中,也会进行类型转换,但是这种转换受到了更多的限制,仅能进行「向上转型」、「const 转换」和「数组或函数指针转换」,其他的都不能应用于 catch。

向上转型在上面的例子中已经发生了,下面的例子演示了 const 转换以及数组和指针的转换:

#include <iostream>
using namespace std;

int main(){
    int nums[] = {1, 2, 3};
    try{
        throw nums;
        cout<<"This statement will not be executed."<<endl;
    }catch(const int *){
        cout<<"Exception type: const int *"<<endl;
    }

    return 0;
}

运行结果:
Exception type: const int *

nums 本来的类型是int [3],但是 catch 中没有严格匹配的类型,所以先转换为int *,再转换为const int *

数组也是一种类型,数组并不等价于指针,这点已在《数组和指针绝不等价,数组是另外一种类型》和《数组到底在什么时候会转换为指针》中进行了详细讲解。

16.4 C++ exception类:C++标准异常的基类

C++语言本身或者标准库抛出的异常都是 exception 的子类,称为标准异常(Standard Exception)。你可以通过下面的语句来捕获所有的标准异常:

try{
    //可能抛出异常的语句
}catch(exception &e){
    //处理异常的语句
}

之所以使用引用,是为了提高效率。如果不使用引用,就要经历一次对象拷贝(要调用拷贝构造函数)的过程。

exception 类位于<exception> 头文件中,它被声明为:

class exception{
public:
    exception () throw();  //构造函数
    exception (const exception&) throw();  //拷贝构造函数
    exception& operator= (const exception&) throw();  //运算符重载
    virtual ~exception() throw();  //虚析构函数
    virtual const char* what() const throw();  //虚函数
}

这里需要说明的是 what() 函数。what() 函数返回一个能识别异常的字符串,正如它的名字“what”一样,可以粗略地告诉你这是什么异常。不过C++标准并没有规定这个字符串的格式,各个编译器的实现也不同,所以 what() 的返回值仅供参考。

下图展示了 exception 类的继承层次:

先来看一下 exception 类的直接派生类:

logic_error 的派生类:

runtime_error 的派生类:

16.4 自定义异常类

标签:16,处理,抛出,try,catch,include,异常,throw
From: https://www.cnblogs.com/yyyylllll/p/18390228

相关文章

  • 13.文件处理
    13.文件处理13.1引言内存中数据的存储是暂时的。文件用于数据的持久化--数据的永久保留。计算机将文件存储在二次存储设备上,如硬盘、CD、DVD、闪存和磁带。13.2文件和流(FilesandStreams)C++将文件简单的看作一系列字节。每个文件以end-of-file标记或在操作系统维护的......
  • elasticsearchClient查询集合数据,过大的处理情况
    如果你不知道实际的记录数,并且想查询某个条件下的所有记录,可以使用以下两种方法来实现:滚动(scroll)查询或search_after。这两种方法都适用于返回大量数据的场景。方法一:滚动(Scroll)查询Scroll查询是一种有效获取大量数据的方式,特别是当你不知道要查询的记录数量时。Scroll查询......
  • autodock vina后处理分析
    拆分对接结果vina_split--inputresult.pdbqt--ligandcomplex/lig使用mv命令批量修改文件名,把01-09修改成1-9,便于批量处理foriin`seq19`;do>mv"lig0${i}.pdbqt""lig${i}.pdbqt">done使用Openbabel把pdbqt转成pdbforiin`seq120`;do>obabel......
  • 基于Python的机器学习系列(16):扩展 - AdaBoost
    简介        在本篇中,我们将扩展之前的AdaBoost算法实现,深入探索其细节并进行一些修改。我们将重点修复代码中的潜在问题,并对AdaBoost的实现进行一些调整,以提高其准确性和可用性。1.修复Alpha计算中的问题        在AdaBoost中,如果分类器的错误率e为0,则......
  • (2024最新毕设合集)基于SpringBoot的校园设备维修管理系统-16364|可做计算机毕业设计JAV
    基于Springboot的校园设备维修管理系统的设计与实现摘 要基于Springboot的校园设备维修管理系统的设计与实现是一个结合了网络技术和信息管理的项目。该系统能满足校园报修管理的实际需求,通过网络进行信息管理,使得设备维修更加及时有效。本设计主要实现集人性化、高效率、......
  • 探索音频处理中的频率分辨率:原理、影响与应用
    目录什么是频率分辨率?频率分辨率对音频处理的影响频率分辨率的实际应用与选择结论在音频信号处理领域,频率分辨率是一个至关重要的概念,它直接影响信号的分析和处理结果。无论是在语音识别、噪声抑制、音乐信号处理,还是在更多复杂的音频处理应用中,理解和选择适当的频率......
  • 编译执行和解释执行是两种不同的程序执行方式,它们在处理源代码时有着本质的区别:
    1.编译执行(CompiledExecution)定义:编译执行是指将源代码一次性转换成机器可执行的二进制代码的过程。这种转换通常是由编译器完成的。过程:编译阶段:源代码被编译器转化为目标代码(通常是机器码)。链接阶段:目标代码被链接器链接成可执行文件。优点:运行速度快,因为执行的是机器码。安......
  • 亦菲喊你来学机器学习(16) --K-means聚类算法
    文章目录K-means基本步骤优缺点构建模型总结K-meansK-means算法是一种广泛使用的聚类算法,旨在将数据集划分为K个簇,使得每个簇内的数据点尽可能相似,而不同簇之间的数据点尽可能不同。这个算法通过迭代的方式实现,每次迭代都会更新簇的中心(即簇内所有点的均值),然后将......
  • 代码随想录day46 || 647 回文子串, 516 最长回文子序列
    647回文字串funccountSubstrings(sstring)int{ //动规五部曲 //dp[i][j]表示s[i:j+1]区间是否是一个回文 //ifs[i]==s[j]{ifi-j<=1||dp[i+1][j-1]==true{dp[i][j]==true}} //初始化为false //从下往上,从左往右 //print varcountint var......
  • [Python办公]一文入门图论Graphs,轻松处理最短路径等问题!
            [Python办公]一文入门图论Graphs,轻松处理最短路径等问题!        图论是研究图这种数学结构的性质和应用的学科。图(Graphs)由节点(或顶点)和连接这些节点的边组成,它是一种强大的数据结构,广泛应用于各种领域。以下举例用最短距离来入门图论。入门问题: ......