核心元件‎ > ‎

智慧指標

使用 C++ 有一個很惱人的問題,當 new 出一個多處共享,甚至在多執行緒共用的物件,使用這個物件時得擔憂是不是已被 delete 掉了,delete 時也得擔心是不是還有其他地方還在使用,或是已不使用了,有沒有被 delete = =? 這些瑣碎的事要是由人力處理,難保不出差錯。

理想中的的想法是當一個物件已沒被使用就自動 delete,但要如何知道已沒被使用了?這裡提供的方法是採用追蹤的方式,當一個物件找不到還有使用者就自動刪除。

擁有者與放棄擁有

程式庫要求任何要具有自動刪除能力的物件,都必須是來自 cxxlObject 的延伸類別,cxxlObject 會自動記錄有誰擁有它,擁有者與被擁有者都須是 cxxlObjec 的延伸物件。

先從廣度的層面來看,假設 x 物件被 a、b、c 三個物件所擁有,表示 x 物件有三個擁有者,x 物件的存活取決於這三個擁有者是否全部放棄擁有,放棄擁有是這個機制很重要的概念。再從深度的層次來看,x 物件若還存在,必至少有一個擁有者存在,假設這個擁有者為 a1,而 a1 為什麼會存在,必至少有一個 a1 的擁有者 a2 存在,而 a2 的存在必至少有一個...,這樣下去要怎麼決定這些物件的存在?

根物件 rootObject

rootObject 是程式一開始就已建好的物件,他的任務只有一個,就是作為 x a1 a2 ... 一連串物件擁有者的最終擁有者。當一個物件發生被放棄擁有的情況發生時,並不是以廣度或深度上是否還有擁有者存在來決定這個物件的存在,而是在眾擁有者(廣度和深度)中是否找得到 rootObject 這個物件,注意!rootObject 是一個物件(當然它也是由 cxxlObject 繼承而來),而 cxxlObject 是一個類別,不要混淆了。

智慧指標 Smart_Ptr

Smart_Ptr 是作為 cxxlObject 和它的擁有者連結的工具,Smart_Ptr 和 cxxlObject 之間已建好了完善的自動銷毀機制,使用 Smart_Ptr 不用也不該再去擔心 delete 的工作。一個 cxxlObject 物件不一定要放入 Smart_Ptr,但 cxxlObject 物件交給 Smart_Ptr 後,就不可以再使用 delete 指令再去刪除這個物件,這點要記住。還有 cxxlObject 物件可以多次被放入 Smart_Ptr 中,cxxlObject 內含 CriticalSec 同步控制,但 Smart_Ptr 並沒有,因此 Smart_Ptr 不應共用,且使用 Smart_Ptr 的物件應做好同步控制。

Smart_Ptr 的建構函數宣如下:

// Constructor
Smart_Ptr(T *pcxxlObj,cxxlObject *Host = rootObject);

 

類似 Smart_Ptr 這種用法的有 boost 的 shared_ptr,但 shared_ptr 採用 Countor 的做法,當遇到循環參照時,會造成 memory leak,使用 Smart_Ptr 不會有這種問題。

另一個比較會被質疑的是效率的問題,首先先瞭解一下 Smart_Ptr 處理銷毀的過程,當一個 cxxlObject 物件要被放棄擁有(可能是擁有者被銷毀或擁有者改用其他物件)時:

  1. 將放棄擁有此物件的 Smart_Ptr 從記錄中剔除。
  2. 搜尋所有的擁有者中(廣度和深度)是否有 rootObject
  3. 找不到 rootObject 才執行 delete 將物件銷毀

以上被稱為放棄擁有程序,有兩種 放棄擁有程序 可以被 cxxlObject 選用,一種稱為 Spirit_Easy,另一種稱為 Spirit_Urgent,Spirit_Urgent 會完整執行完 放棄擁有程序 才返回(即時);Spirit_Easy 只做完第一步驟就返回(非即時),其他就丟給一個負責處理的執行緒在背後處理。

顯然 Spirit_Easy 會快很多,但沒法確定物件會在何時會被銷毀,但採用 Spirit_Urgent 也得是最後一個擁有者放棄擁有才能看到銷毀效應,所以 Spirit_Urgent 比較適用於不是作為共享物件時使用,不過若不是要作為共享物件只要不放入 Smart_Ptr 就行了(得自行處理 delete),因此 Spirit_Urgent 的功用在一般使用上還看不出它的用途。

 

範例一

