第 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

運行結果

見上方每個示例的"運行結果"。

示例中的關鍵語法解釋

示例講了什麼新出現的語法為什麼這樣寫注意事項
示例 1RAII 基本原理構造函數獲取,析構函數釋放展示了手動管理的問題和 RAII 的解決方案RAII 的"一定執行析構"是 C++ 的核心保證
示例 2lock_guard 是 RAIIstd::lock_guard<std::mutex>構造時加鎖,析構時解鎖,異常安全比手動 lock/unlock 安全得多
示例 3fstream 是 RAIIstd::ofstreamstd::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);
    // 锁在这里面生效
}  // 离开作用域,解锁

// 在外面访问共享数据没有保护!

正確做法:確保在鎖的作用域內訪問共享數據。

使用建議

  1. "需要配對的獲取/釋放"就用 RAII:這是 C++ 的資源管理第一原則。
  2. 永遠不要手動 new/delete、lock/unlock、open/close:用智能指針、lock_guard、fstream。
  3. RAII 對象必須在棧上:利用作用域自動觸發析構。
  4. 析構函數永遠不要拋異常:標記 noexcept
  5. 理解 RAII 就理解了 C++ 的核心設計哲學:後續智能指針、併發編程都建立在 RAII 之上。
  6. 用作用域控制資源持有時間:想早點釋放資源,就把 RAII 對象放進更小的 {} 作用域。

小結

  • RAII = 資源獲取即初始化,是 C++ 最核心的資源管理慣用法。
  • 構造時獲取資源,析構時釋放資源,離開作用域保證釋放。
  • std::unique_ptrstd::lock_guardstd::fstream 都是 RAII。
  • 多個 return、異常、複雜分支下,RAII 的價值最明顯。
  • 永遠不要在析構函數中拋異常。
  • 理解了 RAII,就為理解智能指針和併發編程打好了基礎。
音乐页