Effective C++ 条款13:以对象管理资源(RAII)
·
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_guard和std::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_unique和std::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
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)