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

【C++】C++ 的护身符:解锁 try-catch 异常处理

在这里插入图片描述

C++语法相关知识点可以通过点击以下链接进行学习一起加油!
命名空间缺省参数与函数重载C++相关特性类和对象-上篇类和对象-中篇
类和对象-下篇日期类C/C++内存管理模板初阶String使用
String模拟实现Vector使用及其模拟实现List使用及其模拟实现容器适配器Stack与QueuePriority Queue与仿函数
模板进阶-模板特化面向对象三大特性-继承机制面向对象三大特性-多态机制STL 树形结构容器二叉搜索树
AVL树红黑树红黑树封装map/set哈希-开篇闭散列-模拟实现哈希
哈希桶-模拟实现哈希哈希表封装 unordered_map 和 unordered_setC++11 新特性:序章右值引用、移动语义、万能引用实现完美转发可变参数模板与emplace系列
Lambda表达式、包装器与绑定的应用

这篇文章深入探讨了 C++ 中的异常处理机制,尤其是通过 try-catch 结构来管理运行时错误。文章首先回顾了 C 语言中常见的错误处理方式,然后通过形象的外卖点餐场景,帮助读者理解不同类型错误的合理应对方式。接着,文章详细介绍了 C++ 异常的基本概念和使用方法,解析了 throw、try 和 catch 关键字的作用,并探讨了如何在函数调用链中优雅地处理异常。最后,还提出了自定义异常体系和 C++ 标准库的异常处理方式。

文章目录

  • 一、C 语言中传统的错误处理方式
  • 二、用“外卖点餐”来理解错误处理
    • 2.1 情况一 | 客户端错误
    • 2.2 情况二 | 程序自身的问题
    • 2.3 情况三 | 环境问题
  • 三、C++异常处理简介
    • 3.1 三个关键字:try / throw / catch
  • 四、异常的使用
    • 4.1 异常的抛出与匹配规则
      • 4.1.1 throw可以抛出任意类型的对象
      • 4.1.2 异常处理的“就近原则”
      • 4.1.3 找不到匹配的 catch
      • 4.1.4 异常对象的拷贝机制
      • 4.1.5 catch(...) 与未捕获异常的处理
    • 4.2 在函数调用链中异常栈展开匹配原则
    • 4.3 异常的重新抛出
    • 4.4 异常安全
      • 4.4.1 抛异常出现内存泄漏
      • 4.4.2 异常安全的基本原则
    • 4.5 异常规范(Exception Specification)
  • 五、自定义异常体系
  • 六、C++标准库的异常体系
    • 6.1 std::exception异常继承实操
  • 七、 C++ 异常的优缺点
    • 7.1 异常优点
    • 7.2 异常缺点

