首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

C++ 資源管理深度解析:auto_ptr 源碼剖析與左值右值探討

     編輯:關於C++

1. 知識引入

在 C++ 程式設計中,動態分配的記憶體用完後通常必須 delete(釋放),否則會造成記憶體洩漏。雖然大多數初學者有此意識,但有人不以為意。我曾問同學關於動態記憶體分配與釋放,他回答:「只要保證 new 和 delete 成對出現就行了。如果在建構函式中 new,那麼在解構函式中 delete 就可以避免記憶體洩漏了!」

事實果真如此嗎?

實例一

(當子函式中動態分配的記憶體只在子函式中使用而不返回指向該動態記憶體的指標時,子函式中動態分配的記憶體即使不釋放也不會造成記憶體洩漏,因為在銷毀堆疊的同時,會自動釋放該記憶體。因此,我用 main 函式舉例)

int main(void) {
    int* a = new int(35);
    if (*a != 50) return 0;
    delete a;
    return 0;
}

此情況下 new 和 delete 雖成對出現,但仍出現記憶體洩漏。因此,成對出現並不能保證不會發生記憶體洩漏。

實例二

class A {
public:
    A() : a(NULL) {}
    A(int a) { this->a = new int(a); }
    ~A() { if (a != NULL) delete a; }
    A operator=(A src) { this->a = new int(*src.a); return *this; }
private:
    int* a;
};
int main(void) {
    A num(10);
    A num1(15);
    num = num1;
    return 0;
}

看似建構函式 new 分配記憶體,解構函式 delete 釋放,一切正常。但問題出在 num = num1; 上,即重載「=」出現問題導致記憶體洩漏。賦值前 num 中的 a 已指向堆中一段記憶體,且是存取該記憶體的唯一方式。直接賦值會導致這塊記憶體無法被存取,因而造成洩漏。

由此可見,記憶體洩漏情況很容易出現,有沒有方法避免?這時我們可以設計一個類似資源管理的類別,來實現對指標指向動態記憶體的管理。此類別精髓在於:成員變數指向動態記憶體,解構函式釋放該變數指向的動態記憶體。

template 
class Manage {
public:
    Manage(T* p) : pArr(p) {}
    ~Manage() { if (pArr != NULL) delete pArr; }
    T* get() { return pArr; }
private:
    T* pArr;
};

這個資源管理類別沒有設置無參數建構函式的原因是:該類別需滿足 RAII 機制。

RAII 機制:RAII,也稱為「資源獲取就是初始化」,是 C++ 等程式語言常用的管理資源、避免記憶體洩漏的方法。
RAII 的做法:使用一個物件,在其建構時獲取資源,在物件生命期控制對資源的存取使之始終保持有效,最後在物件解構時釋放資源。

什麼是資源獲取就是初始化?就是 new(動態分配記憶體)後直接將位址作為參數傳入資源管理類別的物件(呼叫物件的建構函式)。

Manage instance(new int[10]);

new 分配的 int[10] 位址直接用於初始化 instance 物件,這就是資源獲取就是初始化。若再加入成員方法,就構成完整的資源管理類別,也就是智能指標 auto_ptr。

2. 智能指標 auto_ptr 源碼呈現

According to cplusplus.com, auto_ptr 在 <memory> 中。

template  class auto_ptr;

Automatic Pointer [deprecated]
Note: 自 C++11 起此類別樣板已棄用。unique_ptr 是具有類似功能但改進安全性(無假複製賦值)、新增功能(刪除器)並支援陣列的新設施。請參閱 unique_ptr 獲取更多資訊。

此類別樣板為指標提供有限的垃圾回收功能,允許指標在 auto_ptr 物件本身被銷毀時自動銷毀其指向的元素。auto_ptr 物件具有取得指派給它們指標所有權的特性:擁有某元素所有權的 auto_ptr 物件負責銷毀該元素並在自身銷毀時釋放配置給它的記憶體。解構函式透過自動呼叫 operator delete 來做到這一點。因此,不應有兩個 auto_ptr 物件擁有同一元素,因為兩者都會嘗試銷毀它。當兩個 auto_ptr 物件之間發生賦值操作時,所有權會被轉移,意味著失去所有權的物件被設為不再指向該元素(設為空指標)。

不過有一點十分重要:auto_ptr 物件在建構時獲得動態記憶體的 ownership(所有權),且在解構時釋放這段記憶體,更重要的是,在呼叫複製建構函式時是實現 ownership(所有權)的轉移而不是深拷貝,擁有這段動態記憶體的物件只能有一個。

3. 異常安全

