RAII 惯用法
定义与作用
RAII(Resource Acquisition Is Initialization)是 C++ 中最重要的设计惯用法:在对象构造时获取资源,在析构时释放资源,利用 C++ 的确定性析构保证资源在任何退出路径(含异常)下都被正确释放。
{
std::lock_guard<std::mutex> lock(mtx); // 构造 → 加锁
// 临界区
// 离开作用域 → lock 析构 → 自动解锁
}
核心原理
RAII 与异常安全的统一
完整示例
示例一:数据库事务的 RAII 管理
场景说明:模拟一个数据库事务——RAII 确保事务要么提交要么回滚。
#include <iostream>
#include <string>
#include <stdexcept>
#include <vector>
#include <functional>
class Database {
public:
void begin_transaction() {
std::cout << " BEGIN TRANSACTION" << std::endl;
}
void commit() {
std::cout << " COMMIT" << std::endl;
}
void rollback() {
std::cout << " ROLLBACK" << std::endl;
}
void execute(const std::string& sql) {
std::cout << " SQL: " << sql << std::endl;
}
};
// RAII 事务守卫
class TransactionGuard {
public:
explicit TransactionGuard(Database& db) : db_(db), committed_(false) {
db_.begin_transaction();
}
~TransactionGuard() {
if (!committed_) {
db_.rollback();
}
}
void commit() {
db_.commit();
committed_ = true;
}
TransactionGuard(const TransactionGuard&) = delete;
TransactionGuard& operator=(const TransactionGuard&) = delete;
private:
Database& db_;
bool committed_;
};
// 业务操作
void transfer_money(Database& db, const std::string& from,
const std::string& to, double amount) {
TransactionGuard txn(db); // RAII:自动 begin
if (amount <= 0)
throw std::invalid_argument("转账金额无效");
db.execute("UPDATE accounts SET balance = balance - "
+ std::to_string(amount) + " WHERE name = '" + from + "'");
db.execute("UPDATE accounts SET balance = balance + "
+ std::to_string(amount) + " WHERE name = '" + to + "'");
txn.commit(); // 所有操作成功 → 提交
}
int main() {
Database db;
std::cout << "===== 成功转账 =====" << std::endl;
try {
transfer_money(db, "Alice", "Bob", 1000);
std::cout << "转账成功!" << std::endl;
} catch (const std::exception& e) {
std::cerr << "转账失败: " << e.what() << std::endl;
}
std::cout << "\n===== 失败转账(自动回滚)=====" << std::endl;
try {
transfer_money(db, "Bob", "Charlie", -500);
} catch (const std::exception& e) {
std::cerr << "转账失败: " << e.what() << std::endl;
std::cout << "(TransactionGuard 自动 rollback)" << std::endl;
}
return 0;
}
预期输出:
===== 成功转账 =====
BEGIN TRANSACTION
SQL: UPDATE accounts SET balance = balance - 1000.000000 WHERE name = 'Alice'
SQL: UPDATE accounts SET balance = balance + 1000.000000 WHERE name = 'Bob'
COMMIT
转账成功!
===== 失败转账(自动回滚)=====
BEGIN TRANSACTION
ROLLBACK
转账失败: 转账金额无效
(TransactionGuard 自动 rollback)
示例二:多种资源的 RAII 组合
#include <iostream>
#include <fstream>
#include <mutex>
#include <memory>
#include <string>
// 通用 ScopeGuard
class ScopeGuard {
public:
template <typename Func>
ScopeGuard(Func&& f) : on_exit_(std::forward<Func>(f)) {}
~ScopeGuard() { on_exit_(); }
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
private:
std::function<void()> on_exit_;
};
// 日志系统:组合文件 + 互斥锁 RAII
class ThreadSafeLogger {
public:
explicit ThreadSafeLogger(const std::string& path) {
file_.open(path, std::ios::app);
if (!file_) throw std::runtime_error("无法打开日志文件");
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(mutex_); // RAII 锁
file_ << msg << std::endl;
}
private:
std::ofstream file_; // RAII 文件
std::mutex mutex_; // RAII 锁(通过 lock_guard)
};
int main() {
std::cout << "===== RAII 组合 =====" << std::endl;
{
ThreadSafeLogger logger("app.log");
logger.log("系统启动");
logger.log("处理请求...");
// ScopeGuard
auto guard = ScopeGuard([] {
std::cout << " [ScopeGuard] 退出前清理" << std::endl;
});
std::cout << " 业务处理中..." << std::endl;
}
std::cout << " logger 和 guard 都已析构" << std::endl;
return 0;
}
预期输出:
===== RAII 组合 =====
业务处理中...
[ScopeGuard] 退出前清理
logger 和 guard 都已析构
易错场景与面试考点
| 场景 | 错误 | 正确 |
|---|---|---|
裸 new/delete | 异常时泄漏 | 用 unique_ptr/shared_ptr |
手动 lock/unlock | 异常时不 unlock | lock_guard/unique_lock |
| 析构函数抛异常 | terminate | 析构函数 noexcept |
常见面试问题
- RAII 是什么?为什么是 C++ 的核心惯用法?——构造获取资源、析构释放资源,利用确定性析构保证异常安全。
- C++ 标准库中哪些地方使用了 RAII?——
lock_guard、unique_ptr、fstream、jthread。 - RAII 和 GC 的区别?——RAII 确定性、管理所有资源类型;GC 只管理内存、时机不确定。
小结
- RAII:构造获取 → 析构释放 → 确定性资源管理
- 覆盖所有资源类型:内存、文件、锁、线程、事务
- 异常安全的根基——析构函数在栈展开时一定被调用