#include <iostream> #include <SMART_PTR.HPP> // Smart_Ptr 和 cxxlObject 的引入檔 using namespace std; using namespace CxxlMan; // 這個是 CxxlMan 程式庫的命名空間 class A:virtual public cxxlObject // 以 virtual 繼承,以確保一個物件只會有一個 cxxlObject { const char *m_name; public: A(const char *name,ISpirit *spirit = Spirit_Easy) // 建議加上 ISpirit 這個選項 :cxxlObject(spirit) // 每層 cxxlObject 的延伸類別都須指定 ISpirit { m_name = name; cout << m_name << " Constructor" << endl; } virtual ~A() { cout << m_name << " Destructor" << endl; } void Print() const { cout << m_name << " Print" << endl; } }; int main() { // 這兩個的擁有者都是 rootObject Smart_Ptr<A> A1_Ptr(new A("A1"), rootObject); Smart_Ptr<A> A2_Ptr(new A("A2",Spirit_Urgent) ); // 刻意指定 Spirit_Urgent Smart_Ptr<A> A3_Ptr(NULL); // 包裹空物件 A2_Ptr.Destroy(); // 把 A2_Ptr 清為 NULL,物件也會被銷毀 if(A2_Ptr.isNULL()) cout << "A2_Ptr 是空指標" << endl; Smart_Ptr<A> A4_Ptr = A1_Ptr; // 兩者會包裹同一個物件 A1_Ptr.Destroy(); // 把 A1_Ptr 清為 NULL,物件因還有擁有者不會被銷毀 A4_Ptr->Print(); // 用一般的指標用法使用包裹的物件 system("pause"); return 0; }
 

 

參數與回傳值

對於已放入 Smart_Ptr 的 cxxlObjec 要作為參數傳遞有兩種方式

void F1(Smart_Ptr<type> type_arg); // 直接採用 Smart_Ptr 傳遞

void F2(type *type_ptr); // 用 cxxlObject 的指標

採用 F1 的方式會多一點點 Smart_Ptr 間的 assign 時間消耗,就採用 Spirit_Easy 的 cxxlObject 就是多了記錄和剔除 Smart_Ptr 的處理,而使用 F2 的方式可能會讓使用端忘了要把 cxxlObjec 先放入 Smart_Ptr 中,還有若這樣使用 F2(new type),若不被 Smart_Ptr 包裹會造 memory leek,折衷的方法是對外提供的功能採用 F1,對內採用 F2。

若是以下的用法呢:

void F3(Smart_Ptr<type> &type_ref);

void F4(const Smart_Ptr<type> &type_arg);

F3 是傳遞和取值的用法,也就是 type_ref 是可以被更換包裹的物件,而 F4 我認為是代替 F1 的最好的解決用法,推薦採用這種方法來代替 F1 和 F2。

但不管用哪種方式,接收端若要保留 cxxlObjec 物件,一定要用 Smart_Ptr 包裹。

至於作為函數的回傳值以下方式是唯一的選擇

Smart_Ptr<type> F5();

以下的回傳方式都有間題

Smart_Ptr<type> &F6();

type *F7();

F6 等於暴露了物件自己封裝的屬性,而 F7 沒辦法讓使用端知道這是一個 cxxlObject 物件。

 

同步控制

cxxlObject 在生死存取的處理上是有同步控制,所以可允許多執行緒的多個 Smart_Ptr 對同一個 cxxlObject 處理使用。

但 Smart_Ptr 本身是沒有同步控制,所以 Smart_Ptr 本身不應該是可共享的,只可做物件內部的屬性變數,由擁有者的物件提供同步控制的保護。或者作為函數內的 local 變數。

所以像上面提到的 F4() 把 Smart_Ptr 本身傳遞出去必須要多加 const,以免被修改。而 F3() 最好只是作為取值回來使用就好

範例二

延續範例一編寫一個使用 class A 的 class B

class B:virtual public cxxlObject
{ Smart_Ptr<A> A_Ptr;
public:
B(ISpirit *spirit = Spirit_Easy)
:cxxlObject(spirit),
A_Ptr(new A("B 內建的 A"),this) // 指定 A 的擁有者是 B 的實例(用 this)
{ std::cout << "B Constructor" << std::endl;
}

virtual ~B()
{
std::cout << "B Destructor" << std::endl;
}

void PrintA() const
{
A_Ptr->Print();
}

void Set(const Smart_Ptr<A> &p)
{
A_Ptr = p; } Smart_Ptr<A> Get() { return A_Ptr; }
};

  

轉形

智慧指標只包裝物件的一個介面,若要改用同一個物件的其他介面可用如下的方式

Smart_Ptr<T> New1_Ptr = Smart_Cast<T>(Obj, Host);
Smart_Ptr<T> New2_Ptr( Smart_Cast<T>(Obj), Host);
Old_Ptr = Smart_Cast<T>(Obj);
T 是指示要轉形成哪個介面。
Obj 須是 cxxlObject 或包裹 cxxlObject 的 Smart_Ptr
Host 指示要被哪個 cxxlObject 物件擁有,不指定就用內定 rootObject,但用在 Smart_Cast 中只對接收者是最佳化的新建 Smart_Ptr 才有效,不確定就不要這樣用

回傳值為指定介面的 Smart_Ptr<T>,若轉形失敗包裹的是 NULL,可用 Smart_Ptr<T>::isNULL() 做檢查

 

 


引入檔

SMART_PTR.HPP 

程式庫

cxxlobject

 

Comments