September 21, 2025

C++ | Resource Acquisition Is Initialization (RAII)

在 C++ 中開發者需要自己設計記憶體資源的管理機制,但是往往無法保證所有資源都被正常釋放,因此導致 Memory Leak。RAII 是一種 Design Pattern,可以確保資源在不需要時自動釋放,避免記憶體洩漏問題。

1.1 What is RAII

RAII in cppreference.com

Resource Acquisition Is Initialization 中文是 資源取得即初始化,聽起來很抽象,其實本質上是由 C++ 的物件生命週期所衍生出來的設計模式。RAII 的核心概念是將資源的取得和釋放綁定在物件的生命週期中,當物件被建立時,資源被取得;當物件被銷毀時,資源被釋放。

直白一點就是一個資源的取得要在 Constructor 裡面完成,而釋放則是在 Destructor 裡面完成

在 Stack 上生命週期的管理是自動的,而在 Heap 上生命週期的管理則需要開發者手動管理。RAII 的設計也可以說是將 Heap 上的綁定在 Stack 上,讓資源的管理變得自動化。

std::mutex m;
 
void bad() 
{
    m.lock();               // acquire the mutex
    f();                    // if f() throws an exception, the mutex is never released
    if (!everything_ok())
        return;             // early return, the mutex is never released
    m.unlock();             // if bad() reaches this statement, the mutex is released
}
 
void good()
{
    std::lock_guard<std::mutex> lk(m); // RAII class: mutex acquisition is initialization
    f();                               // if f() throws an exception, the mutex is released
    if (!everything_ok())
        return;                        // early return, the mutex is released
}                                      // if good() returns normally, the mutex is released

在 bad() 函數中,如果 f() 拋出 Exception 或者提前 return,mutex 永遠不會被釋放,導致死鎖。在接下來的程式中 m 會一直被鎖定,無法再被其他執行緒使用。而 good() 函數中使用了 std::lock_guard 這個 RAII 類別來管理 mutex 的生命週期,確保如果 good() 結束時,std::lock_guard 物件將會進入 Destructor,因此自動釋放 mutex。


1.2 Stack Unwinding

When an exception is thrown and control passes from a try block to a handler, the C++ runtime calls destructors for all automatic objects constructed since the beginning of the try block. This process is called Stack Unwinding.

Throwing exceptions in cppreference.com

這個機制的過程是由於 C++ 嚴格遵守 Object Lifetime 規範以及 Stack Unwinding 機制所導致的。當 Exception 被拋出時,C++ 會自動呼叫所有在 try block 中建立的物件的 Destructor,這個過程稱為 Stack Unwinding。

當一個函數因為以下原因離開 Scope 時:

  1. 執行到函數結尾
  2. 執行到 return 返回
  3. 發生 Exception 被拋出、並開始 Exception Progagation

所有位於 Stack 上的未見都會依照建立的反向順序被銷毀,呼叫 Destructor。

Exception Progagation: 如果 Function 本身沒有處理 Exception,就會沿著 Call Stack 向上傳遞直到被處理


1.3 Constructor & Destructor

因此 RAII 的實現關鍵在於 Constructor 和 Destructor 的設計。當物件被建立時,Constructor 負責取得資源;當物件被銷毀時,Destructor 負責釋放資源。這樣我們在編寫 C++ 程式時需要注意一些壞習慣,例如在 Constructor 之外取得資源,或者在 Destructor 之外釋放資源,這樣都會破壞 RAII 的原則,導致資源無法被正確管理。

注意以下四點新手常犯的錯誤:

1. 在 Class attribute 直接取得資源 - 盡量在 Constructor 裡面取得資源,保持 Code Style 一致性

class Bad {
    FILE* file = fopen("data.txt", "r"); // bad: resource acquisition outside
}

2. 使用 raw pointer 管理資源 - 使用 smart pointer 來管理動態記憶體,避免記憶體洩漏 - raw pointer 並無法表達 ownership 的概念,因此指向的資源有可能無法被正確釋放

class Bad {
    int* p = new int(42);
};

class Good {
    std::unique_ptr<int> p = std::make_unique<int>(42);
};

3. 在 Destructor 裡面手動寫大量 Cleanup Code - A well-designed destructor should usually be empty. - 將 Cleanup Code 包裝在專門的 RAII 類別中,讓 Destructor 只負責呼叫這些類別的 Destructor - 降低維護成本,否則 Destructor 會變得非常複雜

class Bad {
    FILE* f;
    int* buf;
    Mutex* m;
public:
    ~Bad() {
        if (f) fclose(f);
        if (buf) delete[] buf;
        if (m) m->unlock();
    }
};

class Good {
    std::ifstream file;
    std::vector<int> buf;
    std::lock_guard<std::mutex> lock;
};

4. 重複包裝 RAII 類別 - 避免不必要的包裝,直接使用標準庫提供的 RAII 類別 - 延後或者提前釋放由 Smart Pointer 來負責管理

class Bad {
    std::vector<int>* v;
public:
    Bad() { v = new std::vector<int>(); }
    ~Bad() { delete v; }
};

class Good {
    std::vector<int> v;
};

class Good {
    // Use smart pointer because dynamic lifetime is needed.
    std::unique_ptr<std::vector<int>> v;
};

1.4 Smart Pointer

