C++【异常】
一、异常的概念及使用
1.1 异常的概念

异常处理机制的核心思想是:让程序中独立开发的模块在运行时发生错误时,能够通过异常对象进行通信,并由专门的代码块处理错误。简单来说,程序的一部分负责检测错误并抛出异常,另一部分负责捕获异常并解决问题,检测环节无需知晓处理环节的具体细节。
简单来说,异常让错误处理从 “被动接收错误码” 变成了 “主动抛出并精准捕获”,更适配大型项目的模块化开发。
1.2 异常的抛出和捕获
异常处理的核心操作由throw(抛出)、try(监测)、catch(捕获) 三个关键字构成,三者配合完成从错误检测到错误处理的全流程。
1.2.1 异常的抛出(throw)
当程序检测到错误时,通过throw抛出一个异常对象,语法为:throw 异常对象;。
- 抛出的异常对象可以是任意类型(字符串、内置类型、自定义类对象等),实际开发中推荐使用自定义类对象,便于封装更多错误信息。
throw执行后,其后的代码将不再执行,程序的执行流程会立即跳转到匹配的catch代码块。- catch 可能是同一个函数中的一个局部的catch , 也可能是调用链中另一个函数中的catch , 控制权从throw位置转移到了catch位置 。 这里还有两个重要的含义 : 1. 沿着调用链的函数可能提早退出 2.一旦程序开始执行异常处理程序 , 沿着调用链创建的对象都将销毁。
- 抛出的局部异常对象会生成一个拷贝(类似函数传值返回),该拷贝对象会在
catch处理完成后自动销毁,避免局部对象销毁导致的野指针问题。
示例:除零错误时抛出字符串类型的异常
double Divide(int a, int b)
{
if (b == 0)
{
// 抛出异常:除零错误
throw string("Divide by zero condition!");
}
return (double)a / (double)b;
}
1.2.2 异常的捕获(try-catch)
try块用于监测可能抛出异常的代码,catch块用于捕获并处理异常,一个try可以搭配多个catch,针对不同类型的异常编写不同的处理逻辑。
语法格式:
try
{
// 监测的代码:可能抛出异常的语句
}
catch (异常类型1 变量名)
{
// 处理类型1的异常
}
catch (异常类型2 变量名)
{
// 处理类型2的异常
}
// 捕获任意类型的异常
catch (...)
{
// 处理未匹配到的异常
}

1.2.3 匹配规则:如何找到正确的 catch?
程序抛出异常后,会按照 “类型匹配 + 就近原则” 查找对应的catch块:
- 首先检查
throw是否在try块内部,若在则查找当前try后的catch,匹配类型完全一致的异常; - 若当前函数无匹配的
catch,则退出当前函数,在外层调用链中继续查找,这个过程称为栈展开; - 若直到
main函数仍未找到匹配的catch,程序会调用标准库的terminate函数直接终止; - 若有多个
catch匹配,选择离抛出位置最近的那个。
万能捕获:catch (...):
catch(...)可以捕获任意类型的异常,通常放在所有catch的最后,用于处理未匹配到的异常,避免程序因未捕获的异常终止。但它无法获取异常的具体信息,仅能做兜底处理。
1.3 栈展开


