【C++】异常 -- 详解

一、C 语言传统的处理错误的方式

传统的错误处理机制:

  1. 终止程序,如 assert,缺陷:用户难以接受。如发生内存错误,除 0 错误时就会终止程序。
  2. 返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到 errno 中,表示错误实际中 C 语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。

二、C++ 异常概念

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者来处理这个错误。

  • throw当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch在想要处理问题的地方,通过异常处理程序捕获异常 catch 关键字用于捕获异常,可以有多个 catch 进行捕获
  • trytry 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块
如果有一个块抛出一个异常,捕获异常的方法会使用 try catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码
使用 try/catch 语句的语法如下所示:
try
{
    // 保护的标识代码
}catch( ExceptionName e1 )
{
    // catch 块
}catch( ExceptionName e2 )
{
    // catch 块
}catch( ExceptionName eN )
{
    // catch 块
}

三、异常的使用

1、异常的抛出和捕获

抛异常,异常必须被捕获 ,若没有被捕获就会报错。该图程序中只有抛异常,没有捕获异常存在,所以当 b = 0 时,程序直接报错。


(1)异常的抛出和匹配原则
a. 异常是通过抛出对象而引发的,对象的类型决定了应该激活哪个 catch 的处理代码。
double Division(int a, int b)
{
    // 当b == 0时抛出异常
    if (b == 0)
        throw "Division by zero condition!";//抛异常
    else
        return ((double)a / (double)b);
}
void Func()
{
    int len, time;
    cin >> len >> time;
    cout << Division(len, time) << endl;
}
int main()
{
    try
    {
        Func();
    }
    catch (const char* str)
    {
        cout << str << endl;
    }
    return 0;
}

  • catch 的时候,需要跟 throw 抛出对象的类型进行匹配。上面的 throw 传过来的是字符串,所以 catch 用 const char* 接收。
  • 由于有捕获异常,所以当再次 b = 0 时,就不会报错了,显示的详细信息为 Division by zero condition!
  • try 和 catch 两者是配对的, catch 只能捕获 try 里面的抛的异常。比如,在 main 函数中的 catch 捕获异常只能捕获 Func 函数中抛的异常。

b. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。

抛出异常位置最近的验证:

double Division(int a, int b)
{
    // 当b == 0时抛出异常
    if (b == 0)
        throw "Division by zero condition!";//抛异常
    else
        return ((double)a / (double)b);
}
void Func()
{
    int len, time;
    cin >> len >> time;
    try
    {
        cout << Division(len, time) << endl;
    }
    catch (const char* str)
    {
        cout << str << endl;
    }        
    cout << "void func()" << endl;
}
int main()
{
    try
    {
        Func();
    }
    catch (const char* str)
    {
        cout << str << endl;
    }
    return 0;
}

若在 Func 函数处添加捕获,并且类型与对象类型匹配,则当 b = 0 时,由于 Func 函数处更近,所以在 Func 函数处捕捉异常,而不在 main 函数中捕获异常。

对象类型匹配的验证: 

此时 Func 函数中的捕获异常与对象类型不匹配,当再次输入 b = 0 时,在 main 函数处捕获异常:

double Division(int a, int b)
{
    // 当b == 0时抛出异常
    if (b == 0)
        throw "Division by zero condition!";//抛异常
    else
        return ((double)a / (double)b);
}
void Func()
{
    int len, time;
    cin >> len >> time;
    try
    {
        cout << Division(len, time) << endl;
    }
    catch (char str)
    {
        cout << str << endl;
    }        
    cout << "void func()" << endl;
}
int main()
{
    try
    {
        Func();
    }
    catch (char str)
    {
        cout << str << endl;
    }
    return 0;
}

若 Func 函数和 main 函数的捕获异常与对象类型都不匹配 ,则程序会报错。


c. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被 catch 以后销毁。(这里的处理类似于函数的传值返回)

如果错误信息只是一个字符串,有些过于简单,所以设置一个类,内部包含错误码和错误描述。由于成员变量是私有的,在类外可能拿不到,所以设置两个函数,通过函数返回值的方式取到错误码和错误描述。由于对象类型为 const Exception,所以想要使用对象取到这两个函数 ,就需要在外部加上 const 修饰。

对比上面,将字符串替换成了对象,对象含有错误码和错误描述两部分。通过抛异常的方式将对象 传递给 catch 的捕获,在将对象的错误码和错误信息打印出来。

