当前位置: 首页 > news >正文

异常知识及其使用

异常的简单概念

在C++中,异常处理是一种机制,用于处理程序运行时发生的意外情况。它允许程序在发生错误时,将控制权转移到一个专门的代码块,而不是让程序直接崩溃。C++的异常处理机制包括以下几个关键概念:

throw

  • 用途:用于抛出一个异常。
  • 语法throw exception;
  • 说明throw关键字后面可以跟任何类型的表达式,包括基本数据类型、对象、异常类实例等。当throw被执行时,程序控制流将立即转移到最近的匹配的catch块。
  • 示例
    throw std::runtime_error("An error occurred");

try

  • 用途:标记可能抛出异常的代码块。
  • 语法try { /* 可能抛出异常的代码 */ }
  • 说明try块包含了可能引发异常的代码。如果try块中的代码抛出了异常,程序将搜索匹配的catch块来处理这个异常。如果没有匹配的catch块,异常将被传播到调用栈的上一层。
  • 示例
    try {// 代码可能抛出异常
    } catch (const std::exception& e) {// 处理std::exception类型的异常
    }

catch

  • 用途:捕获并处理异常。
  • 语法catch (exception_type exception_name) { /* 处理异常的代码 */ }
  • 说明catch块用于捕获特定类型的异常,并提供处理异常的代码。一个try块后面可以有多个catch块,以捕获不同类型的异常。
  • 示例
    try {// 可能抛出多种类型的异常
    } catch (const std::bad_alloc& e) {// 处理内存分配失败的异常
    } catch (const std::exception& e) {// 处理其他标准异常
    } catch (...) {// 处理所有未被捕获的异常
    }

组合使用:

  • trycatchthrow通常一起使用,以实现异常的抛出和捕获。throw用于在检测到错误时立即将错误信息传递出去,try块用于包围可能抛出异常的代码,而catch块则用于处理这些异常。
  • 异常处理允许程序的某一部分专注于检测问题,而另一部分专注于解决问题,从而实现代码的解耦和模块化。

异常的抛出和捕获

异常抛出(throw)

  • 引发异常:当程序遇到一个无法处理的情况时,可以通过throw关键字抛出一个异常对象。这个对象可以是任何类型,包括自定义的异常类实例、标准异常类实例或者基本数据类型。

double Division(int a, int b)
{//除数为0无意义,需要抛异常if (b == 0){throw (string)"Division by zero condition!";}//这里不需要else,是因为如果有抛异常,一旦throw被执行,throw之后的代码将不会被执行。程序的执行流程会立即跳转到匹配的catch块。return ((double)a) / ((double)b);
}

这里我们本来抛出的异常对象类型是 const char* ,这里我通过强制类型转换,将该抛异常的对象的类型转换成 string 。 

  • 调用链和匹配:抛出的异常对象的类型将决定哪个catch块会捕获这个异常。编译器会在调用链中寻找匹配的catch块,即类型匹配最接近抛出点catch块。

图一
  • 执行流程的改变:一旦throw被执行,throw之后的代码将不会被执行。程序的执行流程会立即跳转到匹配的catch块。如果当前函数中没有匹配的catch块,异常会沿着调用栈向上传播,直到找到匹配的catch块或者程序终止。

这⾥还有两个重要的含义:
  1. 沿着调⽤链的函数可能提早退出。
  2. ⼀旦程序开始执⾏异常处理程序,沿着调⽤链创建的对象都将销毁。(不能匹配的栈帧就被直接销毁了,它后面的代码不会执行)

