RAII 作为一种模式
本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 102 课,共 104 课。
RAII (Resource Acquisition Is Initialization) 不只是一种 C++ 惯用法,更是一种强大的设计模式,它将资源管理与对象的生命周期绑定在一起。你已经在智能指针中见过 RAII,但这种模式适用于任何资源:文件句柄、网络连接、互斥锁或数据库事务。
核心思想很简单:在构造函数中获取资源,在析构函数中释放资源。由于 C++ 保证对象超出作用域时会运行析构函数,因此清理会自动进行,即使发生异常也一样:
#include <iostream>
#include <fstream>
class FileGuard {
std::ofstream file;
public:
FileGuard(const std::string& filename) : file(filename) {
if (!file.is_open()) {
std::cout << "Failed to open file\n";
}
}
void write(const std::string& text) {
if (file.is_open()) file << text;
}
~FileGuard() {
if (file.is_open()) {
file.close();
std::cout << "File closed automatically\n";
}
}
};
int main() {
{
FileGuard guard("output.txt");
guard.write("Hello RAII");
} // 这里会调用析构函数,关闭文件
std::cout << "After scope\n";
}在多线程代码中管理锁时,RAII 的优势尤为明显。标准库中的 std::lock_guard 遵循这种模式:构造时获取互斥锁,析构时释放互斥锁,避免因忘记解锁而导致死锁。
实现 RAII 类时,记得删除或正确实现复制/移动操作(五法则),以防止资源重复或重复释放问题。RAII 能将容易出错的手动资源管理转变为安全、自动的清理。
挑战
简单让我们使用 RAII 构建一个连接池管理器,以安全地管理数据库连接。在实际应用中,数据库连接是开销很大的资源,必须妥善获取和释放。你将创建一个 RAII 包装器,确保连接始终归还到连接池,即使发生异常或代码路径变得复杂也是如此。
你将把代码分成三个文件:
ConnectionPool.h:创建一个简单的连接池,用于管理数量有限的连接。你的
ConnectionPool类应跟踪可用连接的数量(初始容量由构造函数接收)。实现以下功能:acquire():如果有可用连接,就减少计数并打印Connection acquired (X available),其中 X 是剩余数量;成功时返回true,没有可用连接时返回falserelease():增加可用连接的数量,并打印Connection released (X available)available():返回当前可用连接的数量
ConnectionGuard.h:构建一个 RAII 包装器,用于安全地管理单个连接。你的
ConnectionGuard类应体现 RAII 模式。它应:- 在构造函数中接收一个
ConnectionPool的引用,并尝试获取连接 - 保存获取连接是否成功
- 提供
isConnected()方法,用于检查守卫是否持有有效连接 - 在析构函数中自动将连接释放回连接池(仅当成功获取连接时)
- 删除复制构造函数和复制赋值运算符,以防止资源被重复使用(考虑五法则)
析构函数运行时,如果持有连接,请在调用连接池的 release 之前打印
Guard releasing connection。- 在构造函数中接收一个
main.cpp:通过作用域演示 RAII 的自动清理功能。读取两个输入值:
- 连接池容量(整数)
- 请求的连接数量(整数)
使用给定的容量创建一个
ConnectionPool。然后,在一个嵌套作用域中(使用花括号),创建指定数量的ConnectionGuard对象,并将它们存储在一个 vector 中。对于每个守卫,打印它是否成功连接:- 如果已连接:
Guard N: Connected - 如果未连接:
Guard N: Failed to connect
(N 从 1 开始)
作用域结束后(守卫被销毁),打印
After scope: X connections available,显示连接池的最终状态。
例如,输入 2 和 3 时:
Connection acquired (1 available)
Guard 1: Connected
Connection acquired (0 available)
Guard 2: Connected
Guard 3: Failed to connect
Guard releasing connection
Connection released (1 available)
Guard releasing connection
Connection released (2 available)
After scope: 2 connections available输入 3 和 2 时:
Connection acquired (2 available)
Guard 1: Connected
Connection acquired (1 available)
Guard 2: Connected
Guard releasing connection
Connection released (2 available)
Guard releasing connection
Connection released (3 available)
After scope: 3 connections available注意,当守卫离开作用域时,连接会自动释放。你不会在主代码中显式调用 release。析构函数会按照构造的相反顺序运行(最后创建的守卫最先销毁),并且每个已获取的连接都会被确保归还。这就是 RAII 的优势:无论作用域如何退出,资源都会自动且可靠地清理。
亲手试试
#include <iostream>
#include <vector>
#include "ConnectionPool.h"
#include "ConnectionGuard.h"
using namespace std;
int main() {
// 读取输入
int capacity;
int numConnections;
cin >> capacity;
cin >> numConnections;
// TODO:创建一个容量为给定值的 ConnectionPool
// TODO:用花括号创建一个嵌套作用域
{
// TODO:创建一个 vector 来存储 ConnectionGuard 对象
// 提示:你需要使用指针或智能指针,因为 ConnectionGuard
// 的拷贝构造函数已被删除
// TODO:通过循环创建 numConnections 个 guard
// 对于每个 guard,打印以下两者之一:
// "Guard N: Connected" 或 "Guard N: Failed to connect"
// 其中 N 从 1 开始
}
// 作用域结束时,guard 会在这里被销毁
// TODO:打印 "After scope: X connections available"
return 0;
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: C++ 在线编译器