抛异常时,并不是把 e1 直接传给 e。因为 e1 是一个局部对象,出了作用域就销毁了,会产生一个临时对象,将 e1 对象的错误码和错误描述拷贝给临时对象,再通过临时对象传给对象 e,在 catch 结束后,临时对象销毁。


d. catch(...) 可以捕获任意类型的异常,问题是不知道异常错误是什么。
double Division(int a, int b)
{
    // 当b == 0时抛出异常
    if (b == 0)
        throw "Division by zero condition!";//抛异常
    else
        return ((double)a / (double)b);
}
void Func()
{
    int len, time;
    cin >> len >> time;
    try
    {
        cout << Division(len, time) << endl;
    }
    catch (char str)
    {
        cout << str << endl;
    }        
    cout << "void func()" << endl;
}
int main()
{
    try
    {
        Func();
    }
    catch (char str)
    {
        cout << str << endl;
    }
    catch(...)
    {
        cout << "未知异常" << endl;
    }
    return 0;
}

此时由于两个捕获异常都与对象类型不匹配,所以进入 catch(…) 中,使用 catch(…),若有匹配的就用匹配的,若没有匹配的,就使用 catch(…),对任意类型异常进行捕获防止一些异常没有捕获(没有对象类型匹配),导致程序终止

实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用,后面会详细讲解。


(2)在函数调用链中异常栈展开匹配原则
  1. 首先检查 throw 本身是否在 try 块内部,如果是再查找匹配的 catch 语句
  2. 如果有匹配的,则调到 catch 的地方进行处理。
  3. 没有匹配的 catch 则退出当前函数栈,继续在调用函数的栈中进行查找匹配的 catch。
  4. 如果到达 main 函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的 catch 子句的过程称为栈展开。所以实际上我们最后都要加一个 catch(...) 捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
  5. 找到匹配的 catch 子句并处理以后,会继续沿着 catch 子句后面继续执行。


2、异常的重新抛出

有可能单个的 catch 不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch 则可以通过重新抛出将异常传递给更上层的函数进行处理。
double Division(int a, int b)
{
    // 当b == 0时抛出异常
    if (b == 0)
    {
        throw "Division by zero condition!";
    }
    return (double)a / (double)b;
}
void Func()
{
    int* array = new int[10];
    int len, time;
    cin >> len >> time;
        cout << Division(len, time) << endl;


        cout << "delete []" << array << endl;
        delete[] array;
        throw;

    // ...
    cout << "delete []" << array << endl;
    delete[] array;
}
int main()
{
    try
    {
        Func();
    }
    catch (const char* errmsg)
    {
        cout << errmsg << endl;
    }
    return 0;
}

若抛异常,则会导致内存泄漏(没有使用 delete 释放)。

若要求在 main 函数将异常处理, 所以可以采用异常的重新抛出。当在 Func 函数中的 catch 要捕获异常时,再将异常抛出,使 main 函数中进行捕获异常 。


3、异常安全

  • 构造函数完成对象的构造和初始化最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
  • 析构函数主要完成资源的清理最好不要在析构函数内抛出异常,否则可能导致资源泄漏(存泄漏、句柄未关闭等)。
  • C++ 中异常经常会导致资源泄漏的问题,比如在 new 和 delete 中抛出了异常,导致内存泄漏,在 lock 和 unlock 之间抛出了异常导致死锁,C++ 经常使用 RAII 来解决以上问题,关于 RAII 将在后面的智能指针部分进行详细讲解。

4、异常规范

exception - C++ Reference (cplusplus.com)

  1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。可以在函数的后面接 throw(类型),列出这个函数可能抛掷的所有异常类型。
  2. C++98 中,函数的后面接 throw(),表示函数不抛异常
  3. 无异常接口声明,则此函数可以抛掷任何类型的异常

声明可以不给,但是加上会让人更容易理解。这个函数异常声明并不是强制的,并且比较繁琐,就导致很多人不遵循这个规范。

在 C++11 中,若一个函数明确不抛异常的话,就加 noexcept,可能会抛异常,就什么都不加。


四、自定义异常体系

在实际使用中,很多公司都会自定义自己的异常体系来进行规范的异常管理。因为在一个项目中,如果大家随意抛异常,那么外层的调用者基本就没办法玩了,所以在实际中都会定义一套继承的规范体系。这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了。

// 基类
// 异常
class Exception
{
public:
    Exception(const string& errmsg, int id)
        :_errmsg(errmsg)
        ,_id(id)
    {}

