Effective C++ 条款13:以对象管理资源

获得资源后立刻放进管理对象内。管理对象运用析构函数确保资源被释放。RAII(Resource Acquisition Is Initialization)是 C++ 资源管理的核心原则。

一、资源管理的问题

在 C++ 中,手动管理资源容易出错:

void processFile(const char* filename) {
    FILE* file = fopen(filename, "r");
    // ... 各种操作 ...
    if (some_error) {
        return;  // 糟糕!忘记 fclose 了
    }
    // ... 更多操作 ...
    fclose(file);  // 如果前面有 return、异常,这里不会执行
}

常见问题:

问题 原因
内存泄漏 new 后没有 delete
文件句柄泄漏 打开后忘记关闭
锁未释放 加锁后忘记解锁
网络连接未关闭 异常导致跳过关闭代码
数据库事务未提交/回滚 提前返回导致事务悬挂

二、RAII 原则

RAII(Resource Acquisition Is Initialization)的核心思想:

资源在构造函数中获得,在析构函数中释放。

class FileGuard {
public:
    explicit FileGuard(const char* filename) 
        : file_(fopen(filename, "r")) {
        if (!file_) {
            throw std::runtime_error("Failed to open file");
        }
    }
    
    ~FileGuard() {
        if (file_) {
            fclose(file_);
        }
    }
    
    FILE* get() const { return file_; }

private:
    FILE* file_;
};

// 使用
void processFileSafe(const char* filename) {
    FileGuard file(filename);  // 资源获取
    // ... 各种操作 ...
    if (some_error) {
        return;  // 安全!FileGuard 的析构函数会自动 fclose
    }
    // ... 更多操作 ...
}  // 函数结束时,file 的析构函数自动释放资源

三、智能指针:RAII 的最佳实践

C++11 起提供了三种智能指针,它们是 RAII 原则的标准实现:

1. std::unique_ptr:独占所有权

#include <memory>

void useUniquePtr() {
    // 独占所有权,不能复制,只能移动
    std::unique_ptr<int> ptr(new int(42));
    
    // 自动释放内存
    auto ptr2 = std::make_unique<int>(100);  // C++14 推荐
    
    // 转移所有权
    std::unique_ptr<int> ptr3 = std::move(ptr);
    // 现在 ptr 为空,ptr3 拥有资源
    
    // 自定义删除器
    auto fileDeleter = [](FILE* f) { fclose(f); };
    std::unique_ptr<FILE, decltype(fileDeleter)> 
        file(fopen("test.txt", "r"), fileDeleter);
}  // 自动调用删除器释放资源

2. std::shared_ptr:共享所有权

#include <memory>

void useSharedPtr() {
    // 多个指针共享同一资源
    std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
    {
        std::shared_ptr<int> ptr2 = ptr1;  // 引用计数 +1
        std::cout << *ptr2 << std::endl;   // 输出 42
    }  // ptr2 销毁,引用计数 -1
    // ptr1 仍然有效
}  // ptr1 销毁,引用计数为 0,释放资源

3. std::weak_ptr:弱引用

#include <memory>

void useWeakPtr() {
    std::shared_ptr<int> shared = std::make_shared<int>(42);
    std::weak_ptr<int> weak = shared;  // 不增加引用计数
    
    if (auto locked = weak.lock()) {   // 尝试获取强引用
        std::cout << *locked << std::endl;
    }
}  // 解决循环引用问题

四、智能指针对比

特性 unique_ptr shared_ptr weak_ptr
所有权 独占 共享 无(弱引用)
复制 禁止 允许 允许
移动 允许 允许 允许
引用计数 不影响计数
内存开销 最小 额外分配控制块 最小
适用场景 独占资源 共享资源 打破循环引用

五、自定义 RAII 类

当标准智能指针不满足需求时,可以自定义 RAII 类:

示例1:互斥锁管理器

#include <mutex>

class LockGuard {
public:
    explicit LockGuard(std::mutex& mutex) : mutex_(mutex) {
        mutex_.lock();  // 构造时加锁
    }
    
    ~LockGuard() {
        mutex_.unlock();  // 析构时解锁
    }
    
    // 禁止复制
    LockGuard(const LockGuard&) = delete;
    LockGuard& operator=(const LockGuard&) = delete;

private:
    std::mutex& mutex_;
};

// 使用
std::mutex mtx;

void safeFunction() {
    LockGuard lock(mtx);  // 加锁
    // ... 临界区代码 ...
}  // 自动解锁

C++11 标准库已提供 std::lock_guardstd::unique_lock,生产环境优先使用标准库。

示例2:数据库事务管理器

class Transaction {
public:
    explicit Transaction(DatabaseConnection& conn)
        : conn_(conn), committed_(false) {
        conn_.beginTransaction();
    }
    
    void commit() {
        conn_.commit();
        committed_ = true;
    }
    
    ~Transaction() {
        if (!committed_) {
            conn_.rollback();  // 未提交则回滚
        }
    }

private:
    DatabaseConnection& conn_;
    bool committed_;
};

// 使用
void transferMoney(Account& from, Account& to, double amount) {
    Transaction tx(dbConnection);
    
    from.withdraw(amount);
    to.deposit(amount);
    
    tx.commit();  // 成功提交
}  // 如果中途异常,自动回滚

