
往期《C++初阶》回顾:《C++初阶》目录导航
往期《C++进阶》回顾:
/------------ 继承多态 ------------/
【普通类/模板类的继承 + 父类&子类的转换 + 继承的作用域 + 子类的默认成员函数】
【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】
【多态:概念 + 实现 + 拓展 + 原理】
/------------ STL ------------/
【二叉搜索树】
【AVL树】
【红黑树】
【set/map 使用介绍】
【set/map 模拟实现】
【哈希表】
【unordered_set/unordered_map 使用介绍】
【unordered_set/unordered_map 模拟实现】
/------------ C++11 ------------/
【列表初始化 + 右值引用】
【移动语义 + 完美转发】
【可变参数模板 + emplace接口 + 新的类功能】
【lambda表达式 + 包装器】
Hi~ 小伙伴们大家好呀!(ノ≧∀≦)ノ♪ 今天已经是国庆叠加中秋的 8 天小长假第 4 天啦~不知道大家这几天是在出门游玩 🚗✧、宅家充电 📚✧,还是补觉回血 😴✧ 呢? 不管是哪种状态,都祝大家玩得尽兴、学得轻松、睡得踏实(〃’▽’〃)!,好好享受这段难得的假期时光!♪(´▽`)♪
不过放松之余,咱们的 C++ 学习也别断档哦~╰(▔∀▔)╯ 今天就接着往下学,带大家认识一个超重要的新知识点 ——【异常】!✧(≖ ‸ ≖✿)! 掌握它能帮咱们更好地处理代码里的突发情况,赶紧一起开始吧!✧。٩(ˊᗜˋ)و✧*。
异常处理(Exception Handling):是一种处理程序运行时错误的机制,它允许程序在检测到错误时跳转到专门的错误处理代码,而不是 直接崩溃或产生不可预测的行为
异常处理的基本概念:
C++ 异常处理通过三个关键字实现:
try:标识可能抛出异常的代码块catch:捕获并处理特定类型的异常throw:抛出一个异常1. throw:抛出异常
当程序检测到错误时,使用throw关键字抛出一个异常(可以是任意类型的值,如:基本类型、自定义类型等)
if (divisor == 0)
{
throw "除数不能为0"; // 抛出字符串类型的异常
}2. try:监控可能抛出异常的代码块
try块包裹可能抛出异常的代码,当其中的代码抛出异常时,程序会跳转到对应的catch块处理
try
{
// 可能抛出异常的代码
}3. catch:捕获并处理异常
catch块用于捕获try块中抛出的异常,每个catch块指定其能处理的异常类型
当try块中抛出异常时,程序会寻找第一个匹配类型的catch块执行
catch (异常类型 变量名)
{
// 异常处理逻辑
}异常处理的基本语法:
/*-------------------------异常处理的基本语法-------------------------*/
/* 说明:
* 1. error_condition:错误条件
* 2. exception_type:异常类型
*/
try
{
// 可能抛出异常的代码
if (error_condition)
{
throw exception_type("Error message");
}
}
catch (const exception_type& e)
{
// 处理异常
std::cerr << "Caught exception: " << e.what() << std::endl;
}
catch (...)
{
// 捕获所有其他类型的异常
std::cerr << "Unknown exception caught" << std::endl;
}异常处理的执行流程:
try块中的代码try块中没有异常抛出,catch块会被跳过,继续执行后续代码try块中通过throw抛出异常,程序立即终止try块的执行,寻找最近的、类型匹配的catch块catch块中的错误处理代码catch块之后的代码代码示例:异常处理的使用
#include <iostream>
using namespace std;
/*------------------------ 除法函数 ------------------------*/
double divide(int num, int divisor)
{
if (divisor == 0) // 当除数为0时抛出异常
{
throw "错误:除数不能为0"; //注:抛出异常(此处为字符串类型)
}
return (double)num / divisor;
}
/*------------------------ 主函数 ------------------------*/
int main()
{
//1.定义三个变量用于计算和保存结果
int a = 10, b = 0;
double result;
//2.监控可能抛出异常的代码
try
{
//2.1:
result = divide(a, b);
//2.2:
cout << "结果:" << result << endl; //注意:若divide抛出异常,这行代码不会执行
}
//3.捕获字符串类型的异常
catch (const char* errorMsg)
{
cout << "捕获到异常:" << errorMsg << endl;
}
//4.异常处理后,程序继续执行
cout << "程序继续运行..." << endl;
return 0;
}
异常可以是任意类型,包括:
基本类型(如:int、double)字符串(如:const char*、std::string)自定义类(推荐用于复杂异常信息)代码示例:自定义类类型的异常
#include <iostream>
#include <string>
using namespace std;
/*---------------------------自定义异常类---------------------------*/
class DivideError : public exception //注:继承自标准库的exception类,可以兼容标准异常处理机制
{
private:
string msg; // 用于存储异常的详细信息
public:
//1.实现:“构造函数”
DivideError(string message)
: msg(message)
{ }
/* 说明:
* 1. 接收异常信息字符串并初始化成员变量
* 2. 参数message:描述异常原因的字符串
*/
//2. 重写exception类的what()方法
const char* what() const noexcept override
{
return msg.c_str(); // 将string转换为C风格字符串返回
}
/* 说明:
* 1. 该方法返回异常的描述信息,是异常处理的标准接口
* 2. const noexcept:保证该方法不会修改对象状态,也不会抛出异常
*/
};
/*---------------------------除法函数---------------------------*/
double divide(int num, int divisor)
{
//1.检查除数是否为0,如果是则抛出异常
if (divisor == 0)
{
//1.1:抛出自定义异常对象,包含具体的错误信息
throw DivideError("除数不能为0(自定义异常)");
}
//2.执行正常的除法运算并返回结果
return (double)num / divisor;
}
int main()
{
//1.try块:包含可能抛出异常的代码
try
{
//1.1:调用divide函数
cout << divide(10, 0) << endl; //注:此处传入除数0,会触发异常
//1.2:输出提示信息
cout << "除法运算完成" << endl; //注:如果上面的函数调用抛出异常,下面的代码不会执行
}
//2.catch块:捕获并处理特定类型的异常
catch (const DivideError& e) //这里捕获DivideError类型的异常,使用const引用接收以避免对象拷贝
{
//2.1:调用异常对象的what()方法获取错误信息并输出
cout << "捕获到异常:" << e.what() << endl;
}
//3.程序执行到这里,无论是否发生异常都会继续运行
cout << "程序继续执行..." << endl;
return 0;
}
1. 异常类型匹配:catch块只捕获与自身类型匹配的异常(不进行隐式类型转换)
catch(int)无法捕获double类型的异常catch,C++ 允许以下有限的隐式转换,让异常能匹配到兼容的 catch转换场景 | 示例说明 |
|---|---|
非常量 → 常量 | 抛出 string(非常量)可匹配 const string&(权限缩小,更安全) |
数组 → 指针 | 抛出 int arr[5]可匹配 int*(数组退化为指针) |
函数 → 指针 | 抛出函数 void foo()可匹配 void (*)()(函数退化为指针) |
派生类 → 基类 | 抛出 Derived(派生类)可匹配 Base&(多态场景核心规则) |
2. 捕获所有异常:使用catch(…)可以捕获任意类型的异常
若异常传播到 main 函数后仍无匹配的 catch,程序会调用 std::terminate() 强制终止(通常表现为崩溃)
为避免程序意外终止,建议在 main 中添加兜底捕获通常作为最后一个catch块,处理未预料到的错误
try
{
// 可能抛出异常的代码
}
catch (const DivideError& e)
{
// 处理特定异常
}
catch (...)
{
// 处理所有其他未捕获的异常
cout << "发生未知异常" << endl;
}3. 异常的匹配:catch捕获逻辑:
抛出异常后,如果try块中抛出的异常在当前作用域没有匹配的catch块,异常会沿调用链向上传播到调用者的作用域,直到找到匹配的catch块或程序终止
匹配规则:
catch,选择离抛出位置最近的那个(栈展开时先遇到的)
#include <iostream>
#include <string>
using namespace std;
void func()
{
throw string("异常测试");
}
int main()
{
//1.
try
{
func();
}
//2.需特殊转换,才会匹配 const char*()
catch (const char* e)
{
cout << "捕获 C 字符串异常" << endl;
}
//3.完全匹配 string 类型
catch (string& e)
{
cout << "捕获 string 异常" << endl;
}
}
funcA → funcB → main ),直到找到匹配的catch块或程序终止

4. 异常对象的拷贝与销毁:抛出异常时,若异常对象是局部对象(如:函数内的栈对象),会生成一个拷贝传递给 catch 块(类似函数的传值返回)
5. 资源管理:异常可能导致程序跳过某些清理代码(如:释放内存、关闭文件)
RAII(资源获取即初始化) 机制(如:智能指针)管理资源,确保异常发生时资源能被正确释放6. 不要滥用异常:异常适用于处理罕见且不可预测的错误(如:文件损坏、网络中断)
7. 异常执行流程的细节
#include <iostream>
#include <string>
using namespace std;
/*-------------------------------自定义异常类-------------------------------*/
class DivideError
{
public:
//存储异常的具体描述信息
string msg;
//构造函数:初始化异常信息
DivideError(string m)
: msg(m)
{ }
};
/* 说明:
* 1. 自定义异常类:用于表示除法运算中的错误
* 2. 推荐使用类对象传递异常信息,可包含更丰富的错误详情
*/
/*-------------------------------函数A-------------------------------*/
void funcA()
{
//1.此处模拟检测到除数为0的错误,直接抛出自定义异常
throw DivideError("除数不能为 0"); //异常对象包含具体的错误信息:"除数不能为 0"
}
/* 说明:
* 1. 函数A:负责抛出异常的函数
* 2. 当检测到除法错误时,抛出自定义的DivideError异常对象
*/
/*-------------------------------函数B-------------------------------*/
void funcB()
{
//1.调用可能抛出异常的函数
funcA();
//2.打印提示内容
cout << "funcB:这条语句不会执行" << endl;
}
/* 说明:
* 1. 函数B:中间调用层,不处理异常
* 2. 调用funcA(),但不捕获其抛出的异常,异常会向上传播
*/
/*-------------------------------主函数-------------------------------*/
int main()
{
//1.try块:包裹可能抛出异常的代码
try
{
funcB(); // 调用funcB(),该函数间接调用可能抛出异常的funcA()
}
//2.第一个catch块:专门捕获DivideError类型的异常
catch (DivideError& e) //注:使用引用接收异常对象,避免对象拷贝,提高效率
{
//2.1:输出异常信息
cout << "捕获异常:" << e.msg << endl; //通过异常对象的msg成员获取错误描述
}
//3.第二个catch块:捕获所有其他未被处理的异常(通配符)
catch (...) //注:作为兜底处理,确保所有异常都能被捕获,避免程序崩溃
{
cout << "捕获未知异常" << endl;
}
//4.异常处理完成后,程序继续执行后续代码
cout << "程序正常结束" << endl;
return 0;
}
执行流程解析:
funcA 抛出 DivideError 异常 → funcA 立即停止执行funcB 中 funcA 之后的代码(cout)被跳过,异常传播到 main 函数main 中的 try 块匹配到 catch (DivideError& e) → 执行错误处理逻辑funcB 中的临时变量)被自动销毁C++ 异常处理流程(抛出 → 捕获 → 栈展开) 1. 异常抛出后的基本流程 当程序执行 throw 抛出异常时,会触发以下步骤:
2. 栈展开:跨函数的异常传播 如果当前函数中没有 try/catch,或 catch 的类型不匹配,会触发栈展开:
funcA → funcB → main ),直到找到匹配的catch 或到达程序入口(main 函数)3. 未捕获异常的最终结果 如果异常传播到 main 函数后,仍未找到匹配的 catch:
std::terminate(),直接终止程序运行(通常表现为崩溃)4. 异常处理后的执行流程 一旦找到匹配的catch块并执行:
代码示例:异常处理流程
#include <iostream>
#include <string>
using namespace std;
/*----------------------------除法函数----------------------------*/
double Divide(int a, int b)
{
//1.包裹可能出现异常的代码
try
{
//1.1:当除数 b 为 0 时抛出异常
if (b == 0)
{
//第一步:定义一个局部字符串对象,存储异常信息
string s("Divide by zero condition!");
//第二步:抛出异常类型为 string(实际是抛出局部对象的拷贝)
throw s;
}
//1.2:正常除法运算返回结果
else
{
return ((double)a / (double)b);
}
}
//2.捕获int类型的异常
catch (int errid) //注意:仅处理 int 类型的异常
{
cout << errid << endl; // 若捕获到 int 类型异常,输出错误码
}
return 0;
}
/*----------------------------功能函数----------------------------*/
void Func()
{
//1.定义两个变量并输出并进行赋值
int len, time;
cin >> len >> time;
//2.包裹可能出现异常的代码
try
{
cout << Divide(len, time) << endl; //调用 Divide 函数,输出结果
}
//3.捕获 const char* 类型的异常
catch (const char* errmsg)
{
cout << errmsg << endl; //输出 C 风格字符串的异常信息
}
//4.输出函数名和行号,标记函数执行到此处
cout << __FUNCTION__ << ":" << __LINE__ << " 执行" << endl;
/* 说明:
* 1. __FUNCTION__:当前函数名
* 2. __LINE__:当前行号
*/
}
//:
/*----------------------------主函数----------------------------*/
int main()
{
while (1) //循环调用 Func,持续测试异常
{
//1.定义两个变量并输出并进行赋值
try
{
// 调用 Func 函数
Func();
}
//2. 捕获string 类型的异常(与 Divide 中抛出的类型匹配)
catch (const string& errmsg)
{
cout << errmsg << endl; //输出 string 类型的异常信息
}
}
return 0;
}
关键逻辑注释: 1. Divide函数的问题:
string 类型(throw s;),但 catch 块只处理 int 类型2. 异常的实际传播路径:
Divide 中抛出 string → 未被 Divide 的 catch (int) 捕获Func 的 try 块 → Func 的 catch (const char*) 也不匹配main 的 catch (const string&) → 最终被捕获3. catch类型不匹配的影响:
Divide 的 catch (int)、Func 的 catch (const char*) 均无法匹配 string 类型的异常,导致异常必须传播到 main 才能被处理4. 调试信息 FUNCTION 和 LINE:
__FUNCTION__ 输出当前函数名,__LINE__ 输出代码行号,用于标记程序执行位置(如 Func:129 执行)核心总结:
main 函数)添加兜底 catch(...)C++ 标准库定义了一系列异常类(继承自std::exception),用于标准库函数抛出的异常,例如:
std::bad_alloc:new分配内存失败时抛出std::out_of_range:访问容器(如:vector)越界时抛出(如:vector::at())std::invalid_argument:无效参数时抛出 这些异常类都可以通过catch(const std::exception& e)捕获,并通过e.what()获取异常描述。
代码示例:
#include <iostream>
#include <vector>
#include <stdexcept> // 包含标准异常类的头文件,提供std::exception及其派生类
using namespace std;
int main()
{
//1.创建一个vector容器并初始化
vector<int> v = { 1, 2, 3 };
//2.try块:包含可能抛出异常的代码
try
{
//1.使用vector的at()方法会进行边界检查
cout << v.at(10) << endl;
/* 说明:
* 1. 尝试访问索引为10的元素,而容器只有3个元素(索引0,1,2)
* 2. 此时会抛出out_of_range类型的异常
*/
//2.打印输出信息
cout << "元素访问成功" << endl; //注:如果上面的代码抛出异常,下面的语句不会执行
}
//3.第一个catch块:专门捕获out_of_range类型的异常
catch (const out_of_range& e) //注:out_of_range是std::exception的派生类,用于表示范围越界错误
{
//3.1:调用异常对象的what()方法,获取异常的描述信息
cout << "异常:" << e.what() << endl;
}
//4.第二个catch块:捕获所有其他标准异常(std::exception及其派生类)
catch (const exception& e) //注:作为兜底处理,确保所有标准异常都能被捕获
{
cout << "标准异常:" << e.what() << endl;
}
//5.异常处理完成后,程序继续执行
cout << "程序继续运行..." << endl;
return 0;
}
总结:通过异常处理,C++ 程序能够更优雅地应对运行时错误,提高了代码的健壮性和可维护性。
异常处理机制让程序中独立开发的模块,能在运行时高效传递错误信息并处理。 它的核心优势是 “分离错误检测与处理逻辑”
检测错误的代码(如:文件读取失败)只需抛出异常,无需关心如何处理处理错误的代码(如:重试、提示用户)只需捕获异常,无需关心错误如何触发对比 C 语言的错误码方案:
错误码标识错误,但需手动查询错误码含义(如:查文档才知 errno=2 是文件不存在),且错误码无法携带复杂信息(如:自定义错误描述)抛出对象传递错误,可携带更丰富的上下文(如:错误详情、调用栈),处理更灵活C++ 异常的设计,本质是通过 “对象传递错误 + 调用链跳转 + 自动资源清理”,解决传统错误码的缺陷:
下面的代码是一个模拟多层服务调用中异常处理机制的示例程序。 主要功能是展示大型项目中如何
设计异常体系、抛出异常以及统一捕获处理异常
#include<iostream>
#include<string>
#include<thread>
#include<cstdlib> // rand、srand所需头文件
#include<ctime> // time函数所需头文件
#include<chrono> // 时间相关工具(this_thread::sleep_for)
using namespace std;
// ===================== 异常类体系 =====================
/*------------------------------【异常基类设计】------------------------------*/
class Exception //定义通用异常接口,派生出各模块异常
{
protected:
string _errmsg; // 错误描述信息
int _id; // 错误码(可用于分类处理)
public:
//1.构造函数:初始化错误信息和错误ID
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
, _id(id)
{ }
//2.虚函数:返回异常描述(多态关键,子类需重写)
virtual string what() const
{
return _errmsg;
}
//3.获取错误ID
int getid() const
{
return _id;
}
};
/*------------------------------【SQL模块异常】------------------------------*/
class SqlException : public Exception //继承Exception,补充SQL语句信息
{
private:
const string _sql; // 存储触发异常的SQL语句
public:
//1.构造函数:传递基类参数 + SQL语句
SqlException(const string& errmsg, int id, const string& sql)
:Exception(errmsg, id)
, _sql(sql)
{ }
//2.重写what:拼接SQL异常信息
virtual string what() const override
{
string str = "SqlException:";
str += _errmsg;
str += "->";
str += _sql;
return str;
}
};
/*------------------------------【缓存模块异常】------------------------------*/
class CacheException : public Exception //继承Exception,基础错误场景
{
public:
//1.构造函数:传递基类参数
CacheException(const string& errmsg, int id)
:Exception(errmsg, id)
{ }
//2.重写what:标识缓存异常
virtual string what() const override
{
string str = "CacheException:";
str += _errmsg;
return str;
}
};
/*------------------------------【HTTP模块异常】------------------------------*/
class HttpException : public Exception //继承Exception,补充请求类型
{
private:
const string _type; // 请求类型(如get/post)
public:
//1.构造函数:传递基类参数 + HTTP请求类型
HttpException(const string& errmsg, int id, const string& type)
:Exception(errmsg, id)
, _type(type)
{ }
//2.重写what:拼接HTTP请求类型 + 错误
virtual string what() const override
{
string str = "HttpException:";
str += _type;
str += ":";
str += _errmsg;
return str;
}
};
// ===================== 模块功能函数 =====================
/*------------------------------【SQL管理模块】------------------------------*/
void SQLMgr() //随机触发“SQL异常”
{
//情况一:1/7概率触发异常 ----> 这里以及下面的异常触发条件都是只是为了帮助理解,无实际意义
if (rand() % 7 == 0)
{
// 抛SQL异常:带错误信息+SQL语句
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
//情况二:正常调用
else
{
cout << "SQLMgr 调用成功" << endl;
}
}
/*------------------------------【缓存管理模块】------------------------------*/
void CacheMgr() //随机触发“缓存异常”
{
//1.
//情况一:1/5概率触发“权限不足”
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
//情况二:1/6概率触发“数据不存在”
else if (rand() % 6 == 0)
{
throw CacheException("数据不存在", 101);
}
//情况三:正常调用
else
{
cout << "CacheMgr 调用成功" << endl;
}
//2.调用SQL模块(模拟多层依赖)
SQLMgr();
}
// :
/*------------------------------【HTTP服务模块】------------------------------*/
void HttpServer() //随机触发“HTTP异常”
{
//1.
//情况一: 1/3概率触发“资源不存在”(get请求)
if (rand() % 3 == 0)
{
throw HttpException("请求资源不存在", 100, "get");
}
//情况二:1/4概率触发“权限不足”(post请求)
else if (rand() % 4 == 0)
{
throw HttpException("权限不足", 101, "post");
}
//情况三:正常调用
else
{
cout << "HttpServer调用成功" << endl;
}
//2.调用缓存模块(模拟多层依赖)
CacheMgr();
}
// ===================== 主逻辑 =====================
int main()
{
//1.初始化随机数种子(按时间)
srand(time(0));
//2.持续模拟服务调用
while (1)
{
//2.1:线程休眠1秒(避免高频输出)
this_thread::sleep_for(chrono::seconds(1));
//2.2:包含可能抛出异常的代码
try
{
HttpServer(); // 启动HTTP服务(触发多层调用)
}
//2.3:专门:捕获基类Exception类型的异常
catch (const Exception& e) //注意:利用多态,派生类异常会匹配到基类引用
{
cout << "捕获异常:" << e.what() << endl;
cout << "错误码:" << e.getid() << endl;
}
//2.4:兜底:捕获所有未处理异常
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
return 0;
}
核心设计解析:
1. 异常体系分层
Exception定义通用接口(what/getid),派生类(SqlException等)补充模块特有信息catch (const Exception& e)可统一处理所有模块异常,符合开闭原则(新增模块只需继承基类)2. 多态的关键作用
3. 模块异常补充信息
SqlException存储 SQL 语句、HttpException存储请求类型4. 服务调用链路
异常会沿调用链向上传播,最终被main的catch (Exception&)捕获
HttpServer() → CacheMgr() → SQLMgr() 5. 随机触发逻辑
rand() % N == 0控制异常概率,模拟真实服务的偶发错误sleep_for(1s)控制输出频率,避免刷屏在复杂业务中,捕获异常后常需
分类处理:对特定类型异常做特殊逻辑,其他异常则继续向上传播。
假设一个函数需要:捕获异常后,先检查是否是特定类型(如:SqlException)
核心语法:throw 重新抛出当前异常 捕获异常后,用无参数的 throw; 可直接抛出当前捕获的异常对象(不改变异常类型)
示例代码:
void processRequest()
{
//1.包含可能抛出异常的代码
try
{
callService(); // 可能抛出多种异常的逻辑
}
//2.捕获基类异常(多态场景)
catch (const Exception& e)
{
//2.1:特殊处理特定异常
if (typeid(e) == typeid(SqlException))
{
// 假设是数据库异常,执行回滚
rollbackTransaction();
cout << "已回滚事务:" << e.what() << endl;
}
//2.2:其他异常重新抛出
else
{
throw; // 不改变异常,继续向上传播
}
}
}
int main()
{
//1.包含可能抛出异常的代码
try
{
processRequest();
}
//2.最终捕获并处理所有异常
catch (const Exception& e)
{
cout << "外层处理异常:" << e.what() << endl;
}
}关键逻辑说明: 1. 分类处理
typeid 或 动态类型判断(如:if (dynamic_cast<SqlException*>(&e)) )识别异常类型2. 重新抛出的作用
3. 对比:throw e; vs throw;
throw e;:会拷贝异常对象,可能切片(若 e 是基类引用,派生类信息丢失)throw;:直接抛出当前捕获的异常对象,无拷贝,保留完整类型信息 总结:通过 throw; 重新抛出,既能实现局部异常的特殊处理,又能保留异常的完整上下文,是复杂异常流程中的关键技巧。
代码示例:HTTP 相关场景下的异常重抛的使用
#include <iostream>
#include <string>
#include <ctime>
#include <cstdlib>
using namespace std;
/*-------------------------基类异常-------------------------*/
class Exception
{
public:
//1.纯虚函数what:返回异常的“描述信息”
// 作用:多态调用时,能返回具体异常的详细说明(如 "网络不稳定")
virtual string what() const = 0;
//2.纯虚函数getid:返回异常的“错误码”
// 作用:用于分类处理异常(如:102 代表网络错误、103 代表业务错误)
virtual int getid() const = 0;
//3.虚析构函数~Exception:
// 1)确保派生类对象销毁时,先调用派生类析构,再调用基类析构
// 2)使用 =default 让编译器生成默认实现,简洁且符合规范
virtual ~Exception() = default;
};
/*-------------------------HTTP 异常-------------------------*/
class HttpException : public Exception
{
private:
string _msg; // 异常的具体描述信息(如:"网络不稳定,发送失败")
int _id; // 异常对应的错误码(如:102 代表网络错误)
string _type; // HTTP 请求类型(如:"put"/"get",区分不同场景的 HTTP 操作)
public:
//1.构造函数:初始化私有成员
HttpException(const string& msg, int id, const string& type)
: _msg(msg), // 初始化异常描述
_id(id), // 初始化错误码
_type(type) // 初始化 HTTP 请求类型
{ }
//2.重写基类的纯虚函数 what()
string what() const override
{
return "HttpException: " + _type + " -> " + _msg; //返回包含 HTTP 场景信息的异常描述
}
//3.重写基类的纯虚函数 getid()
int getid() const override //返回异常对应的错误码(让外层代码通过错误码分类处理)
{
return _id;
}
};
/*-------------------------模拟发送消息的底层函数-------------------------*/
void _SeedMsg(const string& s) //可能抛出异常
{
if (rand() % 2 == 0) // 随机数模拟异常概率
{
// 抛出网络不稳定异常(102 号错误)
throw HttpException("网络不稳定,发送失败", 102, "put");
}
else if (rand() % 7 == 0)
{
// 抛出好友关系异常(103 号错误)
throw HttpException("你已经不是对象的好友,发送失败", 103, "put");
}
else
{
// 模拟发送成功
cout << "发送成功: " << s << endl;
}
}
/*-------------------------封装重试逻辑的发送函数-------------------------*/
void SendMsg(const string& s) //核心异常处理
{
for (size_t i = 0; i < 4; ++i) //最多重试 3 次
{
//1.1:包含可能抛出异常的代码
try
{
//第一步:尝试发送消息
_SeedMsg(s);
//第二步:发送成功则跳出循环
break;
}
//1.2:捕获Exception类型的异常
catch (const Exception& e)
{
//情况一:错误码是102
if (e.getid() == 102)
{
// 重试 3 次后仍失败,重新抛出异常
if (i == 3)
{
cout << "重试 3 次均失败,网络太差!" << endl;
throw;
}
//输出重试提示
cout << "开始第 " << (i + 1) << " 重试" << endl;
}
//情况二:错误码不是102
else
{
throw; //直接重新抛出
}
}
}
}
/*-------------------------主函数-------------------------*/
int main()
{
//1.初始化随机数种子(按当前时间)
srand(time(0));
//2.定义字符串来接受用户输入的消息
string str;
//3.持续接收用户输入
while (cin >> str)
{
try
{
//发送消息并处理可能的异常
SendMsg(str);
}
catch (const Exception& e)
{
//精准:输出异常详细信息
cout << "捕获异常: " << e.what() << endl << endl;
}
catch (...)
{
//兜底:捕获未知异常
cout << "Unknown Exception" << endl;
}
}
return 0;
}
异常安全是要避免因异常抛出,导致程序出现资源泄漏、执行流程失控等风险。
主要体现在以下两类场景:
一、普通函数执行中因异常引发的资源泄漏
举个示例:
void func()
{
//1.申请内存资源
int* p = new int[100];
//2.中间逻辑可能抛出异常
if (/* 某种错误条件 */)
{
throw "内存操作异常";
}
//3.正常流程下的资源释放
delete[] p; //注意异常安全:若异常被抛出,delete[] p 不会执行,内存无法释放,造成泄漏
}安全问题:若异常被抛出,delete[] p 不会执行,内存无法释放,造成泄漏。
解决思路:
用try-catch主动捕获异常,在捕获后补充资源释放逻辑,必要时可重新抛出异常让上层处理
void func()
{
//1.申请内存资源
int* p = new int[100];
//2.包含可能抛出异常的代码
try
{
if (/* 错误条件 */)
{
throw "内存操作异常";
}
}
//3.捕获所有其他未被处理的异常
catch (...)
{
//3.1:异常时释放资源
delete[] p;
//3.2:重新抛出,让上层感知错误
throw;
}
//4.正常流程释放
delete[] p;
}更推荐RAII(资源获取即初始化)机制(如:智能指针unique_ptr/shared_ptr),让资源的释放由对象析构自动完成,无需手动控制
// 函数功能:演示使用智能指针unique_ptr实现异常安全的内存管理
void func()
{
// 1. 使用unique_ptr智能指针管理动态数组内存
unique_ptr<int[]> p(new int[100]);
/* 说明:
* 1. unique_ptr是C++11引入的智能指针,遵循RAII(资源获取即初始化)原则
* 2. 这里分配了一个包含100个int元素的动态数组
* 3. 模板参数<int[]>表示管理的是int类型的数组
*/
// 2. 模拟可能抛出异常的错误检查
if (/* 错误条件 */) // 假设这里是某种错误条件判断,实际使用时会替换为具体的判断逻辑
{
//2.1:当错误条件满足时,抛出异常
throw "内存操作异常"; //异常类型为const char*(C风格字符串),描述错误信息
}
// 3. 后续正常业务逻辑(此处省略)
// ...
// 4. 无需手动释放内存
/* 说明:
* 1. 无论函数是正常执行结束,还是因抛出异常而提前退出
* 2. unique_ptr会在离开其作用域(即func()函数结束)时自动调用delete[]
* 3. 释放所管理的动态数组内存,彻底避免内存泄漏
*/
}二、析构函数中抛出异常的风险
假设析构函数要释放 10 个资源,执行到第 5 个时抛出异常,程序会直接退出析构流程,导致后续 5 个资源无法释放。
举个示例:
class Resource
{
public:
~Resource()
{
releaseResource1(); // 释放第1个资源
releaseResource2(); // 释放第2个资源(假设此处抛异常)
releaseResource3(); // 若上面抛异常,这里不会执行
// ... 后续7个资源释放逻辑
}
private:
void releaseResource2()
{
throw "释放资源2失败";
}
};安全问题:若 releaseResource2() 抛异常,releaseResource3() 及之后的资源释放逻辑会被跳过,造成泄漏。
解决原则:(参考《Effective C++》条款 8)
析构函数中应避免抛出异常
若无法避免(如:某些资源释放必然可能抛异常),需在析构函数内部用try-catch捕获并处理,不让异常 “逃离” 析构函数
~Resource()
{
try
{
releaseResource2();
}
catch (...)
{
logError("释放资源2失败");
}
releaseResource3(); // 继续执行后续资源释放逻辑
}或设计接口,让调用者在对象销毁前,主动处理可能抛异常的资源释放逻辑,避免析构函数中处理复杂风险
异常安全的核心目标:
#include <iostream>
#include <string>
using namespace std;
/*------------------------除法函数------------------------*/
double Divide(int a, int b)
{
//1.当除数 b 为 0 时抛出异常
if (b == 0)
{
//抛出 C 风格字符串异常,表示“除以 0”的错误
throw "Division by zero condition!";
}
//2.正常计算除法并返回结果
return static_cast<double>(a) / b;
}
/*------------------------功能函数------------------------*/
void Func()
{
//1.动态分配一个大小为 10 的 int 数组(需要手动释放内存)
int* array = new int[10];
//2.包含可能抛出异常的代码
try
{
//2.1:从控制台输入两个整数
int len, time;
cin >> len >> time;
//2.2:调用 Divide 函数,可能抛出异常
cout << Divide(len, time) << endl;
}
//3.捕获所有类型的异常(通配符捕获)
catch (...)
{
//3.1:释放动态分配的数组内存
cout << "delete [] " << array << endl;
delete[] array;
//3.2:重新抛出异常,让上层调用者继续处理
throw; //这里不会改变异常的类型,只是将异常传递给更外层的 catch
}
//4.释放动态分配的数组内存
cout << "delete [] " << array << endl; //注意:如果上面的 try 块中没有抛出异常,会执行到这里
delete[] array;
}
/*------------------------主函数------------------------*/
int main()
{
//1.包含可能抛出异常的代码
try
{
Func(); // 调用 Func 函数,可能抛出异常
}
//2.捕获 const char* 类型的异常(与 Divide 中抛出的类型匹配)
catch (const char* errmsg)
{
cout << errmsg << endl; // 输出异常信息
}
//3.捕获 std::exception 类型的异常(C++ 标准库异常)
catch (const exception& e)
{
cout << e.what() << endl; //输出异常的详细描述(通过 what() 方法)
}
//4.捕获所有其他类型的异常(兜底处理)
catch (...)
{
cout << "Unknown Exception" << endl; // 输出未知异常的提示
}
return 0;
}
详细注释说明
Func函数: 1. 动态内存分配:
new int[10] 动态分配一个大小为 10 的数组,需要在适当的地方用 delete[] 释放内存,否则会导致内存泄漏2. try-catch 块:
len 和 time,然后调用 Divide 函数Divide 抛出异常,会立即跳转到 catch 块... 捕获所有类型的异常(无论异常是什么类型,都会被捕获)throw; 重新抛出异常,让上层调用者(如:main 函数)继续处理try 块中没有抛出异常,会执行到 try-catch 块之后的代码,同样需要释放动态分配的数组内存异常规范(Exception Specification,也叫异常说明):存在的意义,本质是解决 “函数行为的可预测性” 问题—— 让开发者和编译器能提前知道函数会不会抛异常、抛哪些异常,从而让代码更安全、更易维护。
可以从以下几个维度理解:
1. 对开发者:明确错误处理边界
想象你调用一个函数 sendRequest(),如果不知道它会不会抛异常,你得时刻担心:
有了异常规范(比如:noexcept 或 C++98 的 throw()),调用者能清晰判断:
// 明确承诺:不会抛异常 → 调用者无需 try/catch
void sendRequest() noexcept
{ ... }
// 明确声明:可能抛 NetworkError 或 TimeoutError → 调用者针对性捕获
void sendRequest() throw(NetworkError, TimeoutError)
{ ... }价值:减少调用者的 “猜测成本”,让错误处理更精准。
2. 对编译器:优化代码 & 提前发现问题
编译器知道函数的异常行为后,能做两件关键事:
(1)优化代码生成
如果函数声明 noexcept,编译器会认为它 “绝对安全”,可以:
(2)提前发现矛盾
C++98 的 throw(类型列表) 会强制检查:如果函数实际抛出的异常类型不在列表里,编译器会报错。虽然这个特性因过于严格被弃用,但本质是想阻止 “意外抛异常”
3. 对项目:统一异常处理规范
大型项目中,团队可以约定:
noexcept(如:工具类的辅助函数)NetworkException)这样,新人接手代码时能快速理解:
noexcept 函数无需处理异常。throw(...) 的函数,必须处理指定类型的异常。价值:降低团队协作的沟通成本,让异常处理逻辑更一致。
一句话总结:异常规范的本质是给函数的 “错误反馈方式” 贴标签:
现代 C++ 中,noexcept 是更简洁、更实用的选择,但核心目标从未改变:让异常处理更可控。
C++98 的异常规范:throw() C++98 用
throw(类型列表)声明函数可能抛出的异常类型:
语法示例 | 含义说明 |
|---|---|
void func() throw(); | 函数不抛出任何异常 |
void func() throw(int); | 函数仅可能抛出 int 类型异常 |
void func() throw(int, string); | 函数可能抛出 int 或 string 异常 |
C++11 的异常规范:noexcept关键字 因 C++98 的
throw(...)过于复杂,C++11 改用更简洁的noexcept:
语法 | 含义说明 |
|---|---|
void func() noexcept; | 函数不会抛出任何异常(编译期承诺) |
void func(); | 函数可能抛出任意异常(无约束) |
C++98 用
throw(类型列表)声明异常规范(如:throw(int)表示仅抛int异常),但因设计复杂已被弃用。noexcept是更简洁的替代方案:
特性 | throw() | noexcept |
|---|---|---|
语法 | 需枚举所有可能抛出的类型 | 仅需 noexcept 或表达式 |
编译器检查 | 强制检查(违反则编译报错) | 不强制检查(仅做承诺) |
异常处理 | 抛异常时调用unexpected() | 抛异常时调用terminate() |
适用场景 | 已被标准弃用,仅兼容旧代码 | 现代 C++ 推荐使用 |
C++98 的
throw(类型列表)设计太复杂:
noexcept 做了减法:
noexcept:是 C++11 引入的异常规范关键字,用于声明函数是否可能抛出异常。
noexcept关键字的两种常见形式:
noexcept
声明函数不会抛出任何异常(编译期承诺)
void func() noexcept
{
// 函数体中不应有 throw 语句,也不应调用可能抛异常的函数
}noexcept(表达式)
根据编译期表达式的结果决定是否抛出异常:
若表达式为 true,等价于 noexcept(不抛异常)
若表达式为 false,函数可能抛异常(等价于不写 noexcept)
// 检测 add 函数是否抛异常,决定当前函数是否声明为 noexcept
void wrapper() noexcept(noexcept(add(1, 2)))
{
add(1, 2);
}
noexcept是 “编译期承诺”,但编译器不会强制检查 :
实际抛出异常: std::terminate() 直接终止(通常崩溃)内部有throw或调用了可能抛异常的函数: 推荐用 noexcept 的场景:
add、sub)、访问器(getter)等确定不会抛异常的函数noexcept(除非显式取消),析构函数抛异常会导致资源释放逻辑中断swap 若声明为 noexcept,可提升性能(避免不必要的拷贝)不建议用 noexcept 的场景:
new 可能抛 bad_alloc)、文件操作(可能抛 IO 异常)等noexcept在 C++ 中,标准库定义了一套
异常类体系,方便开发者处理程序运行过程中出现的各种错误情况。 这些标准异常类都继承自std::exception,构成了一个层次分明的体系。 cplusplus网站上关于标准异常类的介绍:exception - C++ Reference
std::exception:是整个标准异常类体系的基类。
what() 成员函数,用于返回异常的描述信息其派生关系如下:
std::exception 的直接派生类std::exception std::runtime_error和 std::logic_error又各自有一些派生类,用于表示更具体的异常情况:
std::overflow_error:表示算术溢出错误 std::underflow_error:表示算术下溢错误std::range_error:表示计算结果超出了有意义的值域范围std::invalid_argument:当函数接收到无效的参数时抛出 std::length_error:当试图创建一个超出std::string 或其他标准容器最大长度的对象时抛出std::out_of_range:用于表示访问容器时超出有效范围 std::vector 时使用了一个不存在的索引// 标准异常类的层次结构
std::exception
├── std::logic_error
│ ├── std::invalid_argument
│ ├── std::domain_error
│ ├── std::length_error
│ └── std::out_of_range
├── std::runtime_error
│ ├── std::range_error
│ ├── std::overflow_error
│ ├── std::underflow_error
│ └── std::system_error (C++11)
├── std::bad_alloc
├── std::bad_cast
└── std::bad_typeid
一、“常用异常类”的使用示例
#include <iostream>
#include <string>
#include <stdexcept>
int main()
{
try
{
//1.模拟容器越界访问,会抛出std::out_of_range异常
std::string str = "hello";
char c = str.at(10);
//2.模拟无效参数,会抛出std::invalid_argument异常
int num = std::stoi("abc");
//3.模拟算术溢出,会抛出std::overflow_error异常
int a = 2147483647;
int b = 1;
int result = a + b;
}
catch (const std::out_of_range& e)
{
std::cout << "捕获到out_of_range异常: " << e.what() << std::endl;
}
catch (const std::invalid_argument& e)
{
std::cout << "捕获到invalid_argument异常: " << e.what() << std::endl;
}
catch (const std::overflow_error& e)
{
std::cout << "捕获到overflow_error异常: " << e.what() << std::endl;
}
catch (const std::exception& e)
{
std::cout << "捕获到其他异常: " << e.what() << std::endl;
}
return 0;
}
二、“自定义异常类”的使用示例 除了使用标准异常类,开发者还可以根据实际需求自定义异常类。 通常会继承自
std::exception或者它的派生类,以便复用其接口和特性。
#include <iostream>
#include <exception>
#include <string>
// 自定义异常类,继承自std::runtime_error
class MyCustomException : public std::runtime_error
{
public:
MyCustomException(const std::string& msg)
: std::runtime_error(msg)
{}
};
int main()
{
try
{
// 抛出自定义异常
throw MyCustomException("这是一个自定义的运行时异常");
}
catch (const MyCustomException& e)
{
std::cout << "捕获到自定义异常: " << e.what() << std::endl;
}
catch (const std::exception& e)
{
std::cout << "捕获到其他异常: " << e.what() << std::endl;
}
return 0;
}
标准异常类的优势:
总之:C++ 的标准异常类体系为处理程序运行过程中的错误提供了强大且方便的工具,合理使用它们可以使程序更加健壮和可靠。