    virtual string what() const
    {
        return _errmsg;
    }
protected:
    string _errmsg; // 错误信息
    int _id; // 错误码
};

// 派生类
// 数据库异常
class SqlException : public Exception
{
public:
    SqlException(const string& errmsg, int id, const string& sql)
        :Exception(errmsg, id)
        , _sql(sql)
    {}

    virtual string what() const
    {
        string str = "SqlException:";
        str += _errmsg;
        str += "->";
        str += _sql;

        return str;
    }
private:
    const string _sql;
};

// 缓存异常
class CacheException : public Exception
{
public:
    CacheException(const string& errmsg, int id)
        :Exception(errmsg, id)
    {}

    virtual string what() const
    {
        string str = "CacheException:";
        str += _errmsg;
        return str;
    }
};

// 网络异常
class HttpServerException : public Exception
{
public:
    HttpServerException(const string& errmsg, int id, const string& type)
        :Exception(errmsg, id)
        , _type(type)
    {}

    virtual string what() const
    {
        string str = "HttpServerException:";
        str += _type;
        str += ":";
        str += _errmsg;
        return str;
    }
private:
    const string _type;
};

void SQLMgr()
{
    srand(time(0));
    if (rand() % 7 == 0)
    {
        throw SqlException("权限不足", 100, "select * from name = '张三'");
    }

    //throw "xxxxxx";
}

void CacheMgr()
{
    srand(time(0));
    if (rand() % 5 == 0)
    {
        throw CacheException("权限不足", 100);
    }
    else if (rand() % 6 == 0)
    {
        throw CacheException("数据不存在", 101);
    }

    SQLMgr();
}

void HttpServer()
{
    // ...
    srand(time(0));
    if (rand() % 3 == 0)
    {
        throw HttpServerException("请求资源不存在", 100, "get");
    }
    else if (rand() % 4 == 0)
    {
        throw HttpServerException("权限不足", 101, "post");
    }

    CacheMgr();
}

int main()
{
    while (1)
    {
        this_thread::sleep_for(chrono::seconds(1));

        try{
            HttpServer();
        }
        catch (const Exception& e) // 这里捕获父类对象就可以
        {
            // 多态
            cout << e.what() << endl;
        }
        catch (...)
        {
            cout << "Unkown Exception" << endl;
        }
    }

    return 0;
}

五、C++ 标准库的异常体系

C++ 提供了一系列标准的异常,定义在中,我们可以在程序中使用这些标准的异常。它们是以父
子类层次结构组织起来的,如下所示:

实际上,我们可以去继承 exception 类来实现自己的异常类。但是在实际中很多公司会像上面一样,自己去定义一套异常继承体系,因为 C++ 标准库设计的不够好用。
int main()
{
    try{
        vector<int> v(10, 5);
        // 这里如果系统内存不够也会抛异常
        v.reserve(1000000000);

        // 这里越界会抛异常
        v.at(10) = 100; 
    } catch (const exception& e)
    {
        // 这里捕获父类对象就可以
        cout << e.what() << endl;
    } catch (...)
    {
        cout << "Unkown Exception" << endl;
    }

    return 0;
}

六、异常的优缺点

1、C++ 异常的优点

(1)异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的 bug

(2)返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误,具体看下面的详细解释。

  1. 下面这段伪代码我们可以看到 ConnnectSql 中出错了,先返回给 ServerStart,ServerStart 再返回给 main 函数,main 函数再针对问题处理具体的错误。
  2. 如果是异常体系,不管是 ConnnectSql 还是 ServerStart 及调用函数出错,都不用检查,因为抛出的异常异常会直接跳到 main 函数中 catch 捕获的地方,main 函数直接处理错误。
int ConnnectSql()
{
    // 用户名密码错误
    if (...)
        return 1;
 
    // 权限不足
    if (...)
        return 2;
}
  
int ServerStart() {
    if (int ret = ConnnectSql() < 0)
        return ret;
     int fd = socket() 
     if(fd < 0)
        return errno;
}
  
int main()
{
    if(ServerStart() < 0)
        // ...
  
    return 0;
}
(3)很多的第三方库都包含异常,比如 boost、gtest、gmock 等等常用的库,那么我们使用它们也需要使用异常。
(4)部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如,T& operator 这样的函数,如果 pos 越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。

2、C++ 异常的缺点 

