首页 > 编程语言 >关于C++中的异常概念理解

关于C++中的异常概念理解

时间:2024-10-09 19:21:36浏览次数:1  
标签:抛出 void float C++ 理解 catch 异常 throw

1. 基本概念

异常,即 exception,是C++中的基本概念之一,在某段程序发生无法继续正常执行的情况时,C++允许程序进行所谓抛出异常(有时也被称为吐出异常)的行为,这些被抛出的异常,会自动地从触发点开始向外传播,直到被捕获(有时也被称为吞下异常)或者程序终止。


2. 语法

2.1 抛出异常

下面用一个简单的例子,说明异常是如何发生的:

// 一个简易除法器,返回 a ÷ b 的结果
float divider(float a, float b)
{
    // 当除数b为0时,抛出异常!
    if(b == 0)
        throw "除数不可为零";
    
    return a/b;
}

int main(void)
{
    float a,b;

    // 从键盘获取被除数a和除数b,并输出结果
    while(1)
    {
        cin >> a >> b;
        cout << divider(a, b) << endl;
    }
    return 0;
}

在上述代码中,除法器函数 divider() 中出现了一个新的关键字:throw,它代表抛出异常操作,所谓的抛出异常,就是在程序中触发一个必须处理的情况,如果不处理,程序将会被强行终止。像上述代码,main() 函数没有对除法器 divider() 潜在会抛出的异常进行任何处理,因此当输入的除数 b 为0时,触发的异常将直接关闭程序。

注意
上述代码的流程是:
image
如果异常发生在更里层的函数调用中,那么异常会一直向外传播,直到遇到main函数,如果一直没有任何函数捕获异常,那么异常就会终结程序,这是异常的第一个特征:强制性。

假设有如下代码:

void f3()
{
    throw "抛出异常";
}
void f2()
{
    f3();
}
void f1()
{
    f2();
}

int main()
{
    f1();
    cout << "此语句不会被执行" << endl;
}

此时,异常发生在函数 f3() 处,然后一路传播到最终的调用者 main() 函数,这就是异常第二个特性:传播性。
image


2.2 捕获异常

如果程序不想被异常终止,那么就应该在异常的传播链上的某一环对其进行处理,即所谓的捕获异常,或吞下异常等操作,即 catch,例如上述调除法器的程序,可以让main函数捕获除数为0的异常:

/ 一个简易除法器,返回 a ÷ b 的结果
float divider(float a, float b)
{
    // 当除数b为0时,抛出一个字符串异常
    if(b == 0)
        throw "除数不可为零";
    
    return a/b;
}

int main(void)
{
    float a,b;
    float ans; // ans = a ÷ b

    // 从键盘获取被除数a和除数b,并输出结果
    while(1)
    {
        cin >> a >> b;

        // 试图执行 a÷b
        try{
            ans = divider(a, b);
        }
        catch(const char * &e)
        {
            // 发生了异常
            cout << e << endl;
            break;
        }

        // 输出运算结果
        cout << ans << endl;
    }
    return 0;
}

以下是执行效果:

gec@ubuntu:~$ ./a.out
88 0
除数不可为零
gec@ubuntu:~$ 

关键代码:

try{
    有潜在抛出异常可能的代码块
}
catch(类型1 e)
{
    处理异常类型1
}
catch(类型2 e)
{
    处理异常类型2
}
catch(...)
{
    处理其他异常类型
}

语法要点:

将有可能出现异常且需要捕获这些异常的代码,放入 try{} 代码块中。
try{} 代码块至少带一个 catch(){} 代码块,也可以带多个。
当异常发生时,多个 catch(){} 代码块中最多执行一个,用类型来区分它们。
当没有任何 catch(){} 的类型能匹配异常时,异常会被继续向main函数传播,如果没有任何函数捕获异常,程序将被异常终止。
可以用 catch(...){} 来匹配任意类型的异常。

2.3 异常的类型

捕获异常时,如果有多个 catch(){} 代码块,它们是以异常的类型来彼此区分的,也就是说如果被调函数有触发异常的多种不同的可能,且每种不同的异常情况需要不同的处理方式,那么要抛出不同的异常类型来区分它们,示例代码如下:

