第三章 异常(一)
条款9:利用destructors避免泄露资源
一、核心概念解析
首先,我们要理解这个条款解决的核心问题:手动管理资源(如内存、文件句柄、网络连接等)时,容易因忘记释放、程序提前退出(如异常)等原因导致资源泄露。
C++ 的析构函数(destructor)有一个关键特性:当一个对象的生命周期结束(如离开作用域、被 delete)时,其析构函数会自动调用。利用这个特性,我们可以将资源的释放逻辑封装到析构函数中,让资源的生命周期与对象绑定 ------ 这就是 RAII(Resource Acquisition Is Initialization,资源获取即初始化)的核心思想。
问题场景:手动管理资源的风险
先看一个反例,直观感受资源泄露的问题:
c++
#include <iostream>
#include <string>
// 模拟一个需要手动释放的资源(如动态内存)
void createResource(std::string*& ptr) {
ptr = new std::string("我是需要释放的资源");
}
void releaseResource(std::string* ptr) {
delete ptr;
ptr = nullptr;
}
void riskyFunction(bool throwException) {
std::string* res = nullptr;
createResource(res); // 获取资源
// 模拟业务逻辑:如果抛出异常,后续的releaseResource不会执行
if (throwException) {
throw std::runtime_error("业务逻辑异常");
}
// 即使没有异常,也可能忘记写这行,导致内存泄露
releaseResource(res);
}
int main() {
try {
riskyFunction(true); // 传入true触发异常,资源泄露
} catch (const std::exception& e) {
std::cout << "捕获异常:" << e.what() << std::endl;
}
// 程序结束后,res指向的内存未被释放,发生泄露
return 0;
}
问题分析:
1.如果riskyFunction中抛出异常,releaseResource不会执行,资源泄露;
2.即使没有异常,手动调用releaseResource容易遗漏,导致泄露;
3.代码需要手动配对 "获取 - 释放",心智负担重。
二、解决方案:用析构函数自动释放资源
我们可以封装一个资源管理类,在构造函数中获取资源,析构函数中释放资源。只要这个类的对象离开作用域,析构函数就会自动调用,资源被释放,从根本上避免泄露。
代码示例:实现一个简单的资源管理类
c++
#include <iostream>
#include <string>
#include <stdexcept>
// 资源管理类:遵循RAII原则
class ResourceGuard {
private:
std::string* m_resource; // 管理的资源(这里以动态字符串为例)
public:
// 构造函数:获取资源(资源获取即初始化)
explicit ResourceGuard(const std::string& content)
: m_resource(new std::string(content)) {
std::cout << "资源已获取,地址:" << m_resource << std::endl;
}
// 析构函数:自动释放资源(无论正常退出还是异常退出)
~ResourceGuard() {
if (m_resource != nullptr) {
delete m_resource;
m_resource = nullptr;
std::cout << "资源已释放" << std::endl;
}
}
// 禁用拷贝构造和拷贝赋值(避免浅拷贝导致重复释放)
ResourceGuard(const ResourceGuard&) = delete;
ResourceGuard& operator=(const ResourceGuard&) = delete;
// 提供访问资源的接口(可选)
std::string& getResource() const {
if (m_resource == nullptr) {
throw std::runtime_error("资源已释放");
}
return *m_resource;
}
};
// 安全的函数:使用资源管理类
void safeFunction(bool throwException) {
// 创建资源管理对象,构造函数获取资源
ResourceGuard guard("我是受保护的资源");
// 模拟业务逻辑:即使抛出异常,guard的析构函数仍会执行
if (throwException) {
throw std::runtime_error("业务逻辑异常,但资源不会泄露");
}
// 正常使用资源
std::cout << "资源内容:" << guard.getResource() << std::endl;
// 函数结束时,guard离开作用域,析构函数自动释放资源
}
int main()
{
try
{
safeFunction(true); // 触发异常
}
catch (const std::exception& e)
{
std::cout << "捕获异常:" << e.what() << std::endl;
}
std::cout << "程序正常结束" << std::endl;
return 0;
}
关键细节解释:
1.RAII 核心 :ResourceGuard的构造函数负责 "获取资源",析构函数负责 "释放资源",资源的生命周期与guard对象绑定;
2.异常安全 :即使safeFunction抛出异常,guard对象的析构函数仍会被调用(C++ 保证栈上对象的析构函数在异常展开时执行),资源不会泄露;
3.禁用拷贝 :如果允许拷贝,多个ResourceGuard对象会管理同一份资源,析构时会重复释放导致崩溃,因此禁用拷贝构造和拷贝赋值(C++11 后也可使用移动语义);
4.通用性 :这个思路不仅适用于内存,还适用于文件句柄、锁、网络连接等所有需要手动释放的资源(比如std::fstream自动关闭文件、std::lock_guard自动释放锁,都是这个原理)。
进阶:使用标准库的智能指针(更推荐)
实际开发中,我们不需要自己写资源管理类,C++ 标准库提供了现成的智能指针(std::unique_ptr/std::shared_ptr),它们的底层就是利用析构函数自动释放资源:
C++
#include <iostream>
#include <string>
#include <memory> // 包含智能指针头文件
#include <stdexcept>
void smarterFunction(bool throwException) {
// std::unique_ptr:独占式智能指针,析构时自动delete
std::unique_ptr<std::string> res = std::make_unique<std::string>("智能指针管理的资源");
if (throwException) {
throw std::runtime_error("异常发生,但智能指针会自动释放资源");
}
std::cout << "资源内容:" << *res << std::endl;
}
int main() {
try {
smarterFunction(true);
} catch (const std::exception& e) {
std::cout << "捕获异常:" << e.what() << std::endl;
}
return 0;
}
std::unique_ptr是条款 9 的最佳实践落地 ------ 它完全遵循 RAII,无需手动管理,且性能几乎与裸指针一致。
总结
- 核心思想:将资源的释放逻辑封装到析构函数中,利用析构函数 "自动调用" 的特性,避免手动释放资源的遗漏或异常导致的泄露(RAII 原则);
- 关键做法:不要直接管理裸资源,而是用对象(如自定义资源管理类、标准库智能指针)包裹资源,让对象的生命周期与资源绑定;
- 实践推荐 :优先使用 C++ 标准库提供的智能指针(
std::unique_ptr/std::shared_ptr),而非手写资源管理类,避免重复造轮子且更安全。
条款10:在constructors内阻止资源泄露(resource leak)
一、核心问题:构造函数的特殊性
C++ 的构造函数没有返回值,且如果在构造过程中抛出异常,当前对象的析构函数不会被调用 。这意味着:如果构造函数中分配了资源(如动态内存、文件句柄、锁、网络连接等),但在资源分配后、构造完成前抛出了异常,这些已分配的资源就无法被析构函数释放,从而导致资源泄露。
二、解决方案:RAII(资源获取即初始化)
条款 10 的核心解决方案是RAII(Resource Acquisition Is Initialization):将资源的生命周期绑定到对象的生命周期 ------ 资源在对象构造时获取,在对象析构时释放。具体来说:
1.把资源封装到独立的 "资源管理类" 中;
2.在构造函数中只创建资源管理类的对象,而非直接操作裸资源;
3.即使构造函数抛出异常,资源管理类的析构函数仍会被调用,从而保证资源释放。
三、代码示例:反例(有资源泄露)+ 正例(无泄露)
c++
#include <iostream>
#include <stdexcept>
using namespace std;
// 模拟一个需要手动释放的资源(如动态内存、文件句柄)
class Resource {
public:
Resource() { cout << "Resource 分配成功\n"; }
~Resource() { cout << "Resource 释放成功\n"; } // 析构释放资源
void use() const { /* 资源使用逻辑 */ }
};
// 有资源泄露风险的类
class BadClass {
private:
Resource* res1; // 裸指针管理资源1
Resource* res2; // 裸指针管理资源2
public:
BadClass() {
// 第一步:分配资源1(成功)
res1 = new Resource();
// 第二步:模拟构造过程中抛出异常(比如资源2分配失败、逻辑错误)
throw runtime_error("构造函数执行中发生异常");
// 第三步:分配资源2(永远不会执行)
res2 = new Resource();
}
~BadClass() {
// 析构函数不会被调用!因为构造函数抛异常,对象未完全构造
delete res1;
delete res2;
cout << "BadClass 析构,释放所有资源\n";
}
};
int main() {
try {
BadClass obj; // 构造时抛异常
} catch (const exception& e) {
cout << "捕获异常:" << e.what() << endl;
}
// 输出:Resource 分配成功 → 捕获异常 → 无"Resource 释放成功"
// 结论:res1的资源永远无法释放,造成泄露
return 0;
}
2. 正例:用 RAII 封装资源(解决泄露)
核心思路:用智能指针(如 std::unique_ptr) 替代裸指针 ------ 智能指针是 RAII 的典型实现,其析构函数会自动释放管理的资源,即使构造函数抛异常。
C++
#include <iostream>
#include <stdexcept>
#include <memory> // 包含智能指针头文件
using namespace std;
// 待管理的资源(同上)
class Resource {
public:
Resource() { cout << "Resource 分配成功\n"; }
~Resource() { cout << "Resource 释放成功\n"; }
void use() const { /* 资源使用逻辑 */ }
};
// 安全的类:用RAII(智能指针)管理资源
class GoodClass {
private:
// 用std::unique_ptr(独占所有权)替代裸指针,自动管理资源
unique_ptr<Resource> res1;
unique_ptr<Resource> res2;
public:
GoodClass() {
// 第一步:分配资源1(封装到unique_ptr中)
res1 = make_unique<Resource>(); // C++14及以上,等价于 unique_ptr<Resource>(new Resource())
// 第二步:模拟构造过程中抛异常
throw runtime_error("构造函数执行中发生异常");
// 第三步:分配资源2(不会执行)
res2 = make_unique<Resource>();
}
~GoodClass() {
// 即使析构函数不手动释放,unique_ptr也会自动释放资源
cout << "GoodClass 析构\n";
}
};
int main() {
try {
GoodClass obj; // 构造时抛异常
} catch (const exception& e) {
cout << "捕获异常:" << e.what() << endl;
}
// 输出:Resource 分配成功 → 捕获异常 → Resource 释放成功
// 结论:res1的资源被unique_ptr的析构函数自动释放,无泄露
return 0;
}