(1)异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时比较困难
(2)异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
(3)C++ 没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用 RAII 来处理资源的管理问题。学习成本较高。
(4)C++ 标准库的异常体系定义的不好,导致大家各自定义各自的异常体系,非常的混乱。
(5)异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常
规范有两点:
  1. 抛出异常类型都继承自一个基类。
  2. 函数是否抛异常、抛什么异常,都使用 func() throw(); 的方式规范化。

【总结】

异常总体而言, 利大于弊 ,所以在工程中还是鼓励使用异常的。另外 OO 的语言基本都是用异常处理错误,这也可以看出这是大势所趋。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/236403.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vulnhub靶机hacksudo FOG

下载地址&#xff1a;hacksudo: FOG ~ VulnHub 主机发现 目标148 端口扫描 IP过多整理一下 扫描服务 漏洞扫描 去80看看 经典凯撒&#xff0c;后面还是一个github 好好好&#xff0c;mp4 接下来目录爆破 一个一个去看 失败了换一个 少模块&#xff0c;有点麻烦&#xff0c;直接…

C++ //习题2.3 写出以下程序运行结果。请先阅读程序,分析应输出的结果,然后上机验证。

C程序设计 &#xff08;第三版&#xff09; 谭浩强 习题2.3 习题2.3 写出以下程序运行结果。请先阅读程序&#xff0c;分析应输出的结果&#xff0c;然后上机验证。 #include <iostream> using namespace std;int main(){char c1 a, c2 b, c3 c, c4 \101, c5 \116…

scala表达式

1.8 表达式&#xff08;重点&#xff09; # 语句(statement)&#xff1a;一段可执行的代码# 表达式(expression)&#xff1a;一段可以被求值的代码&#xff0c;在Scala中一切都是表达式 - 表达式一般是一个语句块&#xff0c;可包含一条或者多条语句&#xff0c;多条语句使用“…

Android 13 Settings蓝牙列表卡顿问题排查及优化过程

一.背景 此问题是蓝牙列表界面息屏后再点击亮屏蓝牙界面卡住,划不动也不能返回,在人多的时候(附近开启的蓝牙设备过多的时候)会卡住大概四五秒才能滑动. 优化前效果见资源: 二.查找耗时点 根据Android Studio的Profiler工具进行排查,查找主线程时间线比较长的方法,如下:…

java打包到docker,以及idea远程调试

这里主要介绍 dockerfile的打包方式 一、打包jar包到容器 1. 在要打包的项目中创建dockerfile&#xff0c;dockerfile与项目的pom.xml是同级 2. 编辑dockerfile文件 FROM openjdk:8 VOLUME ["/data/untitled"] COPY target/untitled-1.0.jar "/app.jar"…

Proteus仿真--射击小游戏仿真设计

本文介绍基于proteus射击小游戏仿真设计&#xff08;完整仿真源文件及代码见文末链接&#xff09; 仿真图如下 K1-K4为4个按键&#xff0c;用于上移、下移、确认等&#xff0c;模拟单机游戏 仿真运行视频 Proteus仿真--射击小游戏仿真设计 附完整Proteus仿真资料代码资料 …

手动搭建Magento电商网站

Magento是一个用PHP编写的开源电子商务平台。它的架构是可扩展和模块化的&#xff0c;使其成为构建大中型网站的绝佳选择。Magento支持从5.6到7.1的PHP版本&#xff0c;并利用MySQL数据库进行数据存储。本文将为您介绍如何在CentOS 7操作系统的ECS实例上搭建Magento电商网站。 …

厘米级高精度定位系统为什么更倾向于UWB技术?

超宽带&#xff08;Ultra Wide-Band&#xff0c;UWB&#xff09;是一种新型的无线通信技术&#xff0c;根据通信委员会的规范&#xff0c;UWB的工作频带为3.1~10.6GHz&#xff0c;系统-10dB带宽与系统中心频率之比大于20%或系统带宽至少为500MHz。 UWB信号的发生可通过发射时间…

11 月 NFT 动态:交易量增长,Layer 2 格局剧变

作者&#xff1a;stellafootprint.network 11 月份&#xff0c;随着比特币和以太坊价格的提升&#xff0c;加密货币市场活动频繁&#xff0c;市场呈现进一步复苏的迹象。NFT 领域中&#xff0c;Blur 的交易量飙升&#xff0c;进一步巩固地位&#xff1b;Blast 的亮相&#xff…

