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

C++进阶(07): 异常

💬 :如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!

👍 :如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并分享给更多的朋友噢~!


1. C 语言传统的错误处理方式

C 语言在实际应用中,基本采用返回错误码的方式处理错误。在部分非常严重错误的情形下,使用终止程序的方式。

1.1 返回错误码

  • 实现方式:系统中许多库的接口函数通过将错误码存储在errno中来表示错误。

示例:

#include <stdio.h> #include <errno.h> #include <string.h> int main() { FILE* file = fopen("nonexistent_file.txt", "r"); if (file == NULL) { // 打印错误信息 fprintf(stderr, "打开文件时出错: %s\n", strerror(errno)); return errno; } fclose(file); return 0; }
  • 缺陷:程序员需自行查找错误码对应的错误信息。

1.2 终止程序

  • 实现方式:借助assert等手段实现。

示例:

#include <stdio.h> #include <assert.h> double divide(double a, double b) { // 传入的表达式为假时,程序会终止并输出错误信息 assert(b != 0); return a / b; } int main() { double result = divide(10.0, 0.0); printf("结果: %f\n", result); return 0; }
  • 缺陷:用户体验差,当出现内存错误、除零错误等情况时,程序会直接终止运行。

2.C++ 异常

2.1 异常的概念

异常是 C++ 中处理错误的机制。当函数遇到自身无法处理的错误时,会抛出异常给直接调用者。若直接调用者也无法处理,异常便会沿着调用链向上层传递,直至被能处理该错误的调用者捕获并处理。

2.2 相关关键字

关键字作用使用场景特点
throw抛出异常当程序中出现问题,函数无法处理时无,直接抛出指定类型的异常对象,异常对象类型可为内置 / 自定义类型
try界定可能抛出异常的代码范围将可能会抛出异常的代码放在 try 块中后面通常跟随一个或多个 catch 块,用于捕获 try 块中抛出的异常
catch捕获异常在需要对异常进行处理的位置可设置多个 catch 块,每个块可捕获特定类型的异常

throw / try / catch 基本语法

throw 异常对象;
try { // 可能抛出异常的代码 }
catch (异常类型 异常对象名) { // 处理异常的代码 }

2.4 异常的使用

2.4.1 异常的抛出和捕获

2.4.1.1 异常的抛出和匹配原则
  • (1)对象类型决定处理代码:异常通过抛出对象引发,异常对象的类型决定了应该激活哪个catch的处理代码。
  • (2)就近匹配原则:在调用链中,被选中的catch处理代码是与异常对象类型匹配且离抛出异常位置最近的那一个。
  • (3)异常对象拷贝机制:抛出异常对象后,会生成一个该异常对象的拷贝。这个拷贝的临时对象在被catch处理后销毁(其机制类似于函数的传值返回)。
  • (4)通用捕获情况catch能够捕获任意类型的异常,不过使用该方式无法明确具体的错误信息。
  • (5)派生类对象捕获例外:实际应用中,抛出和捕获的匹配原则存在例外情况——可以抛出派生类对象,使用基类的catch块进行捕获,这种方式在实际编程中非常实用。
2.4.1.2 函数调用链中异常栈展开的匹配原则

沿着调用链查找匹配catch子句的过程被称作栈展开。

  • (1) 首先检查throw操作是否处于try块内部。若处于try块内,则开始查找与之匹配的catch语句。若存在匹配的catch语句,程序将跳转到该catch块进行异常处理。
  • (2) 若在当前try块对应的所有catch语句中,没有找到与抛出异常相匹配的catch块,程序会退出当前函数栈。然后,在调用当前函数的函数栈中继续查找匹配的catch语句。
  • (3) 若沿着调用链不断查找,直至到达main函数栈,依然没有找到匹配的catch语句,程序将终止运行。
  • (4) 为了防止因存在未捕获的异常而导致程序直接终止,在实际编程中,通常需要在异常处理的最后添加catch(...){} 语句,用以捕获任意类型的异常。
  • (5) 当程序找到匹配的catch子句并完成异常处理后,会继续执行该catch子句后面的代码。
2.4.1.3 示例
#include <iostream> double divide(double dividend, double divisor) { if (divisor == 0) { // 若除数为 0,抛出一个字符串类型的异常 throw "Error: Division by zero!"; } // 若除数不为 0,进行除法运算并返回结果 return dividend / divisor; } int main() { double num1, num2; std::cout << "请输入被除数: "; std::cin >> num1; std::cout << "请输入除数: "; std::cin >> num2; try { double result = divide(num1, num2); std::cout << "结果: " << result << std::endl; } catch (const char* error) { // 捕获抛出的字符串类型异常,并输出错误信息 std::cerr << error << std::endl; } return 0; }

2.4.2 异常的重新抛出

单个catch块可能无法彻底处理异常。鉴于此,在catch块捕获到异常后,可先进行校正处理,再

重新抛出异常传递给更上层函数处理。

校正处理可能涵盖

  • 资源清理(如释放动态分配的内存、关闭文件句柄或网络连接等)、
  • 数据修复(将数据恢复到某个已知的正确状态)、
  • 日志记录(记录异常的详细信息,以便后续分析)等操作。

示例:

#include <iostream> double Division(int a, int b) { if (b == 0) { throw "Division by zero condition!"; } return (double)a / (double)b; } // 中间函数,捕获异常并进行校正处理后重新抛出 void Func() { int* array = new int[10]; try { int len, time; std::cout << "请输入两个整数(用空格分隔): "; std::cin >> len >> time; std::cout << "除法结果: " << Division(len, time) << std::endl; } catch (...) { // 资源清理 std::cout << " Func 函数中捕获到异常,释放数组内存: delete [] " << array << std::endl; delete[] array; throw; } std::cout << "正常情况释放数组内存: delete [] " << array << std::endl; delete[] array; } // 更上层函数,调用 Func 函数 void Upper() { try { Func(); } catch (...) { // 日志记录 std::cout << " Upper 函数中重新捕获到异常,输出提示信息后,再次重新抛出" << std::endl; throw; } } int main() { try { Upper(); } catch (const char* errmsg) { std::cout << " main 函数中最终捕获到异常: " << errmsg << std::endl; } return 0; }

Division函数中,"Division by zero condition!"是一个字符串字面量,C++ 中字符串字面量的类型就是const char* 。

const char* errmsg声明了一个名为errmsg的参数,errmsg会被初始化为抛出的异常对象,即指向"Division by zero condition!"字符串的指针。虽然errmsg是一个指针,但从功能上它可以看作是对抛出的异常对象的一种引用。通过该指针,我们可以访问和操作异常对象所代表的字符串内容。

2.4.3 异常安全

2.4.3.1 构造函数

构造函数用于对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或未完全初始化。

2.4.3.2 析构函数

析构函数主要用于资源清理,最好不要在析构函数中抛出异常,否则可能导致资源泄漏(如内存泄漏、句柄未关闭等)。

2.4.3.3 RAII

在 C++ 编程里,异常的出现容易引发资源泄漏问题。比如在执行newdelete操作时抛出异常,会造成内存泄漏;在lockunlock之间抛出异常,会导致死锁。

为有效解决这些问题,C++ 广泛采用 RAII(资源获取即初始化)技术。

关于 RAII 的详细内容,将在智能指针部分深入讲解。

2.4.4 异常规范

2.4.4.1 异常类型继承

抛出的异常类型应继承自同一个基类,这样能通过捕获基类异常来处理所有派生类异常。

2.4.4.2函数异常声明

函数需明确声明是否抛出异常以及可能抛出的异常类型。

  • 不抛出异常声明

throw()跟在函数后面,表示该函数不会抛出异常。

​ 返回类型 函数名(参数列表) throw() { // 函数体 }

C++11 引入noexcept关键字,置于函数后同样表明函数不会抛出异常。

返回类型 函数名(参数列表) noexcept { // 函数体 }
  • 无异常声明情况:若函数没有异常接口声明,意味着该函数可以抛出任何类型的异常。
  • 指定异常类型:在函数后面接throw (类型),列出该函数可能抛出的所有异常类型。
返回类型 函数名(参数列表) throw(异常类型1, 异常类型2, ...) { // 函数体 } // 如 void fun() throw(A,B,C,D) // 该函数会抛出A/B/C/D中的某种类型的异常

3. C++ 标准库的异常体系

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

异常描述
std::exception该异常是所有标准 C++ 异常的父类。
std::bad_alloc该异常可以通过 new 抛出。
std::bad_cast该异常可以通过 dynamic_cast 抛出。
std::bad_exception这在处理 C++ 程序中无法预期的异常时非常有用。
std::bad_typeid该异常可以通过 typeid 抛出。
std::logic_error理论上可以通过读取代码来检测到的异常。
std::domain_error当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument当使用了无效的参数时,会抛出该异常。
std::length_error当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator。
std::runtime_error理论上不可以通过读取代码来检测到的异常。
std::overflow_error当发生数学上溢时,会抛出该异常。
std::range_error当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error当发生数学下溢时,会抛出该异常。

4. 自定义异常体系

4.1 C++标准库/自定义异常体系的使用总结

  • 实际开发
    • 标准库异常体系:一些小型项目或对异常处理要求不是特别定制化的场景中较为常用。如,使用标准库容器(如std::vector)、智能指针等时,它们抛出的标准库异常(如std::out_of_range)能直接处理常见错误,方便快捷,且无需额外定义异常类。同时,在一些跨团队协作项目中,如果大家遵循统一规范,使用标准库异常体系可以让代码具有更好的通用性和可维护性。
    • 自定义异常体系:在大型项目、企业级开发中更为常用。很多公司会自己定义一套异常继承体系以规范异常管理。大型项目业务逻辑复杂,标准库异常难以精准描述业务层面错误。此外,在一些对代码架构和异常管理要求严格的项目中,自定义异常体系有助于规范异常抛出和捕获 。
  • 个人练习:前期多使用标准库异常体系,随着能力提升和练习项目复杂度增加,自定义异常体系使用会增多。

4.2 示例

服务器开发中常用的异常继承体系:

#include <iostream> #include <string> #include <thread> #include <chrono> #include <cstdlib> #include <ctime> class Exception { public: Exception(const std::string& errmsg, int id) : _errmsg(errmsg) , _id(id) {} virtual std::string what() const { return _errmsg; } protected: std::string _errmsg; int _id; }; class SqlException : public Exception { public: SqlException(const std::string& errmsg, int id, const std::string& sql) : Exception(errmsg, id) , _sql(sql) {} virtual std::string what() const { std::string str = "SqlException:"; str += _errmsg; str += "->"; str += _sql; return str; } private: const std::string _sql; }; class CacheException : public Exception { public: CacheException(const std::string& errmsg, int id) : Exception(errmsg, id) {} virtual std::string what() const { std::string str = "CacheException:"; str += _errmsg; return str; } }; class HttpServerException : public Exception { public: HttpServerException(const std::string& errmsg, int id, const std::string& type) : Exception(errmsg, id) , _type(type) {} virtual std::string what() const { std::string str = "HttpServerException:"; str += _type; str += ":"; str += _errmsg; return str; } private: const std::string _type; }; void SQLMgr() { std::srand(std::time(0)); if (std::rand() % 7 == 0) { throw SqlException("权限不足", 100, "select * from name = '张三'"); } //throw "xxxxxx"; } void CacheMgr() { std::srand(std::time(0)); if (std::rand() % 5 == 0) { throw CacheException("权限不足", 100); } else if (std::rand() % 6 == 0) { throw CacheException("数据不存在", 101); } SQLMgr(); } void HttpServer() { // ... std::srand(std::time(0)); if (std::rand() % 3 == 0) { throw HttpServerException("请求资源不存在", 100, "get"); } else if (std::rand() % 4 == 0) { throw HttpServerException("权限不足", 101, "post"); } CacheMgr(); } int main() { while (1) { std::this_thread::sleep_for(std::chrono::seconds(1)); try { HttpServer(); } catch (const Exception& e) // 这里捕获父类对象就可以 { // 多态 std::cout << e.what() << std::endl; } catch (...) { std::cout << "Unkown Exception" << std::endl; } } return 0; }

5. 异常的优劣

5.1 异常的优点

  • 相比返回错误码方式,异常对象定义好后能清晰准确地展示错误信息,甚至包含堆栈调用信息,利于更好地定位程序的 bug。
  • 返回错误码方式在函数调用链中,深层函数返回错误时,需层层返回错误,最外层才能拿到错误。而异常体系中,不管是哪个调用函数出错,抛出的异常会直接跳到main函数中catch捕获的地方,main函数可直接处理错误。

示例:

// 传统错误码方式 int ConnnectSql() { // 用户名密码错误 if (...) return 1; // 权限不足 if (...) return 2; return 0; } int ServerStart() { if (int ret = ConnnectSql() < 0) return ret; int fd = socket(); if (fd < 0) return errno; return 0; } int main() { if (ServerStart() < 0) // 处理错误 ; return 0; }
  • 很多第三方库都包含异常,如boostgtestgmock等常用库,使用它们需要使用异常。
  • 部分函数使用异常更好处理,例如构造函数没有返回值,不方便使用错误码方式处理;像T& operator[]这样的函数,若pos越界,只能使用异常或者终止程序,无法通过返回值表示错误。

5.2 异常的缺点

  • 异常会使程序的执行流乱跳,且非常混乱,运行时出错抛异常就会乱跳,这会增加跟踪调试和分析程序的难度。
  • 异常会有一些性能开销,但在现代硬件速度较快的情况下,该影响基本可忽略不计。
  • C++ 没有垃圾回收机制,资源需自己管理,有了异常容易导致内存泄漏、死锁等异常安全问题,需要使用 RAII 处理资源管理问题,学习成本较高。
  • C++ 标准库的异常体系定义得不好,导致大家各自定义异常体系,比较混乱。
  • 异常需规范使用,否则后果严重,随意抛异常会让外层捕获的用户难以处理。

5.3 总结

异常总体而言利大于弊,所以在工程中鼓励使用异常。

另外,面向对象的语言基本都用异常处理错误,这是大势所趋。

http://www.cnnetsun.cn/news/3483503.html

相关文章:

  • 麒麟linux系统lv_vg逻辑卷磁盘配置与扩展
  • 从%格式化到f-strings:Flynt让你的Python代码更简洁、更高效
  • 深入解析McBSP时钟与电源管理:从Smart Idle到低功耗实战
  • PowerShell自动化部署方案:winget-install一键安装Windows包管理器终极指南
  • MADRL中的深度强化学习算法:Policy定义与训练流程解析
  • AWS Service Operator 代码生成器揭秘:如何扩展支持新的 AWS 服务
  • 抓包大师(Sniff Master)与其他抓包工具对比分析
  • Learning OpenCV 4 Computer Vision with Python 3:初学者快速入门指南与安装教程
  • gosql REPL使用详解:交互式数据库操作的终极技巧
  • Tachometer 最佳实践:从团队协作到生产部署的完整流程
  • 为什么顶尖Go团队已弃用VS Code?Cursor for Go的4大不可替代性(附Benchmark数据对比)
  • Notepad--跨平台文本编辑器深度剖析:解决中文编码与多文件处理的实战指南
  • CANN/asc-devkit:int转half精度转换函数
  • CANN/asc-devkit函数语法限制
  • libMesh社区贡献指南:如何参与开源有限元项目开发
  • pwa-install部署实战:从零到一的完整PWA应用安装流程
  • 如何高效掌握Path of Building:流放之路角色构建的专业进阶指南
  • Huihui-gemma-4-12B-coder-fable5-composer2.5-v1-abliterated-4bit-msq常见问题解答:新手必知的8个关键知识点
  • 开源文本到视频模型实战:Wan 2.7、HunyuanVideo 1.5和LTX-2.3快速入门教程
  • CANN/asc-devkit HCCL服务端类型配置
  • maven_crate项目架构揭秘:10个技巧高效管理Apache Aurora、Hadoop等大型依赖
  • CANN/asc-devkit编程接口概述
  • 强力释放C盘空间:FreeMove如何实现无痛文件迁移
  • VGG-T³未来路线图:NVIDIA如何规划下一代3D视觉模型发展
  • 探索QQgroup-annual-report-analyzer:10个你可能不知道的实用功能
  • CANN/asc-devkit:HostCPU自定义算子UserDefined配置
  • TI 18xx芯片时钟管理与中断路由实战:从CCC到EPWM同步与DMMSWINT配置
  • C++继承机制深度解析:从语法到多态与设计模式实战
  • 深入解析TI C2000 SCI寄存器:从原理到实战的嵌入式串口通信指南
  • vue-timeago实战案例:10分钟内构建动态时间显示功能