void f3(int n)
{
    // 当遇到三种不同的异常需要区别以待时
    // 抛出三种不同类型的异常
    if(n == 1)
        throw 666;
    if(n == 2)
        throw 1.23;
    if(n == 3)
        throw "abc";

    cout << "程序正常执行" << endl;
}
void f2(int n)
{
    try{
        f3(n);
    }
    catch(const int &e)
    {
        cout << "遇到了异常情况1:" << e << endl;
        cout << "处理完毕" << endl;
    }
}
void f1(int n)
{
    try{
        f2(n);
    }
    catch(const double &e)
    {
        cout << "遇到了异常情况2:" << e << endl;
        cout << "处理完毕" << endl;
    }
}

int main()
{
    int n;
    cin >> n;

    f1(n);
    cout << "遇到异常情况3时,程序无法处理,将被终止" << endl;
}

上面的程序展示了如何为不同的异常安排一个类型来加以区分,它们分别是:

返回一个 int 型数据时,代表发生了异常情况1
返回一个 double 型数据时,代表发生了异常情况2
返回一个 const char * 型数据时,代表发生了异常情况3
乍一看这设计有问题,因为如果异常情况较多(比如有10种不同的异常情况),那么就很难找到这么多不同的数据类型来对它们进行彼此区分了,而在原来C语言的框架下,不同的错误情况只需要对应不同的错误值即可,比如返回-1、-2、-3来区分不同的错误。

问题的奥秘在于,抛出的异常一般是以类对象的方式出现的,而类是可以继承的,这样一来,不仅可以让每一种不同的类型异常对应灵活定义的类对象,还能利用类的层次关系在顶层的异常父类中定义通用的报错渠道,极大地扩展了对错误信息处理的灵活性。事实上,C++已经提供了标准的异常类,来作为各种不同异常情况的顶层父类,供我们扩展使用,它们被定义在如下头文件之中:

#include <exception>

C++为每种不同的异常定义了标准异常类,既可以直接使用这些异常类,也可以根据实际所需扩展这些类:
image


一个使用上述标准异常类的简单例子:

#include <iostream>
#include <exception>

using namespace std;

float divider(float a, float b)
{
    // 抛出标准异常对象
    if(b == 0)
        throw invalid_argument("除数不可为零");
    
    return a/b;
}
int main(void)
{
    float a,b;
    float ans; // ans = a ÷ b

    while(1)
    {
        cin >> a >> b;

        try{
            ans = divider(a, b);
            cout << ans << endl;
        }
        // 捕获参数非法异常
        catch(invalid_argument &e)
        {
            cout << e.what() << endl;
        }
    }
    return 0;
}

关于运用类的继承机制,基于标准异常类扩展自定义异常类,可以等到学习了类与继承之后,再自行进行练习,目前可以简单将标准异常类看做就是一种数据类型即可。


3. 异常与返回错误值的区别

在刚接触C++异常机制的时候,很容易会跟C语言的函数出错返回相对比,掌握异常机制的语法规范很简单,但关键还要理解这么设计的初衷和用意。

首先要理解,当一个函数发生异常时,类似于在某个组织中的一个基层人员遇到了某个无法处理的问题,这个问题可能是基层人员能力、权限、格局所致,正确的做法是将该问题逐级上报,每一级有每一级的解决问题的层次,基层解决不了的,上一级有可能可以解决,或者基层不知道该怎么处理的,上一级有可能知道怎么处理,这种层层上报的工作须是自动发生的,如果某一个层级不对基层上报上来的异常做任何处理,那么该异常会自动地传播给上上级,且如果组织中没有任何对已出现的异常负责,也无人进行处理,那么为了避免埋雷,避免留下隐患,整个组织将会因为这个无人认领的异常而被强制终止运转。基于这样的逻辑设定,C++发明了异常机制,来更好地、更方便地维护程序的健壮和安全。
在函数内抛出异常,跟返回错误值,有如下区别:
(1)传播性
返回错误值,不具备自动传播性,某层级的代码无法处理基层传来的错误,依然需要做判定和处理,非常麻烦。
异常具备自动传播性,遇到与本层级无关、或不属于本层级管理的异常,不管它即可,它会自动向主函数传播。
(2)强制性
返回错误值,不具备强制性,容易给程序埋下难以察觉的隐患。异常具备强制性,不处理程序就会自动终止,将隐患尽早暴露出来。
(3)灵活性
返回错误值时,用不同的值来区分不同的错误,类型是固定的,即无法使用类与继承的方式代码重用。触发异常时,用不同的类型来区分不同的错误,天生就支持类与继承方式来代码重用,并且可以通过设定标准异常类来规范各种形式的错误信息,灵活性大大提高。