基于人工智能技术的《量化投资AI系统》集群架构设计与实现

乔总&#xff1a;您好&#xff01; 前些日子你我的共同朋友潘总&#xff0c;推荐您来聊聊将ChatGPT应用于量化投资的合作。在与您及您的团队进行了超过2个多小时的沟通后&#xff0c;恕我直言&#xff0c;不客气地说&#xff0c;感觉您的团队对人工智能技术几乎是空白。为了让…

【基于大数据的人肥胖程度预测分析与可控策略】

基于大数据的人肥胖程度预测分析与可控策略 前言数据获取与清洗数据挖掘与分类建模1. K-means聚类2. 层次聚类3. DBSCAN4. 分类建模 数据可视化模型肥胖程度预测分析与可控策略结语 前言 随着现代生活方式的改变&#xff0c;肥胖问题逐渐成为全球性的健康挑战。为了更好地理解…

Nginx rewrite 参数

目录 常用的Nginx 正则表达式 rewrite 和 location的区别 location location 大致分三类&#xff1a; location 常用的匹配规则&#xff1a; location 优先级&#xff1a; rewrite rewrite跳转实现 rewrite 执行顺序如下 语法格式 flag标记说明 rewrite实际操作 基…

hdlbits系列verilog解答(Ringer)-55

文章目录 一、问题描述二、verilog源码三、仿真结果 一、问题描述 本次我们设计一个电路以实现对手机铃声和振动的控制。当工作在振动模式时&#xff0c;开启振动&#xff0c;否则开启铃声。 尝试只使用assign语句&#xff0c;测试一下你是否能将描述转化成数字逻辑电路。 二…

微信商家收款码扣多少手续费

很多人想申请低手续费率的收款码不知从何下手&#xff0c;在参考了大量博客教学之后&#xff0c;终于搞懂了详细流程以及注意事项。在此记录一下。我申请的是一个只需要0.2%费率的微信收款码&#xff0c;申请时间是2022年2月12日。申请之前只需要准备营业执照和法人身份z&#…

成都工业学院Web技术基础(WEB)实验八:BOM、DOM基本操作

写在前面 1、基于2022级计算机大类实验指导书 2、代码仅提供参考&#xff0c;前端变化比较大&#xff0c;按照要求&#xff0c;只能做到像&#xff0c;不能做到一模一样 3、图片和文字仅为示例&#xff0c;需要自行替换 4、如果代码不满足你的要求&#xff0c;请寻求其他的…

Java集合进阶

目录 集合体系结构 Collection集合 List集合 ArrayList集合 LinkedList集合 集合体系结构 注意:有序:存进去的数组和取出来时一样 而不是大小的那种有序 Collection集合 单列集合顶层接口Collection import java.util.ArrayList; import java.util.Collection;public cl…

SAP UI5 walkthrough step1 hello word

这里我用的VS Studio 来进行本地化学习 关于SAP UI5是啥&#xff0c;我就不再赘述了&#xff0c;另外还有VS Studio 的安装&#xff0c;请提前做好准备 下面我们直接进入正文 1.首先在你的本地新建一个文件夹&#xff0c;此处我命名为&#xff1a;walkthrough 2.在VS中打开…

中文语音标注工具FunASR(语音识别)

全称 A Fundamental End-to-End Speech Recognition Toolkit&#xff08;一个语音识别工具&#xff09; 可能大家用过whisper&#xff08;openAi&#xff09;&#xff0c;它【标注英语的确很完美】&#xff0c;【但中文会出现标注错误】或搞了个没说的词替换上去&#xff0c;所…

Pico VR眼镜(XR) Unity开发环境部署及打包教程

创建项目 我这里选择的是URP项目。URP对移动端性能比较友好&#xff0c;另外VR平台也不支持HDRP渲染管线。 然后进入unity工具栏->File -> Build Settings 点击 Android后&#xff0c;点就Switch Platform将项目转为Android项目 安装依赖包 在unity的工具栏中点击Wi…

10天玩转Python第1天:python基础知识与常用开发工具全面详解

这里写自定义目录标题 1.课程之前1.1 课程介绍1.2 今日内容1.3 Python 介绍[了解]1.4 语言的分类 2 Python 环境配置2.1 Python 解释器的安装2.2 pycharm 的配置安装2.3 路径的选择(建议) 3 使用 pycharm 书写代码3.1 双击打开 pycharm 软件3.2 创建新 项目3.3 配置项目的路径和…