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

C++ 异常

一、异常处理

        异常处理是一种在程序运行时处理错误和异常情况的机制。它允许程序在遇到问题时不会立即崩溃,而是可以捕获并处理这些异常,从而使程序更加健壮和可靠。
        想象你在驾驶一辆汽车,突然遇到一个障碍物。你可以选择立即刹车(捕获异常),或者继续前进(程序崩溃)。显然,刹车是更安全的选择。异常处理就像是汽车的刹车系统,帮助你在遇到问题时安全地停下来。

二、C++中的异常处理机制

C++中的异常处理主要通过三个关键字来实现:trycatch 和 throw

  1. try:用于标记一个代码块,这个代码块中的代码可能会抛出异常。
  2. throw:用于抛出一个异常。当程序遇到问题时,可以使用throw语句抛出一个异常。
  3. catch:用于捕获并处理异常。catch块中的代码会在try块中抛出异常时执行。

基本语法

try {// 可能会抛出异常的代码throw exception_object; // 抛出异常
} catch (exception_type1 e1) {// 处理 exception_type1 类型的异常
} catch (exception_type2 e2) {// 处理 exception_type2 类型的异常
} catch (...) {// 处理所有其他类型的异常
}
1. 异常是通过 抛出对象而引发的,该 对象的类型决定了应该激活哪个catch的处理代码。
2. 被 选中的处理代码是调用链中 与该对象类型匹配且离抛出异常位置最近的那一个。
3. 抛出异常对象后,会生成一个 异常对象的拷贝,因为抛出的异常对象可能是一个临时对象, 所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)。
4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。
5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配, 可以抛出的派生类对象, 使用基类捕获,这个在实际中非常实用,我们后面会详细讲解这个

 

示例代码

#include <iostream>
#include <stdexcept> // 包含标准异常类double divide(double a, double b) {if (b == 0) {throw std::runtime_error("Division by zero!"); // 抛出异常}return a / b;
}int main() {try {double result = divide(10.0, 0.0); // 可能会抛出异常std::cout << "Result: " << result << std::endl;} catch (const std::runtime_error& e) { // 捕获并处理异常std::cerr << "Error: " << e.what() << std::endl;} catch (...) { // 捕获所有其他类型的异常std::cerr << "An unknown error occurred." << std::endl;}return 0;
}

在这个例子中,divide函数在除数为零时抛出一个std::runtime_error异常。main函数中的try块调用divide函数,如果抛出异常,catch块会捕获并处理这个异常。

在函数调用链中异常栈展开匹配原则

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

        4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

        也就是说当一个异常被抛出时,它会沿着调用栈向上传播,直到找到一个匹配的catch块。如果在当前函数中没有找到匹配的catch块,异常会传播到调用该函数的函数,依此类推,直到找到匹配的catch块或到达main函数。如果在main函数中仍未找到匹配的catch块,程序会终止并调用std::terminate。当异常被抛出时,C++会进行栈展开,即在传播异常的过程中,会依次退出调用栈中的函数,直到找到匹配的catch块。在这个过程中,所有局部对象会被销毁,调用它们的析构函数。

#include <iostream>
#include <stdexcept>class Resource {
public:Resource() { std::cout << "Resource acquired" << std::endl; }~Resource() { std::cout << "Resource released" << std::endl; }
};void func3() {throw std::runtime_error("Error in func3");
}void func2() {Resource res;func3();
}void func1() {try {func2();} catch (const std::runtime_error& e) {std::cerr << "Caught exception: " << e.what() << std::endl;}
}int main() {func1();return 0;
}

在这个例子中,func3抛出一个异常,func2中的Resource对象在栈展开过程中被销毁,调用其析构函数。

异常的重新抛出

有可能单个的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()
{// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再// 重新抛出去。int* array = new int[10];try {int len, time;cin >> len >> time;cout << Division(len, time) << endl;}catch (...){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;
}

异常的类型

C++支持多种类型的异常,包括标准库中的异常类(如std::runtime_errorstd::logic_error等)和用户自定义的异常类。

三、用户自定义异常类:

#include <iostream>
#include <exception>class MyException : public std::exception {
public:const char* what() const noexcept override {return "My custom exception!";}
};int main() {try {throw MyException(); // 抛出自定义异常} catch (const MyException& e) {std::cerr << "Caught exception: " << e.what() << std::endl;} catch (...) {std::cerr << "Caught an unknown exception." << std::endl;}return 0;
}

在这个例子中,我们定义了一个自定义异常类MyException,并在main函数中抛出和捕获这个异常。

四、异常安全

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

五、异常规范

1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
2. 函数的后面接throw(),表示函数不抛异常。
3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

六、异常的优缺点

 优点:

1. 异常对象定义好了, 相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息, 这样可以帮助更好的定位程序的bug
2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得 层层返回错误,最外层才能拿到错误。
3. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。
4.部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。

缺点:

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

 


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

相关文章:

  • RAG(Retrieval-Augmented Generation)基建之PDF解析的“魔法”与“陷阱”
  • EF Core 执行原生SQL语句
  • 每天认识一个设计模式-建造者模式:复杂对象的“装配式革命“
  • 05.AI搭建preparationの(transformers01)BertTokenizer实现分词编码
  • EMC知识学习一
  • 2.7 进度控制习题-2
  • 【AI学习】Transformer 模型
  • ffmpeg+QOpenGLWidget显示视频
  • Microi吾码界面设计引擎之基础组件用法大全【内置组件篇·上】
  • Deepseek API+Python 测试用例一键生成与导出 V1.0.4 (接口文档生成接口测试用例保姆级教程)
  • 深度学习框架PyTorch——从入门到精通(10)PyTorch张量简介
  • Windows命令提示符(CMD) 中切换目录主要通过 cd(Change Directory)命令实现
  • WPF InkCanvas 控件详解
  • package.json版本前缀
  • 零拷贝原理面试回答(参考小林Coding)
  • 蓝桥杯题型分布2
  • LLM - R1 强化学习 DRPO 策略优化 DAPO 与 Dr. GRPO 算法 教程
  • 可视化工具TensorBoard
  • AI小白的第八天:梯度下降(含代码实现)
  • AI数据分析:一键生成数据分析报告