2021年2月20日 星期六

[Effective c++] [閱讀心得]: 條款13: 以對象管理資源

Photo by Alvaro Reyes on Unsplash

class A { ... };

void foo() {
    A a = new A;
    ...
    if (...) {
        delete a;
        return;
    }
    ...
    delete a;
    return;
}

如同上面的例子,因為if condition成立時foo()會提前return,所以必須在其中delete a,以免產生記憶體洩漏。
然而,這樣的撰寫風格其實具有極高的風險性。

舉個例子,假如另外一位工程師A也參與了foo()的開發,但他並沒有注意到foo()中a物件的存在。這時,假如他在foo()中新增了一個可能提前return的區塊,則記憶體洩漏就產生了。

class A { ... };

void foo() {
    A a = new A;
    ...
    if (...) {
        delete a;
        return;
    }
    ...
    // 工程師A加入的區塊
    if (...) {
        return; // memory leak!!!
    }
    ...
    delete a;
    return;
}

要解決這問題,一個好的方法便是使用對象來管理raw pointer。

class A { ... };

class SmartPtr {
public:
    explicit SmartPtr(A *a) : a(a) {
    }
    ~SmartPtr() {
        delete a;
    }
private:
    A *a;
};

void foo() {
    SmartPtr sPtr(new A);
    ...
    if (...)
        return;
    ...
    return;
}

在上面的程式碼中,我們引入了一個SmartPtr的類別,這個類別內含一個成員變數A *a,而在SmartPtr的解構函式中會去delete a。這表示,每當SmartPtr的物件被摧毀時,它所擁有的a會被自動delete。
我們在回過頭看看foo(),一開始,我們宣告了一個類別為SmartPtr的物件sPtr,並將new A傳入它的建構函式中。由於sPtr是foo()中的一個區域變數,當foo() return時,sPtr會自動被銷毀,聯帶著也自動delete a。如此一來,我們便不用在每個return的地方都去delete a,大大降低了記憶體洩漏的風險。

C++標準程序庫如何處理上述議題?

為了應付上述的需求,在C++標準程序庫中,提出了std::auto_ptr來管理raw pointer,用法如下:

class A { ... };

void foo() {
    std::auto_ptr<A>(new A);
    ...
    if (...)
        return;
    ...
    return;
}

然而,使用std::auto_ptr有幾個需注意的地方:

  • 不能讓兩個以上的auto_ptr指向同一個raw pointer,否則會出現delete兩次的情況(這在C++中屬於undefined behavior)。有鑑於此,auto_ptr如果呼叫了copy constructor或是 copy assignment的話,原來的auto_ptr會變成指向null。此一特性讓auto_ptr無法用於某些STL的容器中(因為這些容器要求其元素必須有正常的copy行為)。
  • 由於std::auto_ptr在解構函式中總是做delete,因此無法用std::auto_ptr來管理陣列(欲解決此一問題,可使用C++11所提供的std::unique_ptr)。

void wrong() {
    std::auto_ptr<A>(new int[10]) // 糟糕的用法;
    ...
    if (...)
        return;
    ...
    return;
}

void correct() {
    std::unique_ptr<int[]>(new int[10]) // 使用std::unique_ptr就可解決上面的問題。
    ...
    if (...)
        return;
    ...
    return;
}

應以獨立語句來生成智慧指標


foo(std::shared_ptr<int>(new int), bar());

上面的程式碼看起來很平常,但其實也潛藏了記憶體洩漏的風險。主要的原因是C++標準並未定義一個函式中參數執行的順序。舉個例子,foo()中參數的執行順序可能為:
  1. new int
  2. 執行bar()
  3. 生成shared_ptr
這時候,假如bar()執行的過程中拋出了異常,那麼步驟1所產生的int便來不及放入shared_ptr中,因而就導致了記憶體洩漏。

因此,比較安全的寫法是,將生成智慧指標的敘述獨立出來,如下所示:

std::shared_ptr<int> ptr(new int); // 單獨生成智慧指標
foo(ptr, bar());

沒有留言:

張貼留言