4. 异常与析构函数

5. 异常安全性问题

#include <iostream>
#include <string>
#include <exception>
#include <stdexcept>

using namespace std;

class err
{
public:
        err(string const &s){_msg = s;}
        string &msg(){return _msg;}

private:
        string _msg;
};

void f1(int a)
{
	// 抛出的异常,可以是普通变量,也可以是类对象
	// 抛出的异常对象与函数传参类似,被传送至catch语句
	// 但有以下两点不同:
	// ① 异常对象能持续追踪try...catch语句,直到遇到匹
	// 配的类型,或者程序退出
	// ② 异常对象在匹配的catch语句之后仍然存在不会释放
    if(a == 1)
        throw "abcd";

	else if(a == 2)
        throw 3.14;

	else if(a == 3)
    {
        throw (runtime_error("运行时错误"));
    }

	else if(a == 4)
    {
        throw (err("自定义错误"));
    }
	else
	{
		throw ('a');
	}

        return;
}

void f2(int n)
{
	try
	{
		f1(n);
	}
	catch(const char *e)
	{
		cerr << "错误:" << e << endl;
	}
	catch(double e)
	{
		cerr << "错误:" << e << endl;
	}
}

int main(void)
{
	int n;
        while(1)
	{
		cin >> n;

		try
		{
			f2(n);
		}
		catch(runtime_error &e)
		{
			string errstr;
			errstr.append(__FUNCTION__).append(":").append(e.what());
			cerr << errstr << endl;
		}
		catch(err &e)
		{
			string errstr;
			errstr.append(__FUNCTION__).append(":").append(e.msg());
			cerr << errstr << endl;
		}
		catch(...)
		{
			cerr << "其他未知情况" << endl;
		}
	}

        return 0;
}

#include <iostream>

using namespace std;

bool DBEnable = false;

void close()
{
    if(!DBEnable)
        throw 100;
    else
        cout << "数据库正常关闭了" << endl;
}

// 类对象用来管理一个数据库的连接
// 当对象被释放时,析构函数将自动断开数据库的连接
class A
{
    bool closed;

public:
    A(){closed = false;}

    void dbclose();
    ~A();
};

void A::dbclose()
{
    close();
    closed = true;
}

// 当对象被释放时,析构函数将自动断开数据库的连接
// 而析构过程中的 close() 可能会触发异常
A::~A()
{
    if(!closed)
    {
        // close会触发异常,不处理的话
        // 该异常将外溢到析构函数之外的其他代码,造成程序终止或者不明确行为
        // 因此使用try...catch捕获异常是类对象职责所在
        // 但如果仅提供此项功能,就意味着类对象的使用者无法干预close所带来的问题
        try
        {
            close();
        }
        catch(...)
        {
            cout << "close触发了异常" << endl;
        }
    }
}

int main()
{
    // 对象 a 出了代码块将被释放,届时触发析构函数
    // 由于 a 的使用者(即本main函数)不对数据库关闭可能触发的异常感兴趣
    // 因此以下代码可能会错失处理该异常的第一时机
    {
        A a;
    }

    A b;
    // 作为类对象的使用者,可以显式地使用 try...catch 来捕获异常
    // 一旦发现确有问题,便可酌情作出处理
    // 相反,如果将可能触发异常的 close 只封装在类析构函数中,由于析构函数
    // 被调用的时机的特殊性,将会使得类对象使用者则很难或无法针对异常做出反应
    try
    {
        b.dbclose();
    }
    catch(...)
    {
        cout << "数据库关闭异常,接下来做出某些操作... ..." << endl;
        DBEnable = true;
    }

    return 0;
}