示例3:动态数组管理

template<typename T>
class ArrayGuard {
public:
    explicit ArrayGuard(size_t size) 
        : data_(new T[size]), size_(size) {}
    
    ~ArrayGuard() { delete[] data_; }
    
    T& operator[](size_t index) { return data_[index]; }
    const T& operator[](size_t index) const { return data_[index]; }
    size_t size() const { return size_; }
    
    // 支持移动语义
    ArrayGuard(ArrayGuard&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }
    
    // 禁止复制(或实现深拷贝)
    ArrayGuard(const ArrayGuard&) = delete;
    ArrayGuard& operator=(const ArrayGuard&) = delete;

private:
    T* data_;
    size_t size_;
};

六、实际应用场景

场景1:工厂函数返回智能指针

#include <memory>

class Image {
public:
    void render() {}
};

// 工厂函数 - 返回 unique_ptr 明确转移所有权
std::unique_ptr<Image> createImage(const std::string& path) {
    auto img = std::make_unique<Image>();
    // ... 加载图片 ...
    return img;  // 移动语义,无需复制
}

// 使用
void displayImage() {
    auto img = createImage("photo.jpg");
    img->render();
}  // 自动释放 Image

场景2:PIMPL 惯用法

// Widget.h - 头文件中无需包含完整定义
class Widget {
public:
    Widget();
    ~Widget();  // 需要显式声明,因为 unique_ptr 需要完整类型
    
    Widget(Widget&&) noexcept;            // 移动构造
    Widget& operator=(Widget&&) noexcept; // 移动赋值
    
    void doSomething();

private:
    class Impl;  // 前向声明
    std::unique_ptr<Impl> pImpl;  // RAII 管理实现
};

// Widget.cpp
class Widget::Impl {
public:
    void doSomething() { /* ... */ }
    std::vector<int> data;
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;

void Widget::doSomething() {
    pImpl->doSomething();
}

场景3:资源池管理

#include <memory>
#include <stack>

class Connection {
public:
    void query(const std::string& sql) {}
};

class ConnectionPool {
public:
    using ConnectionPtr = std::shared_ptr<Connection>;
    
    ConnectionPtr acquire() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (!available_.empty()) {
            auto conn = available_.top();
            available_.pop();
            // 自定义删除器:归还连接而非真正释放
            return ConnectionPtr(conn.get(), [this](Connection* c) {
                this->release(c);
            });
        }
        return std::make_shared<Connection>();
    }

private:
    void release(Connection* conn) {
        std::lock_guard<std::mutex> lock(mutex_);
        available_.push(
            std::shared_ptr<Connection>(conn, [](Connection*) {})
        );
    }
    
    std::stack<std::shared_ptr<Connection>> available_;
    std::mutex mutex_;
};

七、常见陷阱与注意事项

陷阱1:不要混合使用原始指针和智能指针

void badPractice() {
    int* raw = new int(42);
    std::shared_ptr<int> ptr1(raw);
    std::shared_ptr<int> ptr2(raw);  // 危险!两个独立的引用计数
}  // double free!

// 正确做法
void goodPractice() {
    auto ptr1 = std::make_shared<int>(42);
    auto ptr2 = ptr1;  // 共享引用计数
}

陷阱2:避免循环引用

class B;  // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;  // 强引用
};

class B {
public:
    std::shared_ptr<A> a_ptr;  // 强引用 - 循环引用!
};

// 正确做法:其中一个使用 weak_ptr
class BFixed {
public:
    std::weak_ptr<A> a_ptr;  // 弱引用,打破循环
};

陷阱3:不要在构造函数中暴露 this 指针给智能指针

class BadExample : public std::enable_shared_from_this<BadExample> {
public:
    BadExample() {
        // 危险!对象尚未构造完成
        std::shared_ptr<BadExample> ptr(this);  // UB!
    }
};

// 正确做法
class GoodExample : public std::enable_shared_from_this<GoodExample> {
public:
    std::shared_ptr<GoodExample> getShared() {
        return shared_from_this();  // 安全使用
    }
};

八、总结

请记住:

  • 获得资源后立刻放进管理对象(如智能指针)
  • 管理对象运用析构函数确保资源被释放
  • 推荐使用 std::unique_ptr 管理独占资源
  • 推荐使用 std::shared_ptr 管理共享资源
  • 优先使用 std::make_uniquestd::make_shared 创建智能指针
  • 自定义 RAII 类时,注意复制行为的设计(参见条款14)

RAII 是 C++ 最核心的设计原则之一。通过将资源生命周期绑定到对象生命周期,我们可以写出异常安全、资源安全的代码,彻底告别内存泄漏和资源泄漏的烦恼。现代 C++ 的智能指针和容器已经很好地实践了 RAII,优先使用标准库设施,必要时再自定义资源管理类。


参考阅读:

  • 《Effective C++》第3版,Scott Meyers,条款13
  • 《Effective Modern C++》,Scott Meyers
  • C++ Core Guidelines: R.1 - R.30 (Resource Management)
  • cppreference.com: std::unique_ptr, std::shared_ptr, std::weak_ptr
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