Smart Pointer 是 C++11 正式被標準化引入語言核心的,在此之前期待 new / delete 的管理方式非常容易出錯。 在下面的範例中,任何 do_something() 內部發生的 Exception 都會導致 delete ptr 永遠不會被呼叫,造成 Memory Leak。

ResourceType* ptr = new ResourceType(); // acquire resource

ptr->do_something();

delete ptr; // release resource

std::unique_ptr 是一種 Smart Pointer,當 unique_ptr 被銷毀時會自動去將指向的資源釋放掉,避免記憶體洩漏問題。而現代 C++ 更推薦使用 std::make_unique 來建立 unique_ptr,這樣可以避免在 new 過程中發生 Exception 時導致的資源洩漏問題。

std::make_unique 是 C++14 引入的 std::unique_ptr 的 factor method,推薦使用它來建立 unique_ptr

void function() {
    std::unique_ptr<MyObject> ptr{new MyObject()};
    ptr->do_something();
}

void function() {
    auto ptr = std::make_unique<MyObject>();
    ptr->do_something();
}

要注意的是 smart pointer 應該要存在於 Stack 上才能確保,當它離開 Scope 時會自動呼叫 Destructor 釋放資源。如果 smart pointer 本身存在於 Heap 上,那就失去了 RAII 的意義。這樣的話使用的是 unique_ptr 來管理另一個 unique_ptr,反而增加了複雜度。

void bad() {
    // bad: unique_ptr itself is allocated on the heap
    auto ptr = new std::unique_ptr<MyObject>(new MyObject());
    ptr->get()->do_something();
}

1.5 Common Smart Pointer Types

接下來接紹 C++ 中常見的 smart pointer 類型:

1.5.1 std::unique_ptr<T>

  • Unuque Owner 獨佔擁有權的指標,只能有一個 unique_ptr 指向同一個資源
  • 不可以被複製 (copy),只能被移動 (move)
  • 預設首選的 smart pointer 類型
std::unique_ptr<Foo> p1 = std::make_unique<Foo>();
std::unique_ptr<Foo> p2 = std::move(p1); // ownership transfer

1.5.2 std::shared_ptr<T>

  • Shared Ownership 將共享擁有權的指標,可以有多個 shared_ptr 指向同一個資源
  • 使用 reference counting 來追蹤有多少個 shared_ptr 指向同一個資源
  • 當最後一個 shared_ptr 被銷毀時,資源才會被釋放
std::shared_ptr<Foo> p1 = std::make_shared<Foo>();
std::shared_ptr<Foo> p2 = p1; // shared ownership

有額外的效能開銷 atomic reference count,並且使用上要注意循環參考 (circular reference) 問題

如果 A 指向 B,B 也指向 A,這樣兩個物件都無法被釋放,導致記憶體洩漏。可以使用 std::weak_ptr 來解決這個問題。在下面的範例中,A 持有 B 的 shared_ptr,而 B 持有 A 的 shared_ptr,這樣就會導致循環參考問題。

即使 a, b 都被銷毀,但是 A, B 依然持有對方的 shared_ptr,導致記憶體無法被釋放。同時 A, B 存在於 Heap 上,無法利用 Stack Unwinding 來自動釋放資源。

auto a = std::make_shared<A>(); 
auto b = std::make_shared<B>(); 
a->b = b; // make A hold B
b->a = a; // make B hold A (causes circular reference)

1.5.3 std::weak_ptr<T>

  • 需要解決 std::shared_ptr 的循環參考問題,因此引入了 weak_ptr
  • weak_ptr 不擁有資源的所有權,不會影響 reference count
struct A {
    std::shared_ptr<B> b;
};

struct B {
    std::weak_ptr<A> a; // use weak_ptr to break circular reference
};
  • 需要透過 lock() 方法轉換成 shared_ptr 才能擁有資源的存取權
    • 因為 weak_ptr 不擁有資源的所有權,資源可能已經被釋放
std::weak_ptr<Foo> wp = ...;
if (auto sp = wp.lock()) { // try to get a shared_ptr
    sp->do_something();
} else {
    // resource has been released
}

最後總結一下三種常見的 smart pointer 類型及其適用場景:

Smart Pointer Type Ownership Model Use Case
std::unique_ptr Unique Ownership Default choice for exclusive ownership
std::shared_ptr Shared Ownership When multiple owners are needed
std::weak_ptr Non-owning Reference To break circular references in shared ownership scenarios

C-Style Array with Smart Pointer

要注意在 C++ 中對於 Array 的最佳實踐是使用 container class (e.g., std::vector) 來管理動態陣列,而不是使用 raw pointer 或 smart pointer 來管理 C-Style Array。

例如 C API 固定為 T** 才不得已使用 smart pointer 來管理 C-Style Array:

std::unique_ptr<T[]> data = std::make_unique<T[]>(rows * cols);
std::unique_ptr<T*[]> rows_ptr = std::make_unique<T*[]>(rows);
for (size_t i = 0; i < rows; ++i) {
    rows_ptr[i] = data.get() + i * cols;
}
c_api_function(rows_ptr.get());

這樣至少確保 data 和 rows_ptr 都會在離開 Scope 時自動釋放,避免記憶體洩漏問題。

Last Edit

11-12-2025 01:50

results matching ""

    No results matching ""

    , C++, RAII