一、C 语言中传统的错误处理方式

  1. 终止程序(如 assert
  • 优点:适用于开发阶段快速发现严重错误。
  • 缺点:用户体验差,程序在运行时一旦遇到严重错误(如内存访问违规、除以零),会立即终止,难以接受。
  1. 返回错误码(如通过 errno
  • 优点:灵活,允许程序继续运行,适合错误可恢复的场景。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。
  • 缺点:程序员需手动检查返回值并查找错误码含义,增加了开发复杂度。

在实际开发中,C 语言主要采用返回错误码的方式进行错误处理。对于一些致命错误(如数组越界),虽然属于运行时行为,但如果被编译器静态检查到,往往会强制终止程序以避免更严重的问题。

二、用“外卖点餐”来理解错误处理

想象你正在用手机点外卖,点的是一份奶茶。整个点餐的过程就像一个运行中的程序,每一个操作(比如选择口味、下单、付款)都可能出错。如果出错了就直接关闭整个APP,那你肯定会觉得这个软件太差劲了,对吧?

所以我们来看看,具体会遇到哪些“错误”,程序应该怎么合理地处理它们。

2.1 情况一 | 客户端错误

场景】:余额不足,付款失败

你下单准备付款,但微信钱包里没钱了。如果这时程序直接崩溃或退出,那你可能连点别的奶茶的机会都没有了,非常不合理。

正确做法】:提示“余额不足”,引导你去充值或者换付款方式。 这个错误是可以预料并处理的客户端错误

2.2 情况二 | 程序自身的问题

场景】:点击“支付”按钮没有反应

你点了“支付”,但是页面没动。这时候可能是程序写得有问题,按钮绑定错了,或者后端接口挂了。你作为用户也许看不懂程序错误日志,但开发者需要知道这里出问题了。

正确做法】:程序不崩溃,但把这个错误悄悄记录到日志里,方便程序员以后排查。

2.3 情况三 | 环境问题

【场景】::网络不好,订单没发出去

你在地铁里网不好,付款的时候一直转圈圈。程序应该不会立刻提示“失败”,而是先尝试重新连接几次,实在不行了,再告诉你“网络连接失败,请稍后重试”。

正确做法】:设置重试机制,允许等待→重连→最终提示失败这样的流程。属于环境导致的问题,可以尝试恢复处理

一个健壮的程序,应该像一个优秀的服务员,面对突发状况不会慌张,而是冷静地根据情况选择恰当的应对策略。

三、C++异常处理简介

异常(Exception)是一种用于处理程序运行中出现错误的机制。当一个函数在执行过程中发现自己无法处理的问题时,它可以通过抛出异常来将错误交由其调用者(直接或间接)处理。

3.1 三个关键字:try / throw / catch

【throw | 抛出异常】

当程序检测到某个问题无法继续执行时,可以使用 throw 语句将异常抛出。

可以抛出任何类型的异常对象(如整数、字符串、自定义类等)。

【try | 捕获尝试】

将可能抛出异常的代码块包裹在 try 块中。如果在该块中抛出了异常,程序会跳转到匹配的 catch 块执行。

【catch | 捕获处理】

用于处理 try 块中抛出的异常。可以定义多个 catch 块,分别捕获不同类型的异常。

如果没有匹配的 catch 块,异常将继续向上传递,直到被捕获或导致程序终止。

try
{//  受保护的代码:这里放可能抛出异常的语句
}
catch (const ExceptionType1& e)   // ← 捕获第 1 类异常
{// TODO: 针对 ExceptionType1 的处理
}
catch (const ExceptionType2& e)   // ← 捕获第 2 类异常
{// TODO: 针对 ExceptionType2 的处理
}
// ...
catch (const ExceptionTypeN& e)   // ← 捕获第 N 类异常
{// TODO: 针对 ExceptionTypeN 的处理
}
/* 可选:兜底捕获,防止漏网之鱼
catch (...)                     
{// TODO: 处理所有未被前面 catch 捕获的异常
}
*/

四、异常的使用

4.1 异常的抛出与匹配规则

4.1.1 throw可以抛出任意类型的对象

C++ 编译器在运行时会根据你 throw 的对象的类型,去调用链中寻找第一个匹配的 catch 块。匹配规则和函数参数传递类似,是基于类型兼容的匹配。

在这里插入图片描述

关键理解】:

  1. throw 后面的对象类型决定了哪个 catch 能处理它。
  2. catch 是类型敏感的,不支持自动类型转换,例如 throw 3.14 无法被 catch(int) 捕获。
  3. 类型可以是引用,也可以是对象,但推荐使用 const 引用 以避免拷贝。

4.1.2 异常处理的“就近原则”

当程序中通过 throw 抛出一个异常时,C++ 会在调用栈中自下而上寻找一个类型匹配的catch块来处理这个异常。第一个匹配成功的 catch 块将会被激活,其他的将被忽略。

就近原则】 :异常总是由“离抛出位置最近、类型匹配”的 catch 块处理。

场景】:多个函数嵌套,异常向上传播直到就近匹配

void inner()
{throw std::string("Error: file not found");
}
void middle()
{inner(); // 没有 try-catch,异常会继续向上传播
}
void outer()
{try{middle();}catch (const  std::string& e){std::cout << "Caught string in outer(): " << e << '\n';}
}
int main()
{outer();
}

main → outer() → middle() → inner()
↑ ↑
try-catch? throw

4.1.3 找不到匹配的 catch

  1. 异常会沿着调用栈一路向上传播
  2. 如果直到 main() 都没人处理,程序会调用 std::terminate() 立即崩溃
  3. 因此,建议在最外层程序入口处设置兜底的 catch (…)来防止程序异常退出

4.1.4 异常对象的拷贝机制

在 C++ 中,使用 throw 抛出异常对象时,通常会发生一次对象的拷贝或移动。这是因为异常对象的生命周期需要延长:从 throw 抛出开始,直到被 catch 块捕获并处理完毕。

这种处理方式类似于函数的按值传参和返回过程。所幸在现代 C++ 中,如果异常类型支持右值引用和移动构造(例如 std::string),那么这一步通常会通过移动构造完成,几乎不会带来额外的深拷贝开销。

4.1.5 catch(…) 与未捕获异常的处理

在 C++ 中,catch (...) 是一个特殊的捕获形式,它可以捕获任何类型的异常,无论是基本类型、标准库对象,还是用户自定义类型。

在这里插入图片描述

结果:虽然不知道是 double 类型,但异常不会导致程序崩溃。

局限性】:catch(...) 无法提供异常的具体信息,也无法访问抛出的对象,因此你无法得知异常的类型或内容,只能作通用处理或记录。

【使用建议】:catch(...) 可以作为异常处理的兜底机制,用于捕获所有类型的异常,防止程序异常崩溃。但它无法获取异常的具体信息,不能做有针对性的处理,因此不应过度依赖

建议仅在程序的最外层(如 main 函数或线程入口)使用 catch(...) 做统一的日志记录或友好退出,而在正常逻辑中,应优先使用类型明确的 catch 来处理已知异常类型。

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

当异常被抛出时,程序会先检查 throw 是否在 try 块内部,若是,则沿调用链向上查找匹配的 catch。如果当前函数没有匹配的 catch,则退出当前函数,继续在上层函数中查找,这个过程称为栈展开

在这里插入图片描述

如果一直到 main 函数都找不到匹配的处理代码,程序将被终止。因此,实际中建议在程序入口处添加 catch(...) 来兜底异常。异常一旦被捕获,程序将从对应的 catch 块后继续执行。

4.3 异常的重新抛出

在实际开发中,一个 catch 块可能无法完全处理某个异常,此时可以先进行局部处理(如日志记录、资源清理等),然后将异常重新抛出,交由更高层的调用者继续处理。

这称为异常的重新抛出,通常用于确保异常信息不会被吞掉,且能得到更合适的处理。

【示例演示】:

void inner()
{throw std::string("inner error");
}
void middle()
{try {inner();}catch (const std::string& e){std::cout << "main() caught: " << e << '\n';throw;}
}
int main()
{try {middle();}catch (const std::string& e){std::cout << "main() caught again: " << e << '\n';}
}

在这里插入图片描述

使用要点】:

  • 使用 throw;(不带对象)可以将当前捕获的异常原样抛出
  • 重新抛出前可以做一些局部处理(如资源释放、防止内存泄漏);
  • 不建议用 throw e;(抛出变量),那样会复制异常对象,可能丢失原始类型信息。

4.4 异常安全

4.4.1 抛异常出现内存泄漏

示例演示】:

double Division(int len, int time)
{if (time == 0){throw "除0错误";}return (double)len / (double)time;
}void Func()
{int* array1 = new int[10];  // 动态分配资源try{int len, time;cin >> len >> time;cout << Division(len, time) << endl;}catch (const char* errmsg){cout << "delete [] " << array1 << endl;delete[] array1;        // 清理资源throw errmsg;           // 重新抛出异常}cout << "delete [] " << array1 << endl;delete[] array1;            // 正常释放资源
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}return 0;
}

分析说明

  1. 如果 Division 抛出异常,程序会跳转到 catch 块;

  2. catch 中先释放了动态数组,再用 throw 将异常重新抛出;

  3. 如果没有 catch 块手动释放,程序跳过后面的 delete[],就会导致内存泄漏

这说明:在异常发生前分配的资源,如果不能在异常路径上正确释放,就会造成资源泄露,这就是典型的异常安全问题。这个处理方法相对来说并不能解决本质问题,如果有多个这种这种情况,就得做多次处理。

4.4.2 异常安全的基本原则

  1. 构造函数中尽量避免抛出异常,否则对象可能未完全构造,使用时容易出错。
  2. 析构函数中不要抛出异常,否则在对象销毁过程中可能导致资源无法正确释放。
  3. C++ 中异常容易导致资源泄漏,例如 new 后异常未能 delete,或 lock 后异常未能 unlock,严重时会造成内存泄漏或死锁。
  4. 推荐使用 RAII(资源获取即初始化)思想,用对象生命周期管理资源,如使用智能指针和锁管理类,自动释放资源,避免人为遗漏。

4.5 异常规范(Exception Specification)

异常规范用于声明一个函数可能抛出哪些异常类型,目的是让函数调用者有所预期。但需要注意:C++ 的异常规范不是强制机制,而是一种“道德规范”

常见形式】:

// 这里表示这个函数会抛出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;

潜在问题】:

尽管 C++ 提供了异常规范(如 throw()noexcept),但在实际中它们更像是一种“道德约定”,而非强制规则

  1. 即使你声明函数不抛异常,编译器通常也不会严格检查,因为完整分析调用链的成本很高,尤其在大型工程中几乎不可行。
  2. 所谓“道德规范”,是指语言设计者假设开发者会自觉遵守规范,但并不会强制执行。然而,现实中总有人违反规则。

解决措施】:

在复杂项目中,异常规范往往难以写全、写对。为简化这类问题,C++11 引入了 noexcept,统一异常声明风格:

  • 使用 noexcept 表示函数不会抛异常
  • 如果不写,默认该函数可能抛异常。

在这里插入图片描述

五、自定义异常体系

在实际工程开发中,异常处理非常常见,但也容易出现以下问题:

  1. 项目庞大、多人协作,异常风格难以统一
  2. C++ 中 throw 可以抛出任意类型,如果缺乏规范,容易出现异常未被正确捕获,导致程序崩溃;
  3. 随意抛出各种类型的异常,调用者难以处理、问题难以定位,调试成本高

为了解决这些问题,很多公司或大型项目会选择自定义异常体系,统一管理异常行为。

常见做法】:

  • 定义一个统一的异常基类,如 BaseException
  • *大家抛出的都是继承的派生类对象,捕获一个基类即可
  • 同时可通过多态机制,获取具体的错误信息或类型。

在这里插入图片描述

示例演示】:

#include <iostream>
#include <string>
#include <sstream>
#include <cstdlib>
#include <ctime>
#include <windows.h>using namespace std;// 通用异常基类
class Exception {
public:Exception(const string& errmsg, int id): _errmsg(errmsg), _id(id) {}virtual string what() const {return _errmsg;}protected:string _errmsg;int _id;
};// SQL 异常
class SqlException : public Exception {
public:SqlException(const string& errmsg, int id, const string& sql): Exception(errmsg, id), _sql(sql) {}string what() const override {ostringstream oss;oss << "SqlException: " << _errmsg << " -> " << _sql;return oss.str();}private:string _sql;
};// 缓存异常
class CacheException : public Exception {
public:CacheException(const string& errmsg, int id): Exception(errmsg, id) {}string what() const override {return "CacheException: " + _errmsg;}
};// HTTP 异常
class HttpServerException : public Exception {
public:HttpServerException(const string& errmsg, int id, const string& type): Exception(errmsg, id), _type(type) {}string what() const override {ostringstream oss;oss << "HttpServerException: [" << _type << "] " << _errmsg;return oss.str();}private:string _type;
};// 模拟 SQL 服务
void SQLMgr() {if (rand() % 7 == 0) {throw SqlException("权限不足", 100, "SELECT * FROM users WHERE name = '张三'");}
}// 模拟缓存服务
void CacheMgr() {if (rand() % 5 == 0) {throw CacheException("缓存权限不足", 200);} else if (rand() % 6 == 0) {throw CacheException("缓存中找不到数据", 201);}SQLMgr();
}// 模拟 HTTP 服务
void HttpServer() {if (rand() % 3 == 0) {throw HttpServerException("请求资源不存在", 300, "GET");} else if (rand() % 4 == 0) {throw HttpServerException("访问权限不足", 301, "POST");}CacheMgr();
}// 主函数:统一捕获异常
int main() {srand((unsigned int)time(0));while (true) {Sleep(500); // 模拟服务器循环处理请求try {HttpServer();}catch (const Exception& e) {cout << "捕获异常: " << e.what() << endl;}catch (...) {cout << "未知异常发生" << endl;}}return 0;
}

在这里插入图片描述

六、C++标准库的异常体系

C++ 标准库提供了一套定义在 <exception> 头文件中的标准异常类,这些异常按照继承关系组织成一个层次结构。我们可以在程序中直接使用这些标准异常类型来处理常见错误。

下面是 C++ 标准异常类的继承体系结构图:

在这里插入图片描述

在这里插入图片描述

6.1 std::exception异常继承实操

实际上,我们也可以通过继承 std::exception 来实现自己的异常类。但在实际工程中,很多公司更倾向于像前面那样自定义一套异常继承体系,这是因为 C++ 标准库提供的异常类在功能上相对简单,难以满足复杂系统的需求。

因此,这里我们不再展开对标准异常的介绍,下面给出一个简单的测试代码作为对比。

示例演示】:

#include <iostream>
#include <exception>void LoadFile(const std::string& filename) {// 抛出一个标准异常throw std::runtime_error("File not found: " + filename);
}int main() {try {LoadFile("config.json");} catch (const std::exception& e) {// 只能获取字符串描述,无法区分错误类型、来源模块等std::cout << "Error: " << e.what() << std::endl;}return 0;
}

分析代码】:

  • [std::runtime_error] :这就创建了一个异常对象,内部保存了这个字符串,程序就中断并进入 try-catch 流程。
  • [e.what()] : e.what()std::exception 类中的一个 虚成员函数,返回一个 const char* 类型的字符串,表示异常的描述信息。

【统一接口捕获,利用多态的特性】:

catch (const std::exception& e)

你是用引用捕获异常对象,无论你抛的是 std::runtime_errorstd::logic_error 还是其他 std::exception 的子类,都能用这个统一的接口来获取错误信息。

七、 C++ 异常的优缺点

7.1 异常优点

  1. 信息表达清晰

异常对象可以封装丰富的错误信息,相比传统错误码方式更具表达力,甚至可包含堆栈信息,便于定位 Bug。

  1. 避免层层传递错误码

传统错误处理需要在函数调用链中逐层返回错误码,写法冗余且易出错;而异常机制能自动中断执行流,直接跳转到最外层 catch 块,简化了错误处理逻辑。

int ConnectSql() {if (...) return 1; // 用户名错误if (...) return 2; // 权限不足
}int ServerStart() {if (int ret = ConnectSql() < 0)return ret;int fd = socket();if (fd < 0)return errno;
}int main() {if (ServerStart() < 0)... // 错误处理
}

若使用异常,出错可直接跳转至 main() 中的 catch,无需逐级传递。

  1. 与第三方库兼容性好

很多主流 C++ 库(如 Boost、gtest、gmock 等)都广泛使用异常机制,使用这些库时也必须具备异常处理能力。

  1. 适用于无法返回错误码的场景

某些函数(如构造函数、重载 operator[] 等)无法通过返回值传递错误,使用异常能更自然地处理错误情况。

7.2 异常缺点

  1. 执行流乱跳,调试困难
    异常会在程序运行时打断正常的控制流,使得执行流程变得混乱。这使得调试和分析程序变得困难,尤其是在异常传播较深时,定位问题较为复杂。

  2. 性能开销
    异常机制会带来一定的性能开销。虽然在现代硬件上,这种开销已经微乎其微,但在高性能要求的场景(如游戏开发或实时系统)中,仍然需要谨慎使用。

  3. 【资源管理复杂,易导致内存泄漏】
    C++ 没有垃圾回收机制,资源的管理完全依赖开发者。在异常机制下,如果资源管理不当,可能导致内存泄漏、死锁等问题。因此,必须使用 RAII(资源获取即初始化)来保证资源的正确管理,这增加了学习成本。

  4. 【C++ 标准库的异常体系设计不够完善】
    C++ 标准库的异常体系较为简化,无法提供更详细的错误信息(如错误码、模块来源等),导致开发者往往需要自定义异常体系,这样的做法使得异常处理在不同项目间变得更加混乱。

  5. 【异常使用规范不明确,可能导致维护困难】
    异常机制需要严格规范使用,否则会增加维护的难度。随意抛出异常或不合理的异常设计,可能使得外层捕获异常的用户遭遇极大的困扰。为了避免这种问题,应遵循以下两点规范:

  • 抛出的所有异常类型应统一继承自一个基类
  • 函数是否抛出异常、抛出什么异常,应使用 noexcept 明确标注。
http://www.xdnf.cn/news/18332.html

相关文章:

  • 【HarmonyOS】应用设置全屏和安全区域详解
  • 【机器人-基础知识】ROS2常用命令
  • MongoDB 查询方法与高级查询表(Python版)
  • 计算机网络技术学习-day3《交换机配置》
  • steal tsoding‘s pastebeam code as go server
  • SQL详细语法教程(五)事务和视图
  • ubuntu 下载安装tomcat简单配置(傻瓜式教程)
  • 如何生成和安全保存私钥?
  • 信号上升时间Tr不为0的信号反射情况
  • scikit-learn/sklearn学习|弹性网络ElasticNet解读
  • linux系统查看ip命令
  • 深度学习与线性模型在扰动预测上的比较
  • kafka 冲突解决 kafka安装
  • 如何在VS Code中使用Copilot与MCP服务器增强开发体验
  • 【Linux操作系统】简学深悟启示录:进程状态优先级
  • Android RxBinding 使用指南:响应式UI编程利器
  • 数据转换细节揭秘:ETL如何精准映射复杂业务逻辑
  • 27.Linux 使用yum安装lamp,部署wordpress
  • 【自动化测试】Selenium详解-WebUI自动化测试
  • Linux: RAID(磁盘冗余阵列)配置全指南
  • 作业标准化:制造企业的效率基石与品质保障
  • 可编辑150页PPT | 某制造集团产业数字化转型规划方案
  • idea部署到docker
  • 【MyBatis-Plus】一、快速入门
  • kafka 发送消息有哪些模式?各自的应用场景是什么?
  • 秋招笔记-8.17
  • Java 学习笔记(基础篇5)
  • 【OpenAI】 GPT-4o-realtime-preview 多模态、实时交互模型介绍+API的使用教程!
  • 宋红康 JVM 笔记 Day05|运行时数据区内部结构、JVM中的线程说明、程序计数器
  • RAID服务器