3. 扩展:自定义 RAII 资源管理类(理解底层原理)
如果需要管理非内存资源(如文件句柄、锁),可以自定义 RAII 类:
C++
#include <iostream>
#include <stdexcept>
#include <cstdio> // FILE相关头文件
using namespace std;
// 自定义RAII类:管理文件句柄(非内存资源)
class FileHandle {
private:
FILE* file; // 裸句柄(仅在RAII类内部使用)
public:
// 构造:获取资源(打开文件)
FileHandle(const char* filename, const char* mode) {
file = fopen(filename, mode);
if (!file) {
throw runtime_error("文件打开失败");
}
cout << "文件 " << filename << " 打开成功\n";
}
// 析构:释放资源(关闭文件)
~FileHandle() {
if (file) {
fclose(file);
cout << "文件关闭成功\n";
}
}
// 禁用拷贝(避免资源重复释放)
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// 提供资源访问接口
FILE* get() const { return file; }
};
// 使用自定义RAII类的业务类
class FileProcessor {
private:
FileHandle fh; // RAII对象,绑定文件资源
public:
FileProcessor(const char* filename) : fh(filename, "w") {
// 模拟构造过程中抛异常
throw runtime_error("FileProcessor构造异常");
}
};
int main() {
try {
FileProcessor fp("test.txt");
} catch (const exception& e) {
cout << "捕获异常:" << e.what() << endl;
}
// 输出:文件 test.txt 打开成功 → 捕获异常 → 文件关闭成功
// 结论:即使构造抛异常,FileHandle的析构仍会执行,文件句柄无泄露
return 0;
}

总结
- 核心风险:构造函数抛异常时,对象未完全构造,析构函数不会执行,直接管理的裸资源会泄露;
- 核心方案 :采用RAII思想,将资源封装到 "资源管理类" 中(如 std::unique_ptr、std::shared_ptr,或自定义 RAII 类),利用资源管理类的析构函数自动释放资源;
- 最佳实践 :在构造函数中避免直接操作裸资源,优先使用标准库提供的智能指针,自定义资源(如文件、锁)需封装为独立 RAII 类,杜绝构造过程中的资源泄露。
关键点回顾:
- 构造函数抛异常 → 析构函数不执行 → 裸资源泄露;
- RAII:资源绑定到对象生命周期,构造获取、析构释放;
- 智能指针是 RAII 的 "现成方案",自定义 RAII 类适配非内存资源。