根据上图我们可以看出来:Division()进行抛异常后,除法运算没有被执行,而又因为Division()中的catch不匹配,没有进行catch的能力,所以 cout << "double Division(int a, int b)" << endl; 没有被执行。(控制权从throw位置转移到了catch位置

我们可以发现:

if (b == 0)
{int* p = new(int);throw (string)"Division by zero condition!";
}

这样写的话,如果在该生命周期没有因为抛异常被catch,造成没有对该new的资源的释放,就会引起内存泄露等问题,解决该问题,我们主要是通过智能指针进行解决 

异常捕获(catch)

  • 处理异常catch块用于捕获并处理异常。它可以捕获特定类型的异常,也可以捕获所有类型的异常(使用catch(...))。 

目的是为了不让程序直接挂掉,也是为了防止当catch和抛出异常不匹配的,导致执行流乱跳,但是问题就是不知道异常错误是什么。

  • 对象销毁:当控制权转移到catch块时,从抛出异常点到catch块之间的所有局部对象都会被销毁。这是因为C++的异常处理机制要求在异常传播过程中,所有局部对象都必须被正确地析构。

  • 异常对象的拷贝当抛出一个异常时,如果抛出的是一个非静态局部对象(即在函数内部定义的对象),编译器会创建这个对象的一个拷贝。这个拷贝会被传递给catch块,并且在catch块执行完毕后被销毁。这样做的目的是为了确保异常对象在传递过程中的安全性和完整性。

即使std::string具有移动构造函数,当抛出一个std::string对象时,如果没有移动构造函数,编译器仍然会创建一个拷贝。这是因为异常对象需要被传递给catch块,并且必须在整个传递过程中保持有效。

由于移动构造的存在,所以没有进行临时拷贝,如果没有移动构造,抛出的对象是会被拷贝一份的。(C++11引入了移动语义,允许资源的转移而不是拷贝。如果一个对象具有移动构造函数,那么在右值情况下,可以调用移动构造函数来转移资源,而不是拷贝资源。

栈展开 

  • 抛出异常后,程序暂停当前函数的执⾏,开始寻找与之匹配的catch⼦句,⾸先检查throw本⾝是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地⽅进⾏处理。
  • 如果当前函数中没有try/catch⼦句,或者有try/catch⼦句但是类型不匹配,则退出当前函数,继续在外层调⽤函数链中查找,上述查找的catch过程被称为栈展开。
  • 如果到达main函数,依旧没有找到匹配的catch⼦句,程序会调⽤标准库的 terminate 函数终⽌程序。(这里我们可以用catch(...)来进行间接阻断程序的直接报错)
  • 如果找到匹配的catch⼦句处理后,catch⼦句代码会继续执⾏。

其实我们上面的图一示例就是一个栈展开的过程

图二

查找匹配的处理代码

  • ⼀般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个。(这在上面图一也有详细说明)
  • 但是也有⼀些例外,允许从⾮常量向常量的类型转换,也就是权限缩⼩;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派⽣类向基类类型的转换,这个点⾮常实⽤,实际中继承体系基本都是⽤这个⽅式设计的
#include <iostream>
#include <string>class Base {};
class Derived : public Base {};void handleBase(const Base& b) {std::cout << "Caught const Base by reference" << std::endl;
}void handleBasePtr(Base* b) {std::cout << "Caught Base*" << std::endl;
}void handleDerived(Derived d) {std::cout << "Caught Derived by value" << std::endl;
}int main() {Derived d;const Base& b = d;try {// 抛出Derived类型的对象throw d;} catch (const Base& e) { // 匹配规则:const转换handleBase(e);} catch (Base* e) { // 匹配规则:数组到指针的转换(这里Base是多态类型,所以适用)handleBasePtr(e);}try {// 抛出Derived类型的对象throw d;} catch (Derived e) { // 精确匹配handleDerived(e);}return 0;
}
  • 如果到main函数,异常仍旧没有被匹配就会终⽌程序,不是发⽣严重错误的情况下,我们是不期望程序终⽌的,所以⼀般main函数中最后都会使⽤catch(...),它可以捕获任意类型的异常,但是是不知道异常错误是什么。

一般大型项目程序才会使用异常,下面我们模拟设计一个服务的几个模块,每个模块的继承都是Exception的派生类,每个模块可以添加自己的数据,最后捕获时,我们捕获基类就可以:

#include<thread>//Exception:基类,包含错误信息和ID。
class Exception
{
public:Exception(const string& errmsg, int id):_errmsg(errmsg), _id(id){}virtual string what() const{return _errmsg;}int getid() const{return _id;}
protected:string _errmsg;int _id;
};//SqlException:数据库相关的异常,包含SQL语句。
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;
};//CacheException:缓存相关的异常。
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;}
};//HttpException:HTTP服务相关的异常,包含请求类型。
class HttpException : public Exception
{
public:HttpException(const string& errmsg, int id, const string& type):Exception(errmsg, id), _type(type){}virtual string what() const{string str = "HttpException:";str += _type;str += ":";str += _errmsg;return str;}private:const string _type;
};//SQLMgr:模拟数据库管理,随机抛出SqlException。
void SQLMgr()
{if (rand() % 7 == 0){throw SqlException("权限不足", 100, "select * from name = '张三'");}else{cout << "SQLMgr 调用成功" << endl;}
}//CacheMgr:模拟缓存管理,随机抛出CacheException,并调用SQLMgr。
void CacheMgr()
{if (rand() % 5 == 0){throw CacheException("权限不足", 100);}else if (rand() % 6 == 0){throw CacheException("数据不存在", 101);}else{cout << "CacheMgr 调用成功" << endl;}SQLMgr();
}//HttpServer:模拟HTTP服务,随机抛出HttpException,并调用CacheMgr。
void HttpServer()
{if (rand() % 3 == 0){throw HttpException("请求资源不存在", 100, "get");}else if (rand() % 4 == 0){throw HttpException("权限不足", 101, "post");}else{cout << "HttpServer调用成功" << endl;}CacheMgr();
}int main()
{srand(time(0));while (1){this_thread::sleep_for(chrono::seconds(1));try{HttpServer();}//利用继承中的赋值兼容转换,切片操作catch (const Exception& e) // 这里捕获基类,基类对象和派生类对象都可以捕获{// 多态调用// 如果没有将what()声明为虚函数,那么使用基类指针或引用调用what()时// 将始终调用基类的实现,这将不会反映出派生类的特定错误信息,从而失去多态的优势cout << e.what() << endl;}catch (...){cout << "Unkown Exception" << endl;}}return 0;
}

这种像日志一样记录,我们就可以清楚的知道了时哪一个模块的错误,这样就可以更高效的找到抛异常的地方,更快的解决问题 。

异常重新抛出

有时catch到⼀个异常对象后,需要对错误进⾏分类,其中的某种异常错误需要进⾏特殊的处理,其他错误则重新抛出异常给外层调⽤链处理。捕获异常后需要重新抛出,直接 throw ; 就可以把捕获的对象直接抛出。

// 下面程序模拟展示了聊天时发送消息,发送失败补货异常,但是可能在
// 电梯地下室等场景手机信号不好,则需要多次尝试,如果多次尝试都发
// 送不出去,则就需要捕获异常再重新抛出,其次如果不是网络差导致的
// 错误,捕获后也要重新抛出。
void _SendMsg(const string& s)
{if (rand() % 2 == 0){throw HttpException("网络不稳定,发送失败", 102, "put");}else if (rand() % 7 == 0){throw HttpException("你已经不是对象的好友,发送失败", 103, "put");}else{cout << "发送成功" << endl;}
}void SendMsg(const string& s)
{// 发送消息失败,则再重试3次for (size_t i = 0; i < 4; i++){try{_SendMsg(s);break;}catch (const Exception& e){// 捕获异常,if中是102号错误,网络不稳定,则重新发送// 捕获异常,else中不是102号错误,则将异常重新抛出if (e.getid() == 102){// 重试三次以后否失败了,则说明网络太差了,重新抛出异常if (i == 3)throw;cout << "开始第" << i + 1 << "重试" << endl;}else{// 重新抛出throw;}}}
}int main()
{srand(time(0));string str;while (cin >> str){try{SendMsg(str);}catch (const Exception& e){cout << e.what() << endl << endl;}catch (...){cout << "Unkown Exception" << endl;}}return 0;
}

异常安全问题

  • 异常抛出后,后⾯的代码就不再执⾏,前⾯申请了资源(内存、锁等),后⾯进⾏释放,但是中间可能会抛异常就会导致资源没有释放,这⾥由于异常就引发了资源泄漏,产⽣安全性的问题。中间我们需要捕获异常,释放资源后⾯再重新抛出,当然后⾯智能指针章节讲的RAII⽅式解决这种问题是更好的。
  • 其次析构函数中,如果抛出异常也要谨慎处理,⽐如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要捕获处理,否则后⾯的5个资源就没释放,也资源泄漏了。《Effctive C++》第8个条款也专⻔讲了这个问题,别让异常逃离析构函数。
#include <exception>double Divide(int a, int b) {if (b == 0) {throw "Division by zero condition!";}return (double)a / (double)b;
}void Func() {//std::vector<int> array(10); // 使用vector自动管理内存int* array = new int[10];try {int len, time;cin >> len >> time;cout << Divide(len, time) << std::endl;}catch (...) {// 捕获异常释放内存cout << "delete []" << array << endl;delete[] array;throw; // 异常重新抛出,捕获到什么抛出什么}cout << "delete []" << array << endl;delete[] array;
}int main() {try {Func();}catch (const char* errmsg) {std::cout << errmsg << std::endl;}catch (const std::exception& e) {std::cout << e.what() << std::endl;}catch (...) {std::cout << "Unknown Exception" << std::endl;}return 0;
}