#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
double Divide(int a, int b)
{
try
{
//当b==0时抛出异常
if (b == 0)
{
string s("Divide by zero condition!");
throw s;
}
else
{
return ((double)a / (double)b);
}
}
catch (int errid)
{
cout << errid << endl;
}
return 0;
}
void Func()
{
int len, time;
cin >> len >> time;
try
{
cout << Divide(len, time) << endl;
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}
int main()
{
while (1)
{
try
{
Func();
}
catch (const string& errmsg)
{
cout << errmsg << endl;
}
}
return 0;
}



栈展开过程中,沿着调用链创建的局部对象会被自动销毁,这保证了不会因异常导致局部对象的资源泄漏。
1.4 查找匹配的处理代码



在大型项目中,模块众多(如 SQL 模块、缓存模块、网络模块),每个模块的错误类型不同,若为每个错误编写单独的
catch,代码会极其繁琐。利用 “派生类向基类转换” 的匹配规则,设计异常基类 + 派生类的继承体系,可实现异常的统一捕获 + 个性化处理,这是 C++ 异常处理的最佳实践。
1.4.1 设计思路
- 定义基类 Exception:封装通用的错误信息(如错误描述
_errmsg、错误码_id),提供虚函数what()用于返回错误详情,提供getid()获取错误码; - 定义派生类:每个模块对应一个派生类(如 SqlException、CacheException、HttpException),继承自 Exception,添加模块专属的错误信息(如 SQL 语句、请求类型),并重写
what()函数,拼接模块标识和错误信息; - 捕获异常时,直接捕获基类的引用(避免切片问题),即可匹配所有派生类异常,通过
what()获取个性化的错误详情。
1.4.2 完整代码实现
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include <thread>
#include<string>
//一般大型项目程序才会使用异常,下面我们设计一个服务的几个模块
//每个模块的继承都是Exception的派生类,每个模块可以添加自己的数据
//最后捕获时,我们继承基类就可以
//异常基类
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
,_id(id)
{}
//虚函数->返回错误详情
virtual string what() const
{
return _errmsg;
}
int getid() const
{
return _id;
}
protected:
string _errmsg;
int _id;
};
class SqlException : public Exception
{
public:
SqlException(const string& errmsg,int id,const string& sql)
:Exception(errmsg,id)
,_sql(sql)
{}
virtual string what() const
{
string str = "SqlException:";
str += _errmsg;
str += "->";
str += _sql;
return str;
}
private:
const string _sql; //专属信息:出错的SQL语句
};
//缓存模块异常:派生自Exception
class CacheException :public Exception
{
public:
CacheException(const string& errmsg,int id)
:Exception(errmsg,id)
{}
virtual string what() const
{
string str = "CacheException:";
str += _errmsg;
return str;
}
};
//网络模块异常:派生自Exception
class HttpException : public Exception
{
public:
HttpException(const string& errmsg,int id,const string& type)
:Exception(errmsg,id)
,_type(type)
{}
virtual string what() const override
{
string str = "HttpException:";
str += _type;
str += ":";
str += _errmsg;
return str;
}
private:
const string _type;//专属信息:请求类型(get/post/put)
};
//模拟各模块功能
void SQLMgr()
{
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
else
{
cout << "SQLMgr 调用成功" << endl;
}
}
void CacheMgr()
{
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw CacheException("数据不存在", 101);
}
else
{
cout << "CacheMgr 调用成功" << endl;
}
}
void HttpServer()
{
if (rand() % 3 == 0)
{
throw HttpException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw HttpException("权限不足", 101, "post");
}
else
{
cout << "HttpServer调用成功" << endl;
}
CacheMgr();
}
int main()
{
srand(time(0));
while (1)
{
this_thread::sleep_for(chrono::seconds(1));
try
{
HttpServer();
}
//同一捕获基类引用,匹配所有派生类异常
catch (const Exception& e)
{
cout << e.what() << endl;
}
//兜底:捕获未定义异常
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
}

核心优势:
- 统一管理:新增模块时 , 只需要继承Exception实现派生类 , 无需修改捕获代码,符合开闭原则;
- 信息完整:每个派生类可以封装模块专属信息 , what()虚函数保证了错误代码的个性化;
- 处理简洁:仅需一个catch(const Exception& e)即可捕获所有模块的异常,大幅简化代码。
1.5 异常重新抛出
在实际开发中,有时需要对异常进行分层处理:局部
catch处理部分错误,将未处理的错误重新抛出给外层调用链处理,这就是异常的重新抛出,语法为:throw;(无参数)。适用场景
- 局部仅处理特定错误,其他错误交由外层处理(如网络模块仅重试网络波动错误,其他错误抛给业务层);
- 局部需要做资源释放等清理工作,清理完成后将异常抛给外层处理;
- 对错误进行分类统计后,将原始异常抛给外层做具体业务处理。
模拟聊天软件的消息发送功能:网络不稳定(错误码 102)时重试 3 次,重试失败则抛异常;非网络问题(如不是好友)直接抛异常给外层。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include <thread>
#include<string>
using namespace std;
//一般大型项目程序才会使用异常,下面我们设计一个服务的几个模块
//每个模块的继承都是Exception的派生类,每个模块可以添加自己的数据
//最后捕获时,我们继承基类就可以
//异常基类
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
,_id(id)
{}
//虚函数->返回错误详情
virtual string what() const
{
return _errmsg;
}
int getid() const
{
return _id;
}
protected:
string _errmsg;
int _id;
};
class SqlException : public Exception
{
public:
SqlException(const string& errmsg,int id,const string& sql)
:Exception(errmsg,id)
,_sql(sql)
{}
virtual string what() const
{
string str = "SqlException:";
str += _errmsg;
str += "->";
str += _sql;
return str;
}
private:
const string _sql; //专属信息:出错的SQL语句
};
//缓存模块异常:派生自Exception
class CacheException :public Exception
{
public:
CacheException(const string& errmsg,int id)
:Exception(errmsg,id)
{}
virtual string what() const
{
string str = "CacheException:";
str += _errmsg;
return str;
}
};
//网络模块异常:派生自Exception
class HttpException : public Exception
{
public:
HttpException(const string& errmsg,int id,const string& type)
:Exception(errmsg,id)
,_type(type)
{}
virtual string what() const override
{
string str = "HttpException:";
str += _type;
str += ":";
str += _errmsg;
return str;
}
private:
const string _type;//专属信息:请求类型(get/post/put)
};
//模拟各模块功能
void SQLMgr()
{
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
else
{
cout << "SQLMgr 调用成功" << endl;
}
}
void CacheMgr()
{
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw CacheException("数据不存在", 101);
}
else
{
cout << "CacheMgr 调用成功" << endl;
}
}
void HttpServer()
{
if (rand() % 3 == 0)
{
throw HttpException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw HttpException("权限不足", 101, "post");
}
else
{
cout << "HttpServer调用成功" << endl;
}
CacheMgr();
}
//下面程序模拟展示了聊天时发送消息,发送失败补货异常,但是
//可能在电梯地下室等场景手机信号不好,需要多次尝试;如果多次
//尝试都发送不出去,则就需要捕获异常再重新抛出,其次如果不是
//网络差导致的错误,捕获后也要重新抛出
//模拟消息发送核心函数
void _SendMsg(const string& s)
{
if (rand() % 2 == 0)
{
throw HttpException("网络不稳定,发送失败", 102, "put");
}
else if (rand() % 7 == 0)
{
throw HttpException("你已经不是对象的好友,发送失败", 103, "put");
}
else
{
cout << "发送成功" << endl;
}
}
//消息发送封装:包含重试逻辑
void SendMsg(const string& s)
{
//最多重试3次(共4次尝试)
for (size_t i = 0; i < 4; i++)
{
try
{
_SendMsg(s);
break; //发送成功,退出循环
}
catch(const Exception& e)
{
//处理102号错误:网络不稳定,进行重试
if (e.getid() == 102)
{
//第三次重试失败,重新抛出异常
if (i == 3)
throw;
cout << "开始第" << i + 1 << "次重试";
}
else
{
//非102错误,直接重新抛出
throw;
}
}
}
}
int main()
{
srand(time(0));
string str;
while (cin >> str)
{
try
{
SendMsg(str);
}
catch (const Exception& e)
{
cout << "发送失败:" << e.what() << endl;
}
catch (...)
{
cout << "发送失败:未知异常" << endl;
}
}
return 0;
}
- 重新抛出使用
throw;而非throw 异常对象;,throw;会抛出原始的异常拷贝,避免二次拷贝导致的信息丢失;- 重新抛出前,需完成局部清理工作(如释放资源、关闭文件),否则会导致资源泄漏。
1.6 异常安全问题
- 异常抛出后,后面的代码不再执行 , 前面申请了 资源(内存 、 锁)等,后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常引发了资源泄漏,产生安全性的问题 。
1.6.1 常见的异常安全问题
示例:除零异常导致动态数组未释放
void Func()
{
// 申请动态内存
int* array = new int[10];
int len, time;
cin >> len >> time;
// 抛出除零异常,后续的delete[]未执行,内存泄漏
cout << Divide(len, time) << endl;
// 资源释放代码
delete[] array;
}
1.6.2 解决方案
方案 1:
局部捕获异常,释放资源后重新抛出
在申请资源后,用try-catch监测可能抛异常的代码,捕获异常后先释放资源,再将异常重新抛出给外层处理,保证资源不泄漏。
void Func()
{
int* array = new int[10];
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
// 捕获异常,先释放资源
cout << "释放数组:" << array << endl;
delete[] array;
// 重新抛出,交由外层处理异常
throw;
}
// 无异常时正常释放资源
delete[] array;
}
方案 2:
RAII 机制(推荐)
RAII(资源获取即初始化)是 C++ 的核心设计思想,将资源封装到类中,构造函数申请资源,析构函数释放资源。由于栈展开时局部对象会被自动销毁,析构函数会被强制调用,从而保证资源无论是否发生异常,都会被释放。这是解决异常资源泄漏的最优方案,C++ 中的智能指针(unique_ptr、shared_ptr)就是 RAII 的典型实现。
1.6.3 特殊注意
析构函数不能抛出异常
《Effective C++》第 8 条明确指出:别让异常逃离析构函数。析构函数的作用是释放资源,若析构函数抛出异常,会导致:
- 栈展开过程中,析构函数抛出的新异常会与原异常冲突,程序直接调用
terminate终止; - 若析构函数释放多个资源,抛出异常后后续的资源释放代码未执行,导致资源泄漏。
解决办法:若析构函数中可能发生异常,需在析构函数内部捕获并处理,禁止异常逃离析构函数。
1.7 异常规范
为了让函数的调用者明确知道函数是否会抛出异常、抛出何种类型的异常,C++ 提供了异常规范,用于在函数声明时标注其异常行为,提升代码的可读性和可维护性。C++98 和 C++11 分别提供了不同的异常规范,其中 C++11 的方式更简洁、更实用。
1.7.1 C++98 的异常规范(已废弃)
在函数参数列表后通过throw()标注,语法:
throw():表示函数不会抛出任何异常;throw(类型1, 类型2, ...):表示函数仅可能抛出指定类型的异常。
// 仅抛出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 废弃。
1.7.2 C++11 的异常规范(推荐)
C++11 简化了异常规范,提供了noexcept关键字,语法:
noexcept:表示函数不会抛出任何异常;