标签:抛出,void,float,C++,理解,catch,异常,throw
From: https://www.cnblogs.com/hhail08/p/18454971

相关文章

  • C++名字空间
    基本概念名字空间本质上是自定义作用域,由于C++设计的初衷是开发大规模软件,大量的软件库必然会加剧全局符号(变量、函数)的冲突,因此名字空间最基本的作用就是给不同的库和模块拥有自己的独特的作用域,处于不同名字空间中的重名符号相安无事,互不冲突,以此来大大提高编程的便利性。1.1......
  • C++:自治我的世界2D.V0.0.4.5
    更新内容:增加挖掘进度,挖掘需要时间了,但还有BUG操作说明:A,D移动;W跳跃;上,下,右+上,右+下,左+上,左+下撸方块;M开关大地图#include<bits/stdc++.h>#include<windows.h>#defineKEY_DOWN(VK_NONAME)((GetAsyncKeyState(VK_NONAME)&0x8000)?1:0)usingnamespacestd;void......
  • C++消灭星星游戏编程【目录】
    欢迎来到zhooyu的专栏。主页:【zhooyu】专栏:【C++消灭星星游戏编程】特色:【保姆级教程,含每一课程源码】致力于用最简洁的语言,最简单的方式,最易懂的知识,带大家享受编程的快乐。消灭星星游戏编程演示效果消灭星星游戏编程演示效果本专栏内容:消灭星星的小游戏保姆......
  • python——celery异常consumer: Cannot connect to redis://127.0.0.1:6379/1: MISCON
    1.检查Redis日志:查看Redis的日志文件(通常位于/var/log/redis/redis-server.log或者根据你的配置文件中指定的位置),以获取有关错误原因的详细信息。2.检查磁盘空间:确保你的服务器有足够的磁盘空间。使用以下命令检查磁盘使用情况:bashdf-h如果磁盘空间不足,清理一些不必......
  • 实验一 C++
    实验任务1:task1.cpp:1#include<iostream>2#include<string>3#include<vector>4#include<algorithm>56usingnamespacestd;78//声明9//模板函数声明10template<typenameT>11voidoutput(constT&c);1213......
  • 实验1 现代C++编程初体验
    任务一#include<iostream>#include<string>#include<vector>#include<algorithm>usingnamespacestd;template<typenameT>voidoutput(constT&c);voidtest1();voidtest2();voidtest3();intmain(){cout<<&qu......
  • C++编译并运行后出现Process finished with exit code 139 (interrupted by signal 11
    问题描述:        代码运行意外终止,报错信息为Processfinishedwithexitcode139(interruptedbysignal11:SIGSEGV)CMakeList文件如下:cmake_minimum_required(VERSION3.26)project(SLAM)set(CMAKE_CXX_STANDARD17)set(CMAKE_CXX_STANDARD_REQUIRED......
  • 调用sdapi/v1/txt2img接口,报错“Couldn‘t load custom C++ ops”
    后端启动stable_diffusion的api接口nohuppythonlaunch.py --use-cpuall--skip-torch-cuda-test   --api--api-log  --listen--server-name192.168.1.204>/home/third_party_app/llm/stable-diffusion-webui/logs/all.log2>&1 &服务接口http://192.168......
  • (2024最新毕设合集)基于SpringBoot的乡村书屋小程序-31881|可做计算机毕业设计JAVA、PHP
    摘要随着信息技术的快速发展和互联网的广泛普及,数字化服务的需求不断增长,乡村书屋作为传统的文化服务机构也需要适应这一变革。本研究将使用Java开发技术,通过springboot作为框架,结合微信小程序,和MySQL作为数据存储的技术,开发一套功能齐备可移动的乡村书屋小程序,旨在提升乡......
  • 【C++】priority_queue的介绍和模拟实现
    【C++】priority_queue的介绍和模拟实现一.priority_queue的介绍1.priority_queue的基本介绍2.priority_queue的使用介绍二.priority_queue的模拟实现一.priority_queue的介绍1.priority_queue的基本介绍优先队列是一种容器适配器,根据严格的弱排序标准,它的......