[C++] 异常

embedded/2024/11/18 18:58:43/

文章目录

  • 异常的概念
  • 异常的抛出与捕获
  • 栈展开(Stack Unwinding)
        • 四、总结
  • 查找匹配的处理代码
  • 异常的重新抛出
        • 三、模拟示例:服务模块中的异常处理
        • 四、总结
  • C++ 异常规范详解
        • 一、C++98异常规范
        • 二、C++11及其后的异常规范 (`noexcept`)
        • 三、使用`noexcept`的场景与注意事项
        • 四、综合示例
        • 五、总结

异常的概念

异常机制是一种重要的错误处理方法,可以帮助程序在运行时检测并处理问题,从而提高程序的可靠性和可维护性。C++异常机制的核心思想是:将错误检测和错误处理分离,从而让程序结构更清晰。

  1. 异常的作用
    • 检测环节只需发现问题,而不需要关注问题的处理细节。
    • 异常机制允许程序的某个部分通过抛出(throw)信号,将错误信息传递给能够处理它的另一个部分。
    • 异常对象比传统的错误码更灵活,因为它可以包含更丰富的上下文信息。
  2. C语言 vs. C++异常机制
    • C语言通过返回错误码的方式处理错误,开发者需要检查返回值或查询对应的错误信息表,操作繁琐且易遗漏。
    • C++通过异常对象,可以直接携带错误信息,程序员无需额外查找错误码。

异常的抛出与捕获

在C++中,异常的抛出和捕获分为以下几个步骤:

  1. 抛出异常 (throw)
    当程序遇到错误或特殊情况时,使用throw关键字抛出异常对象:
if (b == 0) {string s("Divide by zero condition!");throw s;
}
  • 抛出的对象可以是内置类型、标准库类型(如std::string)或用户自定义类型。
  • 注意:throw之后的代码不会被执行。
  1. 捕获异常 (catch)
    捕获异常通过try-catch块完成:
try {cout << Divide(len, time) << endl;
} catch (const string& errmsg) {cout << errmsg << endl;
}
  • try块包裹可能发生异常的代码。
  • catch子句处理捕获的异常,参数类型必须匹配抛出异常的类型。
  • 捕获顺序:越具体的异常类型越靠前。例如,catch (const std::exception& e)放在通用的catch (...)之前。
  1. 重要规则
  • 如果try块中没有匹配的catch子句,异常会沿调用链向上传递。
  • 如果最终仍未找到匹配的catch,程序会调用std::terminate()终止。

栈展开(Stack Unwinding)

栈展开是C++异常机制的核心,它描述了异常从抛出到被捕获的整个传播过程。

  1. 栈展开的流程
  • 当异常被抛出时,程序会暂停当前函数的执行,并沿调用链查找匹配的catch块。
  • 首先检查throw语句所在函数是否有try-catch,如果没有或类型不匹配,退出当前函数。
  • 依次回退到调用函数,重复上述过程,直到找到匹配的catch块或到达main函数。
  1. 对象销毁
  • 栈展开过程中,函数局部对象会按逆序调用析构函数,释放资源。
  • 这使得RAII(Resource Acquisition Is Initialization)在异常处理期间依然可靠。
  1. 未捕获异常
  • 如果到达main函数仍未找到匹配的catch块,程序会终止。

  1. 示例代码
double Divide(int a, int b) {if (b == 0) {string s("Divide by zero condition!");throw s;}return (double)a / (double)b;
}void Func() {int len, time;cin >> len >> time;try {cout << Divide(len, time) << endl;} catch (const string& errmsg) {cout << "Caught exception: " << errmsg << endl;}
}int main() {try {Func();} catch (const string& errmsg) {cout << "Unhandled exception: " << errmsg << endl;}return 0;
}

四、总结
  • 异常机制的优势
    • 提高代码的可读性和可维护性。
    • 将错误检测与处理解耦,增强模块化设计。
    • 支持复杂对象的生命周期管理(如RAII)。
  • 开发建议
    • 只在异常场景中使用异常,避免过度使用。
    • 异常处理应尽量精准,不要捕获所有异常(如catch (...))。
    • 保证栈展开期间资源正确释放,推荐使用智能指针(如std::shared_ptrstd::unique_ptr)。

查找匹配的处理代码

在C++的异常处理机制中,当程序抛出一个异常对象时,系统会按照一定规则查找与该对象类型匹配的catch代码块,并执行相应的异常处理逻辑。

  1. 完全匹配的优先规则
    一般情况下,抛出的异常对象的类型与catch的形参类型完全匹配时,会优先选中该catch子句。例如:
try {throw std::string("Error occurred");
} catch (const std::string& err) {std::cout << "String exception: " << err << std::endl;
}
  1. 特殊匹配规则
    如果没有完全匹配的catch块,C++允许以下类型转换来匹配:
    • 非常量向常量转换:允许从非const类型转换为const类型。
    • 数组与指针转换:允许数组转换为指向其元素类型的指针。
    • 函数与指针转换:允许函数转换为指向函数的指针。
    • 派生类向基类转换:这是面向对象编程中最常用的设计方式。在捕获派生类对象时,可以通过基类类型进行匹配。
  2. 继承体系下的匹配示例
    继承体系允许捕获基类类型的异常,从而简化代码编写。例如:
class Exception {
public:Exception(const std::string& errmsg) : _errmsg(errmsg) {}virtual std::string what() const { return _errmsg; }
private:std::string _errmsg;
};class SqlException : public Exception {
public:SqlException(const std::string& errmsg) : Exception(errmsg) {}
};try {throw SqlException("SQL Error");
} catch (const Exception& e) { // 基类捕获派生类异常std::cout << e.what() << std::endl;
}
  1. 捕获通配符异常
    如果异常没有与任何具体类型匹配,可以使用通配符catch (...)捕获所有类型的异常。这种方式一般用于处理未知异常:
try {throw 42;
} catch (...) {std::cout << "Unknown exception caught!" << std::endl;
}
  1. 未捕获异常的处理
    如果异常传播到main函数仍未被捕获,程序会调用std::terminate()函数终止程序。为了避免程序非预期终止,可以在main中使用catch (...)捕获所有未匹配的异常。

异常的重新抛出

在某些情况下,捕获到一个异常后,需要将其重新抛出,供调用链上的其他部分继续处理。

  1. 重新抛出异常 (throw;)
    catch块中,使用不带参数的throw关键字可以重新抛出当前捕获的异常。例如:
try 
{throw std::runtime_error("Error occurred");
} 
catch (const std::exception& e) {std::cout << "Caught: " << e.what() << ", rethrowing..." << std::endl;throw; // 重新抛出异常
}
  1. 传递异常的场景
    • 局部处理与传递:在捕获异常后执行部分处理操作,再重新抛出异常让更高层次代码进行处理。
    • 日志记录:记录异常日志,然后将异常重新抛出。
  2. 重新抛出后的异常处理
    重新抛出的异常会沿调用链继续传播,直至找到匹配的catch块。例如:
void InnerFunc() {throw std::runtime_error("Inner exception");
}void OuterFunc() {try {InnerFunc();} catch (...) {std::cout << "Logging exception in OuterFunc" << std::endl;throw; // 重新抛出异常}
}int main() {try {OuterFunc();} catch (const std::exception& e) {std::cout << "Caught in main: " << e.what() << std::endl;}
}

输出结果:

Logging exception in OuterFunc
Caught in main: Inner exception
  1. 带参数的重新抛出
    可以在catch块中捕获异常后,抛出另一个异常对象:
try {throw std::runtime_error("Original Error");
} catch (const std::exception& e) {throw std::logic_error("New Error"); // 抛出新的异常
}
  1. 注意事项
    • 在重新抛出异常时,资源的释放需要特别注意,建议使用智能指针或RAII管理资源。
    • 捕获基类对象重新抛出时,避免丢失原始的派生类信息。

三、模拟示例:服务模块中的异常处理

以下示例展示了如何在复杂项目中使用异常处理、基类匹配以及重新抛出异常。

class Exception {
public:Exception(const std::string& errmsg) : _errmsg(errmsg) {}virtual std::string what() const { return _errmsg; }
private:std::string _errmsg;
};class SqlException : public Exception {
public:SqlException(const std::string& errmsg) : Exception(errmsg) {}
};void SQLMgr() {throw SqlException("SQL Error");
}void CacheMgr() {try {SQLMgr();} catch (const Exception& e) {std::cout << "Caught in CacheMgr: " << e.what() << std::endl;throw; // 重新抛出}
}int main() {try {CacheMgr();} catch (const Exception& e) {std::cout << "Caught in main: " << e.what() << std::endl;}return 0;
}

运行结果:

Caught in CacheMgr: SQL Error
Caught in main: SQL Error

四、总结
  • 查找匹配代码的关键点
    • 完全匹配优先。
    • 支持类型转换,如派生类向基类的转换。
    • 提供通配符捕获(catch (...))以处理未知异常。
  • 异常重新抛出
    • 使用throw;重新抛出当前异常。
    • 可以抛出不同的异常对象,向上传递更多的上下文信息。
    • 重新抛出时要注意资源管理,避免内存泄漏。

通过合理运用异常匹配与重新抛出,能够让程序在复杂情况下保持健壮性和可维护性。

C++ 异常规范详解

在C++中,异常规范是描述函数是否可能抛出异常,以及可能抛出哪些类型的异常。随着C++标准的演变,异常规范的用法发生了一些变化,从C++98的throw()到C++11及之后的noexcept,逐步变得简化和实用。


一、C++98异常规范
  1. 语法
    在C++98中,函数的参数列表后面可以添加throw()throw(类型列表),用于说明函数可能抛出异常的情况:
void func1() throw();              // 表示函数不会抛出任何异常
void func2() throw(std::bad_alloc); // 表示函数可能抛出std::bad_alloc异常
void func3() throw(int, char);     // 表示函数可能抛出int或char类型的异常
  1. 问题
    • C++98异常规范不会被强制执行。即便一个函数声明为throw()(不会抛出异常),但实际抛出了异常,程序仍可能崩溃。
    • 限制过于繁琐,在实践中难以使用。例如,声明多个可能抛出的类型时,类型检查复杂。
  2. 缺点
    • 性能影响:编译器需要生成额外代码进行类型检查。
    • 实际不可靠:标准库函数通常不使用throw(类型),在现代开发中也很少被使用。

二、C++11及其后的异常规范 (noexcept)

为解决C++98中异常规范的不足,C++11引入了noexcept,替代throw(),并提供更强大的功能和简单的语法。

  1. noexcept基本语法
    • noexcept表示函数不会抛出任何异常:
void func1() noexcept;  // 保证函数不会抛出异常
void func2();           // 未声明noexcept,可能抛出异常
- 如果`noexcept`函数实际抛出了异常,程序会调用`std::terminate()`终止执行,而不会进行异常传播。
  1. noexcept(expression)

示例:

int i = 0;
std::cout << noexcept(++i) << std::endl;        // 输出1(不会抛异常)
std::cout << noexcept(throw "Error!");         // 输出0(会抛异常)
- `noexcept`还可以作为**运算符**,用于判断表达式是否可能抛出异常:
noexcept(expression)
    * 如果`expression`在编译期确定不会抛出异常,`noexcept(expression)`返回`true`。* 如果可能抛出异常,返回`false`。
  1. 与C++98的区别
    • 兼容性noexcept取代了throw(),现代C++中几乎不再使用throw()
    • 强制性noexcept是更强的约束,声明为noexcept的函数如果抛出异常,程序直接终止。
    • 简单性noexcept比C++98的throw(类型)更简洁,无需列出具体类型。
  2. 编译器行为
    • 不会强制检查:编译器不会在编译时检查noexcept修饰的函数是否实际可能抛出异常。
    • 运行时行为:如果noexcept函数实际抛出了异常,直接调用std::terminate()

三、使用noexcept的场景与注意事项
  1. 标准库中的noexcept
    标准库中的许多函数使用了noexcept修饰。例如:
size_t size() const noexcept;  // 容器的size()函数不会抛出异常
iterator begin() noexcept;     // begin()函数也不会抛出异常
  1. 用户定义函数
    • 如果可以明确保证函数不会抛出异常,建议使用noexcept,这可以帮助编译器优化代码。
    • 例如:
double Divide(int a, int b) noexcept {if (b == 0) {throw "Division by zero condition!"; // 会导致std::terminate()}return (double)a / (double)b;
}
  1. 异常的影响
    • 如果noexcept函数抛出异常,程序终止执行,且不会传播异常。
    • 因此,在设计API时,应当慎重决定是否使用noexcept,只有在可以完全保证不抛出异常时才使用。
  2. 优化潜力
    • 编译器可以针对noexcept函数进行优化,因为可以假设它们不会抛出异常。
    • 对于容器操作(如std::vector的移动构造),如果被移动的对象的移动操作声明为noexcept,容器可以更高效地移动对象。

四、综合示例

以下代码展示了noexcept的使用,以及noexcept(expression)运算符的行为:

#include <iostream>
#include <stdexcept>int SafeDivide(int a, int b) noexcept {if (b == 0) {throw "Division by zero!"; // noexcept函数抛异常会终止程序}return a / b;
}int PotentialThrow(int x) {if (x < 0) throw std::runtime_error("Negative value!");return x;
}int main() {try {std::cout << "SafeDivide: " << SafeDivide(10, 0) << std::endl;} catch (...) {std::cout << "Caught exception in SafeDivide!" << std::endl;}std::cout << "noexcept(SafeDivide(10, 2)): "<< noexcept(SafeDivide(10, 2)) << std::endl; // 输出1(静态分析)std::cout << "noexcept(PotentialThrow(10)): "<< noexcept(PotentialThrow(10)) << std::endl; // 输出0(可能抛异常)return 0;
}

运行结果:

Caught exception in SafeDivide!
noexcept(SafeDivide(10, 2)): 1
noexcept(PotentialThrow(10)): 0

五、总结
  1. C++98中的异常规范(throw()
    • 提供对可能抛出的异常类型的声明,但在实践中不常用,现代C++中已基本弃用。
  2. C++11及之后的异常规范(noexcept
    • 简洁高效,标记函数不会抛出异常。
    • 编译器可利用noexcept进行优化,增强程序的性能。
  3. 实践建议
    • 对于不会抛出异常的函数,明确声明为noexcept
    • 避免滥用noexcept,因为一旦函数抛出异常,程序会直接终止。
    • 使用noexcept(expression)进行静态分析,确保表达式的安全性。

通过合理使用异常规范,可以提高代码的可读性和可靠性,同时优化程序性能。


http://www.ppmy.cn/embedded/138608.html

相关文章

一、Vue安装

1、安装 1. 必安插件 ( ⽂件夹 ) 下的 google-access-helper-2.3.0( ⽂件夹 ) 复制到你想放的⽂件夹下 ( 安装后不可以挪动 位置 ) 建议 D 盘下 , 弄⼀个专门按软件的⽂件夹 2. 打开⾕歌浏览器 - 扩展程序 - 开发者模式打开 - 把⽂件夹拖进来就安装完毕 功能如下 : ⾕歌…

Docker 基础命令介绍和常见报错解决

介绍一些 docker 可能用到的基础命令&#xff0c;并解决三个常见报错&#xff1a; 权限被拒绝&#xff08;Permission Denied&#xff09;无法连接到 Docker 仓库&#xff08;Timeout Exceeded&#xff09;磁盘空间不足&#xff08;No Space Left on Device&#xff09; 命令以…

Vue3 provide 和 inject的使用

在 Vue 中&#xff0c;provide 和 inject 是 Composition API 的一对功能&#xff0c;用于父子组件之间的依赖注入。它们的作用是让父组件可以向其所有子组件提供数据或方法&#xff0c;而不需要通过逐层传递 props。 1. provide provide 用于父组件中&#xff0c;提供数据或…

多目标优化算法:多目标鹅算法(MOGOOSE)求解UF1-UF10,提供完整MATLAB代码

一、鹅算法GOOSE 鹅算法&#xff08;Goose Algorithm&#xff0c;简称GOOSE&#xff09;是一种新型的元启发式算法&#xff0c;灵感来源于鹅在休息和觅食时的行为。GOOSE算法由Rebwar Khalid Hamad等人于2024年1月发表在Springer旗下的非开源SCI期刊《Evolving Systems》上。该…

Redis 持久化机制 RDB 和 AOF 区别

Redis 是一个开源的内存数据结构存储系统&#xff0c;广泛应用于缓存、会话存储、实时分析等场景。虽然 Redis 本质上是内存数据库&#xff0c;但它支持持久化机制&#xff0c;将数据保存在磁盘中以防止数据丢失。在 Redis 中&#xff0c;主要有两种持久化机制&#xff1a;RDB(…

前端隐藏元素的方式有哪些?HTML 和 CSS 中隐藏元素的多种方法

当面试官突然问你&#xff1a;“前端隐藏元素的方式有哪些&#xff1f;”你还是只知道 display: none 吗&#xff1f; 其实&#xff0c;在前端开发的世界里&#xff0c;隐藏元素的方法非常多。每种方法都有自己的小技巧和使用场景&#xff0c;了解它们不仅能让你应对自如&…

【Python系列】Python中打印详细堆栈信息的技巧

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

elementUI input 禁止内容两端存在空格,或者是自动去除两端空格

需求 项目中有需求&#xff1a;输入框中禁止内容两端存在空格&#xff0c;或者是自动去除两端空格。 解决方法 vue的api文档中有过介绍&#xff0c;使用.trim可以去掉用户输入内容中两端的空格&#xff0c;如下图 代码 <el-input v-model.trim"name" cleara…