对于Func函数中:在catch (...)块中,捕获所有类型的异常,释放动态分配的内存(catch(...)就是为了这个),并重新抛出异常,以便外层catch块可以进一步处理。

异常规范

  1. 构造函数和析构函数中的异常

    • 避免在构造函数中抛出异常:如果在构造函数中抛出异常,可能会导致对象处于不完整或未完全初始化的状态,这可能会导致资源泄漏或程序崩溃。
    • 避免在析构函数中抛出异常:如果在析构函数中抛出异常,可能会导致资源释放不完全,从而引发资源泄漏。
  2. 异常安全

    • 在C++中,异常处理需要特别注意资源管理,以避免因异常而导致的资源泄漏问题。例如,在newdelete操作中,如果抛出异常,可能会导致内存泄漏。
  3. 异常规范

    • C++委员会提出了一套建议性规范,用于明确函数可能抛出的异常类型。这包括在函数声明后使用throw(类型)来列出可能抛出的异常类型,或使用throw()表示函数不抛出异常。
    • 然而,这种规范较为繁琐,且在实际应用中较少使用,因为它只是一个建议,不遵循也不会导致编译错误。
  4. C++11的改进

    • C++11引入了noexcept关键字,用于简化异常声明。在不会抛出异常的函数后添加noexcept,可以明确表示该函数不会抛出异常,从而使异常声明更加简洁。