異常安全是指,一個物件碰到異常之後,還能夠保證自身的正確性。C++ 中「異常安全函式」提供三種安全等級:

  1. 基本承諾:若異常被拋出,物件內任何成員仍能保持有效狀態,無資料破壞及資源洩漏。但物件現實狀態不可預測,不一定是呼叫前狀態,至少保證符合物件正常要求。
  2. 強烈保證:若異常被拋出,物件狀態保持不變。即呼叫成功則完全成功;呼叫失敗則物件依然是呼叫前狀態。
  3. 不拋異常保證:函式承諾不會拋出任何異常。一般內建型別所有操作都有不拋異常保證。

source 碼中重複出現的 throw() 函式類似一個宣告,保證該函式不會拋出任何異常,因為 STL 需要保證異常安全。其實不加 throw() 也是可以的,不過 STL 有時會要求加上。

4. 智能指標 auto_ptr 源碼分析

建構函式與解構函式

// constructor & destructor
explicit auto_ptr(T* ptr = 0) throw() : ap(ptr) {}
~auto_ptr() throw() { delete ap; }

此類別核心在於含有一個樣板指標 ap,以及解構函式 delete 這個指標指向的動態記憶體。explicit 表明這是顯式建構函式,除了滿足 Google 風格外,還限制參數不能有隱式轉換。

複製建構函式與 release()

// release ownership
T* release() throw() {
    T* tmp(ap);
    ap = 0;
    return tmp;
}
// Copy & assignment
auto_ptr(auto_ptr& rhs) throw() : ap(rhs.release()) {}
template
auto_ptr(auto_ptr& rhs) throw() : ap(rhs.release()) {}
auto_ptr& operator=(auto_ptr& rhs) throw() {
    reset(rhs.release());
    return *this;
}
template
auto_ptr& operator=(auto_ptr& rhs) throw() {
    reset(rhs.release());
    return *this;
}

正如前所述,複製建構函式是實現動態記憶體 ownership(所有權)的轉移,而不是深拷貝。為什麼不採用深拷貝?原因是:採用深拷貝就不再滿足我們設計該智能指標的初衷。

既然是實現 ownership 轉移,就要透過 release() 函式,讓原所有權擁有者放出所有權,使之返回原型別指標(即動態記憶體位址),將之作為新所有權擁有者複製建構函式的參數。然後自身置 NULL,但新所有權擁有者也要注意賦值前記憶體的釋放。

細心者可能會覺得奇怪,auto_ptr 是樣板類別,為什麼複製建構函式裡既有尖括號也有省略尖括號?對於樣板類別,一般情況下不是都要加上尖括號嗎?

在 auto_ptr<T> 定義裡,auto_ptr 預設是 auto_ptr<T>。auto_ptr<T> 與 auto_ptr<Y> 是兩個不同類別,它們相互獨立。因此不難理解為什麼這樣定義:

auto_ptr(auto_ptr& a) throw(); // 參數型別是 auto_ptr
template
auto_ptr(auto_ptr& a) throw(); // 參數型別是 auto_ptr 而非 auto_ptr

重載「=」的兩個函式也不難理解。其實不加 auto_ptr(auto_ptr& a) throw(); 好像也沒問題,因為編譯器一般優先呼叫使用者定義的函式。但為保險起見還是加上為好,因為傳入相同型別時,編譯器有呼叫預設建構函式的可能。

重載 * 與 ->

T& operator*() const throw() { return *ap; }
T* operator->() const throw() { return ap; }

獲取成員指標函式

T* get() const throw() { return ap; }

因為 ap 是私有成員,需要 get() 函式提供存取介面。

重置成員指標函式

void reset(T* ptr = 0) throw() {
    if (ap != ptr) {
        delete ap;
        ap = ptr;
    }
}

定義 auto_ptr 的代理類別

最初我很疑惑,為什麼要再定義一個代理類別?原來 auto_ptr 的複製建構函式有缺陷。當傳入參數為左值時可正常編譯,但一旦傳入參數為右值時,g++ 就編譯不通過。(左值右值在後面講解)因為右值引用必須為 const 引用。

template
struct auto_ptr_ref {
    Y* yp;
    auto_ptr_ref(Y* rhs) : yp(rhs) {}
};
auto_ptr(auto_ptr_ref rhs) throw() : ap(rhs.yp) {}
auto_ptr& operator=(auto_ptr_ref rhs) throw() {
    reset(rhs.yp);
    return *this;
}
template
operator auto_ptr_ref() throw() {
    return auto_ptr_ref(release());
}
template
operator auto_ptr() throw() {
    return auto_ptr(release());
}

5. 左值與右值

查閱資料前,我也是誤解了左值與右值定義。常見誤區在於:認為等號左邊就是左值,右邊就是右值。其實不然,等號只是左值右值中的一個特例,並不能概括左值與右值概念。左值與右值是相對於運算式而言:當一個運算式執行結束後,若該物件仍恆定存在,則說明該物件是左值。如果運算式結束後該物件不存在,說明該物件是暫時物件,即為右值。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved