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 编程里异常的出现容易引发资源泄漏问题。比如在执行new和delete操作时抛出异常会造成内存泄漏在lock和unlock之间抛出异常会导致死锁。为有效解决这些问题C 广泛采用 RAII资源获取即初始化技术。关于 RAII 的详细内容将在智能指针部分深入讲解。2.4.4 异常规范2.4.4.1 异常类型继承抛出的异常类型应继承自同一个基类这样能通过捕获基类异常来处理所有派生类异常。2.4.4.2函数异常声明函数需明确声明是否抛出异常以及可能抛出的异常类型。不抛出异常声明throw()跟在函数后面表示该函数不会抛出异常。​ 返回类型 函数名(参数列表) throw() { // 函数体 }C11 引入noexcept关键字置于函数后同样表明函数不会抛出异常。返回类型 函数名(参数列表) noexcept { // 函数体 }无异常声明情况若函数没有异常接口声明意味着该函数可以抛出任何类型的异常。指定异常类型在函数后面接throw (类型)列出该函数可能抛出的所有异常类型。返回类型 函数名(参数列表) throw(异常类型1, 异常类型2, ...) { // 函数体 } // 如 void fun() throw(ABCD) // 该函数会抛出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; }很多第三方库都包含异常如boost、gtest、gmock等常用库使用它们需要使用异常。部分函数使用异常更好处理例如构造函数没有返回值不方便使用错误码方式处理像T operator[]这样的函数若pos越界只能使用异常或者终止程序无法通过返回值表示错误。5.2 异常的缺点异常会使程序的执行流乱跳且非常混乱运行时出错抛异常就会乱跳这会增加跟踪调试和分析程序的难度。异常会有一些性能开销但在现代硬件速度较快的情况下该影响基本可忽略不计。C 没有垃圾回收机制资源需自己管理有了异常容易导致内存泄漏、死锁等异常安全问题需要使用 RAII 处理资源管理问题学习成本较高。C 标准库的异常体系定义得不好导致大家各自定义异常体系比较混乱。异常需规范使用否则后果严重随意抛异常会让外层捕获的用户难以处理。5.3 总结异常总体而言利大于弊所以在工程中鼓励使用异常。另外面向对象的语言基本都用异常处理错误这是大势所趋。

相关新闻

Cursor重构工作流升级手册(JetBrains用户紧急切换必读)

Cursor重构工作流升级手册(JetBrains用户紧急切换必读)

更多请点击: https://intelliparadigm.com 第一章:Cursor重构工作流升级导论 现代前端开发正加速向智能化、上下文感知的协作范式演进。Cursor 作为深度集成 LLM 的代码编辑器,其重构能力已从基础符号替换跃迁至语义级意图理解与跨文件影响分…

2026/7/18 10:32:05阅读更多 →
Matterbridge开发详解:从环境搭建到插件开发全流程

Matterbridge开发详解:从环境搭建到插件开发全流程

Matterbridge开发详解:从环境搭建到插件开发全流程 【免费下载链接】matterbridge Matterbridge plugin manager for Matter 项目地址: https://gitcode.com/gh_mirrors/mat/matterbridge Matterbridge是一款强大的Matter协议插件管理工具,能够帮…

2026/7/18 10:32:05阅读更多 →
昇腾C DoubleBuffer性能优化技术原理

昇腾C DoubleBuffer性能优化技术原理

DoubleBuffer 【免费下载链接】asc-devkit 本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言,原生支持C和C标准规范,主要由类库和语言扩展层构成,提供多层级API,满足多维场景算子开发诉求。 项目地址: https://gitcode.c…

2026/7/18 10:32:05阅读更多 →
Go-Fed Activity安全实践:保护你的联邦应用免受攻击的10个技巧

Go-Fed Activity安全实践:保护你的联邦应用免受攻击的10个技巧

Go-Fed Activity安全实践:保护你的联邦应用免受攻击的10个技巧 【免费下载链接】activity ActivityStreams & ActivityPub in golang, oh my! 项目地址: https://gitcode.com/gh_mirrors/ac/activity Go-Fed Activity是一个强大的Go语言库,用…

2026/7/18 11:42:56阅读更多 →
MCAN模块与CAN FD协议:嵌入式实时通信的核心架构与工程实践

MCAN模块与CAN FD协议:嵌入式实时通信的核心架构与工程实践

1. MCAN模块与CAN FD协议:现代分布式实时控制系统的通信基石在汽车电子、工业自动化乃至航空航天这些对可靠性和实时性要求近乎苛刻的领域,控制器局域网(Controller Area Network, CAN)总线早已成为神经系统般的存在。作为一名长期…

2026/7/18 11:42:56阅读更多 →
深入解析TI CC35xx无线MCU:基于Cortex-M33的物联网安全与高性能设计

深入解析TI CC35xx无线MCU:基于Cortex-M33的物联网安全与高性能设计

1. 项目概述在嵌入式物联网和工业控制领域,选对一颗微控制器(MCU)往往意味着项目成功了一半。今天,我想和大家深入聊聊德州仪器(TI)的CC35xx系列无线MCU,特别是其核心——基于Arm Cortex-M33的架…

2026/7/18 11:42:56阅读更多 →
LVDS/CSI-2接口CBUFF阈值寄存器配置实战与优化

LVDS/CSI-2接口CBUFF阈值寄存器配置实战与优化

1. 项目概述与核心挑战在嵌入式图像处理和数据采集系统中,LVDS和MIPI CSI-2接口是连接传感器与处理器、或处理器与显示器的关键桥梁。我最近在调试一块基于TI Jacinto系列处理器的工业相机模组时,就深刻体会到了这一点。项目要求实现从图像传感器到处理器…

2026/7/18 11:42:56阅读更多 →
如何快速实现AI视频创作自动化:ComfyUI视频处理终极指南

如何快速实现AI视频创作自动化:ComfyUI视频处理终极指南

如何快速实现AI视频创作自动化:ComfyUI视频处理终极指南 【免费下载链接】ComfyUI-VideoHelperSuite Nodes related to video workflows 项目地址: https://gitcode.com/gh_mirrors/co/ComfyUI-VideoHelperSuite 你是否曾经为AI生成的精美图像序列无法变成流…

2026/7/18 11:42:56阅读更多 →
终极指南:5分钟掌握Zotero中文文献管理神器茉莉花插件

终极指南:5分钟掌握Zotero中文文献管理神器茉莉花插件

终极指南:5分钟掌握Zotero中文文献管理神器茉莉花插件 【免费下载链接】jasminum A Zotero add-on to retrive CNKI meta data. 一个简单的Zotero 插件,用于识别中文元数据 项目地址: https://gitcode.com/gh_mirrors/ja/jasminum 你是否曾经为Zo…

2026/7/18 11:37:50阅读更多 →
VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异当你在VSCode中启动一个新的TypeScript项目时,第一个技术决策往往从安装方式开始。这个看似简单的选择——全局安装还是项目本地安装——实际上会深刻影响你的开发流程、团队协作和…

2026/7/18 10:49:13阅读更多 →
智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手 【免费下载链接】zhihuishu 智慧树刷课插件,自动播放下一集、1.5倍速度、无声 项目地址: https://gitcode.com/gh_mirrors/zh/zhihuishu 智慧树刷课插件是一款专为智慧树在线教育平台设计的Chrome浏…

2026/7/18 8:49:08阅读更多 →
Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案 【免费下载链接】WorkshopDL WorkshopDL - The Best Steam Workshop Downloader 项目地址: https://gitcode.com/gh_mirrors/wo/WorkshopDL 你是否在GOG或Epic Games Store购买了心仪的游戏…

2026/7/17 13:22:23阅读更多 →
从模糊意图到可执行指令:Claude PRD中Prompt Engineering与需求颗粒度的5级映射法则

从模糊意图到可执行指令:Claude PRD中Prompt Engineering与需求颗粒度的5级映射法则

更多请点击: https://kaifayun.com 第一章:从模糊意图到可执行指令:Claude PRD中Prompt Engineering与需求颗粒度的5级映射法则 在Claude驱动的产品需求文档(PRD)生成实践中,原始业务意图往往以自然语言片…

2026/7/18 0:00:14阅读更多 →
Cursor配置生成失效?3大隐藏陷阱+4行修复代码,资深工程师连夜整理的紧急补救清单

Cursor配置生成失效?3大隐藏陷阱+4行修复代码,资深工程师连夜整理的紧急补救清单

更多请点击: https://codechina.net 第一章:Cursor配置生成失效?3大隐藏陷阱4行修复代码,资深工程师连夜整理的紧急补救清单 Cursor 配置生成突然失效,是近期高频报障场景。表面看是 cursor.config.json 未更新或 LSP…

2026/7/18 0:00:14阅读更多 →
某智驾大牛创业

某智驾大牛创业

作者:钟声编辑:Mark出品:红色星际头图:智能驾驶图片据悉,国内某头部智驾公司端到端模型技术大牛Z投身创业,并且已经拿到融资。Z不仅是该头部公司内部最年轻的对标阿里P10级别技术负责⼈,更是业内…

2026/7/18 0:00:14阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/17 22:48:46阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/17 13:22:38阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/17 17:26:50阅读更多 →