以下是一些示例代码,展示了如何在C++中应用这些最佳实践:

#include <iostream>
#include <new> // 用于std::bad_allocclass Resource {
public:Resource() {// 构造函数中避免抛出异常}~Resource() noexcept {// 析构函数中避免抛出异常// noexcept确保析构函数不会抛出异常}
};void func() noexcept {// 表示这个函数不会抛出异常
}void mayThrow() {throw std::bad_alloc(); // 可能抛出异常
}int main() {Resource r;try {mayThrow();} catch (const std::bad_alloc& e) {std::cout << "Caught bad_alloc: " << e.what() << std::endl;}return 0;
}

在这个示例中:

  • Resource类的构造函数和析构函数都被设计为不会抛出异常,以避免资源泄漏和对象状态不一致的问题。
  • func函数使用noexcept关键字声明,表示它不会抛出异常。
  • mayThrow函数可能抛出std::bad_alloc异常,这需要在调用它的代码中进行处理。

通过遵循这些最佳实践,可以提高C++程序的健壮性和异常安全性。

noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,可能会则返回
false,不会就返回true。
int i = 0;// 检查表达式是否是noexcept
cout << noexcept(Divide(1, 2)) << endl;
cout << noexcept(Divide(1, 0)) << endl;
cout << noexcept(++i) << endl;

结果:

(0,0,1)

C++标准库也定义了⼀套⾃⼰的⼀套异常继承体系库,基类是exception,所以我们⽇常写程序,需要在主函数捕获exception即可,要获取异常信息,调⽤what函数,what是⼀个虚函数,派⽣类可以重写:

C语言和C++的错误处理区别

1. 错误处理方式

C语言:

  • 错误码:C语言通常使用返回值或全局变量来传递错误信息。函数执行完毕后,会返回一个特定的错误码,表示操作是否成功或失败。调用者需要检查这个返回值,并根据错误码查询相应的错误信息。
  • 缺点这种方式需要调用者记住各种错误码的含义,增加了记忆负担,且代码可读性较差。

C++异常处理:

  • 异常对象:C++通过抛出异常对象来处理错误。异常对象可以携带丰富的错误信息,包括错误类型、错误消息等
  • 优点异常处理使得代码更加简洁和易于理解,因为异常对象可以包含详细的错误信息,调用者不需要记住错误码,只需要捕获异常并处理即可。

2. 代码结构和可读性

C语言:

  • 错误检查:在C语言中,函数调用后通常需要检查返回值,这可能导致代码中出现大量的if语句,降低了代码的可读性。

