Menu
CoddyTech

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 能将容易出错的手动资源管理转变为安全、自动的清理。

challenge icon

挑战

简单

让我们使用 RAII 构建一个连接池管理器,以安全地管理数据库连接。在实际应用中,数据库连接是开销很大的资源,必须妥善获取和释放。你将创建一个 RAII 包装器,确保连接始终归还到连接池,即使发生异常或代码路径变得复杂也是如此。

你将把代码分成三个文件:

  • ConnectionPool.h:创建一个简单的连接池,用于管理数量有限的连接。

    你的 ConnectionPool 类应跟踪可用连接的数量(初始容量由构造函数接收)。实现以下功能:

    • acquire():如果有可用连接,就减少计数并打印 Connection acquired (X available),其中 X 是剩余数量;成功时返回 true,没有可用连接时返回 false
    • release():增加可用连接的数量,并打印 Connection released (X available)
    • available():返回当前可用连接的数量
  • ConnectionGuard.h:构建一个 RAII 包装器,用于安全地管理单个连接。

    你的 ConnectionGuard 类应体现 RAII 模式。它应:

    • 在构造函数中接收一个 ConnectionPool 的引用,并尝试获取连接
    • 保存获取连接是否成功
    • 提供 isConnected() 方法,用于检查守卫是否持有有效连接
    • 在析构函数中自动将连接释放回连接池(仅当成功获取连接时)
    • 删除复制构造函数和复制赋值运算符,以防止资源被重复使用(考虑五法则)

    析构函数运行时,如果持有连接,请在调用连接池的 release 之前打印 Guard releasing connection。

  • main.cpp:通过作用域演示 RAII 的自动清理功能。

    读取两个输入值:

    1. 连接池容量(整数)
    2. 请求的连接数量(整数)

    使用给定的容量创建一个 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;
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C++ 在线编译器