- 不标注任何关键字:表示函数可能抛出任意类型的异常(默认行为);
noexcept(表达式):作为运算符,检测表达式是否会抛出异常,返回bool值(true表示不会,false表示可能)。
核心特性
- 编译器不强制检查:若函数标注
noexcept但实际抛出了异常,编译器不会报错(可能警告),但程序会直接调用terminate终止; - 提升程序性能:编译器对
noexcept的函数会做优化,无需生成异常处理的相关代码; - 运算符用法:可在编译期检测表达式的异常行为,用于模板、泛型编程。
// 声明函数不会抛出异常
double Divide(int a, int b) noexcept
{
if (b == 0)
{
// 违反noexcept,程序会调用terminate终止
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
int main()
{
int i = 0;
// noexcept作为运算符:检测表达式是否会抛异常
cout << noexcept(Divide(1,2)) << endl; // 输出1(true)
cout << noexcept(Divide(1,0)) << endl; // 输出1(true,编译期无法检测运行时错误)
cout << noexcept(++i) << endl; // 输出1(true)
return 0;
}
二、标准库异常
https://legacy.cplusplus.com/reference/exception/exception/
C++ 标准库为开发者提供了一套预定义的异常继承体系,基类是std::exception(定义在<exception>头文件中),所有标准库异常都是其派生类,开发者可直接使用,也可继承其实现自定义异常。
2.1 标准库异常的继承结构
核心基类:std::exception(提供虚函数what(),返回 const char * 类型的错误描述)

主要派生类:
std::bad_alloc:内存申请失败(new操作失败时抛出);std::bad_cast:动态类型转换失败(dynamic_cast失败时抛出);std::bad_typeid:typeid操作针对空指针时抛出;std::logic_error:逻辑错误(编译期可检测,如参数无效、数组越界);
- 子派生类:
invalid_argument(无效参数)、out_of_range(越界)、length_error(长度错误);std::runtime_error:运行时错误(编译期无法检测,如算术错误、文件打开失败);
- 子派生类:
overflow_error(上溢)、underflow_error(下溢)、range_error(范围错误)。
2.2 标准库异常的使用
标准库异常的使用方式与自定义异常一致,通过try-catch捕获,调用what()获取错误信息。示例:捕获new操作的内存申请失败异常
#include <iostream>
#include <exception>
using namespace std;
int main()
{
try
{
// 申请超大内存,导致bad_alloc
int* p = new int[1000000000000];
}
// 捕获标准库异常
catch (const bad_alloc& e)
{
cout << "内存申请失败:" << e.what() << endl;
}
// 捕获所有标准库异常
catch (const exception& e)
{
cout << "异常:" << e.what() << endl;
}
return 0;
}
2.3 自定义异常继承 std::exception
实际开发中,推荐让自定义异常继承std::exception,而非自定义基类,这样可以让自定义异常与标准库异常统一处理,提升代码的兼容性。
#include <iostream>
#include <exception>
#include <string>
using namespace std;
// 自定义异常:继承std::exception
class MyException : public exception
{
public:
MyException(const string& errmsg)
: _errmsg(errmsg)
{}
// 重写what(),注意返回值为const char*
virtual const char* what() const noexcept override
{
return _errmsg.c_str();
}
private:
string _errmsg;
};
void func()
{
throw MyException("自定义运行时错误");
}
int main()
{
try
{
func();
}
// 统一捕获std::exception
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}更多推荐

所有评论(0)