第 18.8 節
RAII
0瀏覽次數0訪問次數--跳出率--平均停留
本節解決什麼問題
在 C 語言中,我們經常看到這種代碼:
FILE* f = fopen("data.txt", "r");
// ... 使用文件 ...
fclose(f); // 容易忘记!
如果中間提前 return,或是拋出了異常,fclose 就不會執行,造成資源泄漏。類似的問題也出現在內存(malloc/free)、鎖(lock/unlock)、套接字等所有需要"獲取-釋放"的資源上。
RAII 利用 C++ 對象生命週期確定性,自動管理資源,讓你不再需要手動釋放。
這個特性是什麼
RAII(Resource Acquisition Is Initialization,資源獲取即初始化)是 C++ 中最重要的資源管理慣用法:
- 構造函數中獲取資源。
- 析構函數中釋放資源。
- 當對象離開作用域時,析構函數一定會被調用。
這是 C++ 區別於 C 和其他語言的核心設計理念之一,智能指針、lock_guard、fstream 都是 RAII 的實現。
C++ 標準版本
C++98(RAII 從 C++ 誕生之初就存在,智能指針、lock_guard 等現代 RAII 工具在 C++11 成熟)。
需要的頭文件
RAII 是編程理念,不需要特定頭文件。但 RAII 的實現分散在各處:<memory>(智能指針)、<mutex>(鎖)、<fstream>(文件流)等。
基本語法
class RAIIExample
{
Resource* res; // 管理的资源
public:
RAIIExample() : res(获取资源) { } // 构造:获取资源
~RAIIExample() { 释放资源; } // 析构:释放资源
};
常用 RAII 實現
| RAII 類型 | 管理資源 | 頭文件 |
|---|---|---|
std::unique_ptr | 動態內存 | <memory> |
std::shared_ptr | 動態內存(共享) | <memory> |
std::lock_guard | 互斥鎖 | <mutex> |
std::unique_lock | 互斥鎖(靈活) | <mutex> |
std::fstream | 文件句柄 | <fstream> |
std::thread | 線程(需 join/detach) | <thread> |
示例代碼
示例 1:沒有 RAII 的問題 vs 有 RAII
#include <iostream>
// 模拟一个资源
class Resource
{
public:
void open() { std::cout << "Resource opened\n"; }
void close() { std::cout << "Resource closed\n"; }
void use() { std::cout << "Resource used\n"; }
};
// ❌ 手动管理:容易忘记 close
void no_raii()
{
Resource r;
r.open();
r.use();
// 如果这里抛异常或提前 return,close 不会执行!
r.close();
}
// ✅ RAII:利用析构函数自动释放
class ResourceGuard
{
Resource& r;
public:
ResourceGuard(Resource& res) : r(res)
{
r.open(); // 构造时获取资源
}
~ResourceGuard()
{
r.close(); // 析构时释放资源
}
};
void with_raii()
{
Resource r;
ResourceGuard guard(r); // 构造时 open
r.use();
// guard 离开作用域,析构函数自动 close
}
int main()
{
std::cout << "=== no_raii ===\n";
no_raii();
std::cout << "\n=== with_raii ===\n";
with_raii();
return 0;
}
運行結果:
=== no_raii ===
Resource opened
Resource used
Resource closed
=== with_raii ===
Resource opened
Resource used
Resource closed
示例 2:在示例 1 基礎上,用 lock_guard 理解 RAII 鎖管理
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx; // 共享互斥量
int counter = 0;
// ❌ 手动加锁解锁(容易出问题)
void manual_lock()
{
mtx.lock();
++counter;
// 如果这里抛异常,unlock 永远不会被执行!
mtx.unlock();
}
// ✅ RAII 风格:lock_guard 自动管理锁
void raii_lock()
{
std::lock_guard<std::mutex> lock(mtx); // 构造时 lock
++counter;
// lock 离开作用域,析构函数自动 unlock
}
int main()
{
std::thread t1([] {
for (int i = 0; i < 1000; ++i)
raii_lock();
});
std::thread t2([] {
for (int i = 0; i < 1000; ++i)
raii_lock();
});
t1.join();
t2.join();
std::cout << "counter = " << counter << " (expected 2000)\n";
return 0;
}
運行結果:
counter = 2000 (expected 2000)
示例 3:在示例 2 基礎上,用 fstream 理解文件 RAII
#include <iostream>
#include <fstream>
#include <string>
// RAII:fstream 在析构时会自动关闭文件
void write_and_read()
{
// 写文件
{
std::ofstream out("raii_test.txt");
out << "Hello RAII!\n";
out << "This is line 2.\n";
// out 离开作用域,文件自动关闭
}
// 读文件
{
std::ifstream in("raii_test.txt");
std::string line;
while (std::getline(in, line))
{
std::cout << line << "\n";
}
// in 离开作用域,文件自动关闭
}
}
int main()
{
write_and_read();
std::cout << "File was automatically closed by RAII\n";
return 0;
}
運行結果:
Hello RAII!
This is line 2.
File was automatically closed by RAII
示例 4:在示例 3 基礎上,提前 return 時才看出 RAII 的價值
下面這個例子故意模擬"處理到一半發現數據不合法,提前返回"。正常流程裏手動釋放和 RAII 看起來差不多,但一旦有多個返回路徑,區別就很明顯。
#include <iostream>
#include <string>
#include <utility>
#include <vector>
class Connection
{
std::string name_;
public:
explicit Connection(std::string name) : name_(std::move(name))
{
std::cout << name_ << " connected\n";
}
~Connection()
{
std::cout << name_ << " disconnected\n";
}
void send(const std::string& msg)
{
std::cout << name_ << " send: " << msg << "\n";
}
};
bool upload_with_raii(const std::vector<std::string>& lines)
{
Connection conn("server"); // 构造时连接,函数结束时自动断开
for (const auto& line : lines)
{
if (line.empty())
{
std::cout << "empty line, stop upload\n";
return false; // conn 仍然会析构
}
conn.send(line);
}
return true; // conn 也会析构
}
int main()
{
std::vector<std::string> data = {"hello", "world", "", "after error"};
bool ok = upload_with_raii(data);
std::cout << "upload ok = " << std::boolalpha << ok << "\n";
return 0;
}
運行結果:
server connected
server send: hello
server send: world
empty line, stop upload
server disconnected
upload ok = false
運行結果
見上方每個示例的"運行結果"。
示例中的關鍵語法解釋
| 示例 | 講了什麼 | 新出現的語法 | 為什麼這樣寫 | 注意事項 |
|---|---|---|---|---|
| 示例 1 | RAII 基本原理 | 構造函數獲取,析構函數釋放 | 展示了手動管理的問題和 RAII 的解決方案 | RAII 的"一定執行析構"是 C++ 的核心保證 |
| 示例 2 | lock_guard 是 RAII | std::lock_guard<std::mutex> | 構造時加鎖,析構時解鎖,異常安全 | 比手動 lock/unlock 安全得多 |
| 示例 3 | fstream 是 RAII | std::ofstream、std::ifstream | 構造時打開文件,析構時關閉文件 | 不需要顯式寫 close |
| 示例 4 | 多返回路徑中的 RAII | 構造/析構、提前 return | 真實工程裏經常有提前返回,RAII 能保證資源仍被釋放 | 資源對象要放在正確的作用域裏 |
常見錯誤
錯誤 1:在析構函數中拋出異常
~MyRAII()
{
cleanup(); // 如果 cleanup 抛异常...
}
如果析構函數拋異常,且同時有另一個異常正在傳播,程序會直接 std::terminate。析構函數應標記 noexcept 並捕獲所有異常。
錯誤 2:把 RAII 對象創建在堆上
auto* guard = new std::lock_guard<std::mutex>(mtx); // ❌ 永远不会自动析构!
正確做法:RAII 對象必須在棧上創建,才能利用離開作用域自動析構的特性。
錯誤 3:忘記 RAII 對象的作用域
void func()
{
std::lock_guard<std::mutex> lock(mtx);
// 锁在这里面生效
} // 离开作用域,解锁
// 在外面访问共享数据没有保护!
正確做法:確保在鎖的作用域內訪問共享數據。
使用建議
- "需要配對的獲取/釋放"就用 RAII:這是 C++ 的資源管理第一原則。
- 永遠不要手動 new/delete、lock/unlock、open/close:用智能指針、lock_guard、fstream。
- RAII 對象必須在棧上:利用作用域自動觸發析構。
- 析構函數永遠不要拋異常:標記
noexcept。 - 理解 RAII 就理解了 C++ 的核心設計哲學:後續智能指針、併發編程都建立在 RAII 之上。
- 用作用域控制資源持有時間:想早點釋放資源,就把 RAII 對象放進更小的
{}作用域。
小結
- RAII = 資源獲取即初始化,是 C++ 最核心的資源管理慣用法。
- 構造時獲取資源,析構時釋放資源,離開作用域保證釋放。
std::unique_ptr、std::lock_guard、std::fstream都是 RAII。- 多個
return、異常、複雜分支下,RAII 的價值最明顯。 - 永遠不要在析構函數中拋異常。
- 理解了 RAII,就為理解智能指針和併發編程打好了基礎。