文章目录
-
- C++异常机制详解(二):高级特性与最佳实践
- 一、异常的重新抛出
-
- [1.1 为什么需要重新抛出](#1.1 为什么需要重新抛出)
- [1.2 重新抛出的语法](#1.2 重新抛出的语法)
- [1.3 实战案例:消息发送重试机制](#1.3 实战案例:消息发送重试机制)
- [1.4 throw vs throw e](#1.4 throw vs throw e)
- 二、异常安全问题
-
- [2.1 什么是异常安全](#2.1 什么是异常安全)
- [2.2 异常导致的资源泄漏](#2.2 异常导致的资源泄漏)
- [2.3 解决方案一:异常捕获后释放资源](#2.3 解决方案一:异常捕获后释放资源)
- [2.4 解决方案二:RAII机制(推荐)](#2.4 解决方案二:RAII机制(推荐))
- [2.5 析构函数中的异常](#2.5 析构函数中的异常)
- 三、异常规范
-
- [3.1 C++98的异常规范(已废弃)](#3.1 C++98的异常规范(已废弃))
- [3.2 C++11的noexcept(推荐)](#3.2 C++11的noexcept(推荐))
- [3.3 noexcept作为运算符](#3.3 noexcept作为运算符)
- [3.4 什么时候使用noexcept](#3.4 什么时候使用noexcept)
- 四、C++标准库异常体系
-
- [4.1 标准异常继承体系](#4.1 标准异常继承体系)
- [4.2 exception基类](#4.2 exception基类)
- [4.3 常用标准异常](#4.3 常用标准异常)
- [4.4 使用标准异常的好处](#4.4 使用标准异常的好处)
- 五、异常使用的最佳实践
-
- [5.1 何时使用异常](#5.1 何时使用异常)
- [5.2 异常处理的建议](#5.2 异常处理的建议)
- [5.3 性能考虑](#5.3 性能考虑)
- 六、总结
-
- [6.1 全系列回顾](#6.1 全系列回顾)
- [6.2 核心要点](#6.2 核心要点)
- [6.3 继续学习](#6.3 继续学习)
C++异常机制详解(二):高级特性与最佳实践
💬 欢迎讨论:本文是C++异常机制系列的第二篇,将深入探讨异常的高级特性和实践经验。如果你在学习过程中有任何疑问,欢迎在评论区留言交流!
👍 点赞、收藏与分享:这是系列的完结篇,建议结合第一篇一起学习。如果觉得有帮助,请分享给更多的朋友!
🚀 系列回顾:在第一篇中,我们学习了异常的基本概念、抛出与捕获、栈展开机制以及异常继承体系的设计。本篇将继续深入学习。
一、异常的重新抛出
1.1 为什么需要重新抛出
在实际开发中,我们可能需要这样的场景:
- 分类处理:捕获异常后,只处理某些特定类型,其他类型继续向上抛出
- 记录日志:捕获异常记录日志后,让上层继续处理
- 重试机制:某些错误需要重试,重试失败后再抛出
- 资源清理:捕获异常进行必要的清理后,重新抛出
1.2 重新抛出的语法
捕获异常后,直接使用throw;(不带任何参数)就可以重新抛出当前捕获的异常:
cpp
try
{
// 可能抛出异常的代码
}
catch (const exception& e)
{
// 做一些处理
cout << "记录日志: " << e.what() << endl;
throw; // 重新抛出捕获到的异常
}
注意 :throw;只能在catch块内使用,它会抛出当前正在处理的异常对象。
1.3 实战案例:消息发送重试机制
假设我们在开发一个即时通讯应用,发送消息时可能因为网络不稳定而失败。我们希望:
- 如果是网络问题,自动重试3次
- 如果重试3次后仍失败,抛出异常给上层
- 如果是其他错误(如对方已删除好友),直接抛出,不重试
实现代码
cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include <ctime>
using namespace std;
// 定义HTTP异常
class HttpException : public exception
{
public:
HttpException(const string& msg, int code)
: _msg(msg)
, _code(code)
{}
const char* what() const noexcept override
{
return _msg.c_str();
}
int getCode() const { return _code; }
private:
string _msg;
int _code;
};
// 模拟底层发送消息函数
void _SendMsg(const string& msg)
{
int random = rand() % 10;
if (random < 3) // 30%概率网络不稳定
{
throw HttpException("网络不稳定,发送失败", 102);
}
else if (random == 9) // 10%概率对方删除好友
{
throw HttpException("你已经不是对方的好友,发送失败", 103);
}
else
{
cout << "消息发送成功: " << msg << endl;
}
}
// 带重试机制的发送函数
void SendMsg(const string& msg)
{
const int MAX_RETRY = 3;
for (int i = 0; i < MAX_RETRY + 1; ++i)
{
try
{
_SendMsg(msg);
break; // 发送成功,退出循环
}
catch (const HttpException& e)
{
// 如果是网络问题(错误码102),则重试
if (e.getCode() == 102)
{
// 已经重试3次还是失败,重新抛出异常
if (i == MAX_RETRY)
{
cout << "重试" << MAX_RETRY << "次后仍然失败" << endl;
throw; // 重新抛出
}
cout << "第" << i + 1 << "次重试..." << endl;
}
else // 其他错误,直接抛出,不重试
{
throw;
}
}
}
}
int main()
{
srand(time(0));
string msg;
while (cin >> msg)
{
try
{
SendMsg(msg);
}
catch (const HttpException& e)
{
cout << "最终失败: " << e.what() << endl;
cout << "错误码: " << e.getCode() << endl << endl;
}
}
return 0;
}
运行示例
bash
hello
消息发送成功: hello
world
第1次重试...
消息发送成功: world
test
第1次重试...
第2次重试...
第3次重试...
重试3次后仍然失败
最终失败: 网络不稳定,发送失败
错误码: 102
friend
最终失败: 你已经不是对方的好友,发送失败
错误码: 103
设计亮点
- 智能重试:只对网络错误重试,其他错误直接失败
- 重试限制:避免无限重试导致程序卡死
- 异常传播:重试失败后,异常继续向上传播
- 用户体验:提供详细的重试信息
1.4 throw vs throw e
很多初学者容易混淆throw;和throw e;,它们有重要区别:
cpp
try
{
// ...
}
catch (const DerivedClass& e)
{
// 方式1:重新抛出捕获到的原始异常对象(保持类型)
throw;
// 方式2:抛出e的拷贝(可能发生对象切片)
throw e;
}
对比演示
cpp
class Base
{
public:
virtual void show() { cout << "Base" << endl; }
};
class Derived : public Base
{
public:
void show() override { cout << "Derived" << endl; }
};
void test1()
{
try
{
throw Derived();
}
catch (const Base& e)
{
e.show(); // 输出Derived
throw; // 重新抛出,保持Derived类型
}
}
void test2()
{
try
{
throw Derived();
}
catch (const Base& e)
{
e.show(); // 输出Derived
throw e; // 抛出Base类型!发生对象切片
}
}
int main()
{
cout << "test1:" << endl;
try { test1(); }
catch (const Base& e) { e.show(); } // 输出Derived
cout << "\ntest2:" << endl;
try { test2(); }
catch (const Base& e) { e.show(); } // 输出Base!
return 0;
}
输出
bash
test1:
Derived
Derived
test2:
Derived
Base
结论 :使用throw;重新抛出,能够保持异常对象的真实类型,避免对象切片。
二、异常安全问题
2.1 什么是异常安全
异常安全是指:当异常发生时,程序能够正确处理资源,不会造成资源泄漏或数据不一致。
常见的异常安全问题:
- 内存泄漏:new了内存,异常导致delete没执行
- 文件句柄泄漏:打开文件后,异常导致没有关闭
- 锁没释放:获取锁后,异常导致锁没释放
- 数据不一致:修改到一半时抛出异常
2.2 异常导致的资源泄漏
问题代码
cpp
void Func()
{
int* array = new int[10];
// 这里可能抛出异常
// ...
if (某个条件)
{
throw "error";
}
// ...
delete[] array; // 如果前面抛异常,这行不会执行!
}
这是一个典型的内存泄漏问题。如果在delete之前抛出异常,内存永远不会被释放。
2.3 解决方案一:异常捕获后释放资源
cpp
double Divide(int a, int b)
{
if (b == 0)
{
throw "Division by zero!";
}
return (double)a / b;
}
void Func()
{
int* array = new int[10];
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
// 捕获异常后释放资源
cout << "释放资源: delete[] " << array << endl;
delete[] array;
throw; // 重新抛出异常,让外层处理
}
// 正常情况下也要释放
cout << "正常释放: delete[] " << array << endl;
delete[] array;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << "错误: " << errmsg << endl;
}
return 0;
}
这种方案的问题
- 代码重复:资源释放代码出现两次
- 容易遗漏:如果有多个资源,每个都要这样处理
- 可维护性差:添加新资源时容易出错
2.4 解决方案二:RAII机制(推荐)
RAII(Resource Acquisition Is Initialization)是C++中处理资源的最佳方式。核心思想是:
- 资源的获取在构造函数中完成
- 资源的释放在析构函数中完成
- 利用栈展开时自动调用析构函数的特性
智能指针示例
cpp
void Func()
{
// 使用智能指针,自动管理内存
unique_ptr<int[]> array(new int[10]);
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
// 即使抛出异常,array的析构函数也会被调用
// 内存自动释放,不会泄漏
}
自定义RAII类示例
cpp
// 文件资源RAII封装
class FileGuard
{
public:
FileGuard(const char* filename)
: _file(fopen(filename, "r"))
{
if (!_file)
{
throw runtime_error("打开文件失败");
}
cout << "文件打开成功" << endl;
}
~FileGuard()
{
if (_file)
{
fclose(_file);
cout << "文件已关闭" << endl;
}
}
FILE* get() { return _file; }
// 禁止拷贝
FileGuard(const FileGuard&) = delete;
FileGuard& operator=(const FileGuard&) = delete;
private:
FILE* _file;
};
void ProcessFile()
{
FileGuard file("data.txt");
// 使用文件
// ...
// 如果这里抛异常,file的析构函数仍会被调用
// 文件会被正确关闭
if (某个条件)
{
throw runtime_error("处理文件时出错");
}
}
锁资源RAII封装
cpp
#include <mutex>
class LockGuard
{
public:
LockGuard(mutex& mtx) : _mtx(mtx)
{
_mtx.lock();
cout << "锁已获取" << endl;
}
~LockGuard()
{
_mtx.unlock();
cout << "锁已释放" << endl;
}
LockGuard(const LockGuard&) = delete;
private:
mutex& _mtx;
};
mutex g_mtx;
void CriticalSection()
{
LockGuard lock(g_mtx);
// 临界区代码
// ...
// 即使抛异常,锁也会被正确释放
}
2.5 析构函数中的异常
重要原则:析构函数不应该抛出异常!
原因
- 栈展开过程中调用析构函数,如果析构函数再抛异常,会导致
terminate()被调用 - 一个对象的析构可能需要释放多个资源,如果中途抛异常,后续资源无法释放
错误示例
cpp
class Resource
{
public:
~Resource()
{
if (释放失败)
{
throw runtime_error("释放失败"); // 危险!
}
}
};
void func()
{
Resource r1, r2;
throw "error"; // 栈展开会调用r2和r1的析构
}
// 如果r2的析构抛异常,程序直接terminate
正确做法
cpp
class Resource
{
public:
~Resource() noexcept // 明确标记不抛异常
{
try
{
// 清理资源
cleanup();
}
catch (...)
{
// 吞掉异常,记录日志
cerr << "析构时发生错误,但已被处理" << endl;
}
}
};
多资源释放的安全做法
cpp
class MultiResource
{
public:
~MultiResource()
{
// 即使某个资源释放失败,也要继续释放其他资源
try { delete _resource1; } catch(...) {}
try { delete _resource2; } catch(...) {}
try { close(_file); } catch(...) {}
try { _mutex.unlock(); } catch(...) {}
}
private:
Resource* _resource1;
Resource* _resource2;
int _file;
mutex _mutex;
};
三、异常规范
3.1 C++98的异常规范(已废弃)
C++98允许在函数声明后使用throw()指定可能抛出的异常类型:
cpp
// 表示这个函数只会抛出bad_alloc异常
void* operator new(size_t size) throw(bad_alloc);
// 表示这个函数不会抛出异常
void* operator delete(size_t size, void* ptr) throw();
// 表示可能抛出多种异常
void func() throw(int, double, string);
为什么被废弃?
- 过于复杂,难以维护
- 编译器检查不严格
- 运行时检查有性能开销
- 实际使用中问题多多
3.2 C++11的noexcept(推荐)
C++11简化了异常规范,只提供两种状态:
- 什么都不写:可能抛异常
noexcept:不会抛异常
基本用法
cpp
// 承诺不会抛异常
void func1() noexcept
{
// ...
}
// 可能会抛异常(默认行为)
void func2()
{
// ...
}
编译器不会在编译时强制检查
cpp
void func() noexcept
{
throw runtime_error("error"); // 编译通过(可能有警告)
}
int main()
{
try
{
func(); // 运行时会调用terminate(),程序终止
}
catch (...)
{
cout << "不会执行到这里" << endl;
}
return 0;
}
3.3 noexcept作为运算符
noexcept还可以作为运算符,检查一个表达式是否会抛异常:
cpp
double Divide(int a, int b)
{
if (b == 0)
throw "Division by zero!";
return (double)a / b;
}
int Add(int a, int b) noexcept
{
return a + b;
}
int main()
{
int i = 0;
cout << noexcept(i++) << endl; // 1,基本操作不抛异常
cout << noexcept(Add(1, 2)) << endl; // 1,noexcept函数
cout << noexcept(Divide(1, 2)) << endl; // 0,可能抛异常
cout << noexcept(Divide(1, 0)) << endl; // 0,函数声明未标记noexcept
return 0;
}
输出
bash
1
1
0
0
注意:noexcept运算符在编译期求值,它不会真正执行表达式,只是检查表达式是否被声明为noexcept。
3.4 什么时候使用noexcept
应该使用noexcept的场景
- 析构函数(默认就是noexcept)
cpp
~MyClass() noexcept // 明确标记
{
// 清理资源
}
- 移动构造和移动赋值
cpp
class MyClass
{
public:
MyClass(MyClass&& other) noexcept
{
// 移动操作通常不应抛异常
}
MyClass& operator=(MyClass&& other) noexcept
{
// ...
return *this;
}
};
为什么移动操作应该noexcept?因为STL容器在扩容时,如果移动构造是noexcept的,就会使用移动;否则为了保证异常安全,会使用拷贝。
- swap函数
cpp
void swap(MyClass& other) noexcept
{
// swap操作不应该抛异常
}
- 不会失败的简单操作
cpp
int size() const noexcept { return _size; }
bool empty() const noexcept { return _size == 0; }
不要滥用noexcept
如果不确定函数是否会抛异常,不要轻易加noexcept,因为违反承诺会导致程序终止。
四、C++标准库异常体系
4.1 标准异常继承体系
C++标准库定义了一套完整的异常继承体系,基类是std::exception:
bash
exception (基类)
├── logic_error (逻辑错误)
│ ├── invalid_argument
│ ├── domain_error
│ ├── length_error
│ ├── out_of_range
│ └── future_error
├── runtime_error (运行时错误)
│ ├── range_error
│ ├── overflow_error
│ ├── underflow_error
│ └── system_error
├── bad_alloc (内存分配失败)
├── bad_cast (类型转换失败)
├── bad_typeid
└── bad_exception
4.2 exception基类
cpp
class exception
{
public:
exception() noexcept;
exception(const exception&) noexcept;
exception& operator=(const exception&) noexcept;
virtual ~exception() noexcept;
virtual const char* what() const noexcept;
};
关键方法
what():返回异常描述字符串,是虚函数,派生类可以重写
4.3 常用标准异常
logic_error:逻辑错误
表示程序逻辑上的错误,理论上可以通过检查代码避免:
cpp
#include <stdexcept>
// invalid_argument:无效参数
void setAge(int age)
{
if (age < 0 || age > 150)
{
throw invalid_argument("年龄必须在0-150之间");
}
_age = age;
}
// out_of_range:越界访问
char& at(size_t index)
{
if (index >= _size)
{
throw out_of_range("索引越界");
}
return _data[index];
}
// length_error:长度错误
void resize(size_t n)
{
if (n > max_size())
{
throw length_error("超出最大长度");
}
// ...
}
runtime_error:运行时错误
表示运行时才能检测到的错误:
cpp
// overflow_error:溢出
void compute()
{
if (result > INT_MAX)
{
throw overflow_error("计算结果溢出");
}
}
// 一般的运行时错误
void connect()
{
if (!connected)
{
throw runtime_error("连接失败");
}
}
bad_alloc:内存分配失败
cpp
try
{
int* p = new int[1000000000000]; // 分配巨大内存
}
catch (const bad_alloc& e)
{
cout << "内存分配失败: " << e.what() << endl;
}
4.4 使用标准异常的好处
统一的接口
所有标准异常都继承自exception,可以统一捕获:
cpp
int main()
{
try
{
// 各种可能抛出标准异常的操作
vector<int> v(5);
v.at(10); // 抛出out_of_range
}
catch (const exception& e) // 捕获所有标准异常
{
cout << "异常: " << e.what() << endl;
}
return 0;
}
良好的语义
通过异常类型就能知道错误的大致类别,不需要查错误码。
可扩展性
可以继承标准异常类,添加自己的信息:
cpp
class FileException : public runtime_error
{
public:
FileException(const string& msg, const string& filename)
: runtime_error(msg)
, _filename(filename)
{}
const string& filename() const { return _filename; }
private:
string _filename;
};
五、异常使用的最佳实践
5.1 何时使用异常
应该使用异常的场景
- 真正的错误情况
cpp
void openFile(const string& filename)
{
FILE* file = fopen(filename.c_str(), "r");
if (!file)
{
throw runtime_error("无法打开文件: " + filename);
}
}
- 构造函数中的错误
构造函数没有返回值,异常是报告错误的唯一方式:
cpp
class Socket
{
public:
Socket(const string& ip, int port)
{
_fd = connect(ip, port);
if (_fd < 0)
{
throw runtime_error("连接失败");
}
}
};
- 跨越多层调用的错误传播
cpp
// 底层函数
void lowLevelFunc()
{
if (error)
throw runtime_error("底层错误");
}
// 中间层函数(不需要处理,自动传播)
void midLevelFunc()
{
lowLevelFunc();
}
// 高层函数(统一处理)
void highLevelFunc()
{
try
{
midLevelFunc();
}
catch (const exception& e)
{
// 处理错误
}
}
不应该使用异常的场景
- 正常的控制流程
cpp
// 错误示例:用异常控制循环
try
{
for (int i = 0; ; ++i)
{
if (i == 10)
throw i;
}
}
catch (int n)
{
cout << n << endl;
}
- 高频操作
异常的开销比较大,不适合在高频调用的代码中使用:
cpp
// 不好:在循环中频繁抛异常
for (int i = 0; i < 1000000; ++i)
{
try
{
if (condition)
throw "error";
}
catch (...)
{
}
}
- 可以通过返回值解决的简单情况
cpp
// 简单情况用返回值更好
bool isValid(int x)
{
return x >= 0 && x <= 100;
}
// 不需要这样
void checkValid(int x)
{
if (x < 0 || x > 100)
throw invalid_argument("无效值");
}
5.2 异常处理的建议
建议1:按引用捕获
cpp
// 推荐:按const引用捕获
catch (const exception& e)
{
cout << e.what() << endl;
}
// 不推荐:按值捕获(会发生对象切片)
catch (exception e) // 切片!
{
}
建议2:从具体到一般的顺序
cpp
try
{
// ...
}
catch (const out_of_range& e) // 具体异常
{
}
catch (const logic_error& e) // 较一般的异常
{
}
catch (const exception& e) // 最一般的异常
{
}
catch (...) // 最后的保底
{
}
建议3:main函数的保护
cpp
int main()
{
try
{
// 程序主逻辑
return runApplication();
}
catch (const exception& e)
{
cerr << "程序异常: " << e.what() << endl;
return 1;
}
catch (...)
{
cerr << "未知异常" << endl;
return 2;
}
}
建议4:记录日志
cpp
catch (const exception& e)
{
// 记录详细的错误信息
logError("异常发生",
"类型", typeid(e).name(),
"信息", e.what(),
"文件", __FILE__,
"行号", __LINE__);
// 然后决定是处理还是重新抛出
}
建议5:提供异常安全保证
设计类时,提供以下级别的异常安全保证之一:
- 不抛保证:承诺不抛异常(noexcept)
- 强保证:要么操作成功,要么对象状态不变
- 基本保证:如果抛异常,对象仍处于有效状态
- 无保证:抛异常后对象状态未定义(尽量避免)
5.3 性能考虑
异常的性能特点
- 正常路径开销小:如果不抛异常,现代编译器的实现几乎没有开销
- 异常路径开销大:抛出和捕获异常的开销比较大
- 结论:异常适合处理真正的异常情况,不适合正常控制流
性能测试
cpp
#include <chrono>
// 使用异常
void testException(bool throwIt)
{
if (throwIt)
throw 1;
}
// 使用返回值
bool testReturn(bool error)
{
return !error;
}
int main()
{
using namespace std::chrono;
const int COUNT = 1000000;
// 测试无异常情况
auto start = high_resolution_clock::now();
for (int i = 0; i < COUNT; ++i)
{
try { testException(false); }
catch(...) {}
}
auto end = high_resolution_clock::now();
cout << "无异常: "
<< duration_cast<milliseconds>(end - start).count()
<< "ms" << endl;
// 测试有异常情况
start = high_resolution_clock::now();
for (int i = 0; i < COUNT; ++i)
{
try { testException(true); }
catch(...) {}
}
end = high_resolution_clock::now();
cout << "有异常: "
<< duration_cast<milliseconds>(end - start).count()
<< "ms" << endl;
// 测试返回值
start = high_resolution_clock::now();
for (int i = 0; i < COUNT; ++i)
{
testReturn(false);
}
end = high_resolution_clock::now();
cout << "返回值: "
<< duration_cast<milliseconds>(end - start).count()
<< "ms" << endl;
return 0;
}
六、总结
6.1 全系列回顾
通过两篇文章,我们系统学习了C++异常机制:
第一篇:基础篇
- 异常的概念与优势
- 异常的抛出与捕获
- 栈展开机制
- 异常匹配规则
- 异常继承体系设计
第二篇:高级篇
- 异常重新抛出技巧
- 异常安全与RAII
- 异常规范(noexcept)
- 标准库异常体系
- 最佳实践与性能考虑
6.2 核心要点
异常的本质
异常是一种结构化的错误处理机制,它将错误检测和错误处理分离,让代码更清晰、更易维护。
异常安全的关键
使用RAII技术管理资源,利用析构函数的自动调用来保证资源正确释放。
异常规范的现代实践
- 析构函数默认noexcept
- 移动操作建议noexcept
- 其他函数按需标记
最佳实践总结
- 只在真正的异常情况使用异常
- 按const引用捕获异常
- 从具体到一般安排catch顺序
- 使用标准异常或继承标准异常
- 注意异常安全,使用RAII
- 析构函数不要抛异常
6.3 继续学习
要深入掌握异常机制,建议:
-
阅读经典书籍
- 《Effective C++》
- 《More Effective C++》
- 《C++ Coding Standards》
-
学习标准库实现
- 研究STL容器如何保证异常安全
- 学习智能指针的RAII实现
-
实践项目
- 在实际项目中应用异常处理
- 设计自己的异常体系
- 注重代码的异常安全性
通过这两篇文章的学习,我们全面掌握了C++异常机制。异常是现代C++不可或缺的特性,正确使用异常能让程序更加健壮和易于维护。希望这个系列对你有所帮助!
C++异常机制系列到此完结!感谢你的阅读,如果对你有帮助,记得点赞、收藏、分享!期待在评论区看到你的学习心得!❤️