C++异常处理:

  • 分离错误检测和处理:在C++中,错误检测和错误处理可以完全分离。检测到错误的地方只需抛出异常,而处理异常的地方则在catch块中定义,这样使得代码更加模块化,提高了代码的可读性和维护性。

3. 异常的传递和处理

C语言:

  • 函数调用链:在C语言中,如果一个函数发生错误,它需要返回错误码,调用者需要处理这个错误码,然后可能需要再次调用其他函数来处理错误,这形成了一个错误处理的调用链。

C++异常处理:

  • 异常传播:在C++中,如果一个函数抛出异常而没有被捕获,异常会沿着调用栈向上传播,直到被一个catch块捕获。这种方式简化了错误处理流程,因为不需要在每个函数调用后都检查错误码

4. 异常对象的灵活性

C++异常处理:

  • 自定义异常类:C++允许开发者定义自己的异常类,这些类可以继承自std::exception。这样,异常对象不仅可以包含错误消息,还可以包含其他有用的信息,如错误发生的位置、时间等。

5. 性能考虑

C语言:

  • 性能开销:使用错误码的方式通常没有额外的性能开销,因为不需要额外的内存分配和对象复制。

C++异常处理:

  • 性能开销:异常处理可能会带来一定的性能开销,因为需要分配内存来创建异常对象,以及可能的栈展开(stack unwinding)操作。因此,在性能敏感的场合,需要谨慎使用异常。

总结来说,C++的异常处理机制提供了一种更加灵活和强大的错误处理方式,它允许开发者将错误检测和处理逻辑分离,提高了代码的可读性和维护性。然而,这种机制也带来了一定的性能开销,因此在设计程序时需要权衡使用。

异常处理的注意事项

  • 提早退出:如果一个函数在执行过程中抛出了异常,并且这个异常没有在该函数内部被捕获,那么这个函数将提早退出,并且不会执行任何清理代码。因此,使用异常处理时,需要确保资源的正确管理和释放。

  • 栈展开(Stack Unwinding)当异常沿着调用栈传播时,会触发栈展开过程,这个过程会销毁抛出点和捕获点之间的所有局部对象,并调用它们的析构函数。

  • 性能考虑异常处理可能会带来性能开销,因为涉及到栈展开和异常对象的拷贝。因此,在性能敏感的代码中,应该尽量避免使用异常处理。

  • 异常安全在设计程序时,应该考虑到异常安全,确保在异常发生时,程序的状态仍然保持一致,资源得到正确释放。


http://www.mrgr.cn/news/79030.html

相关文章:

  • mysql 根据顺序赋排序【@rownum循环赋值,变量给排序】
  • 顶刊算法 | 鱼鹰算法OOA-BiTCN-BiGRU-Attention多输入单输出回归预测(Maltab)
  • Python Web 开发:FastAPI 路由装饰器与路径参数应用
  • Ai编程cursor + sealos + devBox实现登录以及用户管理增删改查(十三)
  • 用Go语言重写Linux系统命令 -- ls
  • 【星海随笔】syslinux
  • 级联树结构TreeSelect和上级反查
  • mybatis-xml映射文件及mybatis动态sql
  • 嵌入式蓝桥杯学习1 点亮LED
  • 003-SpringBoot整合Pagehelper
  • C++学习笔记
  • springboot vue 会员收银系统 (12)购物车关联服务人员 订单计算提成 开源
  • 2.2 线性表的顺序表示
  • ultralytics-YOLOv11的目标检测解析
  • WPF+LibVLC开发播放器-LibVLC在C#中的使用
  • Python 入门教程(2)搭建环境 | 2.4、VSCode配置Node.js运行环境
  • 如何手搓一个智能激光逗猫棒
  • 当大的div中有六个小的div,上面三个下面三个,当外层div高变大的时候我希望里面的小的div的高也变大
  • C 语言 “神秘魔杖”—— 指针初相识,解锁编程魔法大门(一)
  • [docker中首次配置git环境与时间同步问题]
  • Spring Cloud Alibaba(六)
  • Java NIO channel
  • 【教学类-43-25】20241203 数独3宫格的所有可能-使用模版替换(12套样式,空1格-空8格,每套510张,共6120小图)
  • Bert+CRF的NER实战
  • OpenSSL 自建CA 以及颁发证书(网站部署https双向认证)
  • 细说STM32单片机用定时器触发DAC输出三角波并通过串口观察波形的方法