在托管 C++ 中,請告訴我使用 delete 操作符銷毀托管對象是否安全?
是的,在托管 C++ 中,你可以刪除( delete )托管對象,只要你理解刪除只不過是調用對象的析構函數,但析構函數必須顯示定義。調用 delete 不會釋放對象的存儲區。只有垃圾收集器才行。Figure 1 展示了一個簡單的程序,該程序定義了一個帶析構函數的托管類,當它運行的時候會顯示一條信息。TESTDTOR 分配兩個 ManagedClass 實例。它顯式刪除第一個實例,但第二個則不然。如果運行 TESTDTOR,你會得到象下面這樣的結果:
Begin main
ManagedClass(04A712D4)::ctor
ManagedClass(04A712D4)::dtor
ManagedClass(04A712E0)::ctor
End main
ManagedClass(04A712E0)::dtor
它說明了當 delete 語句執行時,第一個對象的析構函數立即執行;而第二個對象(at 04A712E0)則沒有被銷毀,直到控制離開 main 並且系統終止代碼調用垃圾收集器釋放逗留對象。
Figure 2 Testdtor 的精彩輸出
不管什麼時候,如果你不能確定 .NET 環境中發生了什麼,你總是可以編寫一些代碼,編譯它並檢查微軟中間語言(MSIL)產生的東西。正如 Figure 2 所展示的,定義析構函數導致編譯器產生兩個方法:一個是 Finalize 方法,它包含你的實現(這裡是調用 printf),一個是 __dtor 方法,它調用 System.GC::SuppressFinalize,然後再調用 Finalize。當你刪除對象時,編譯器產生一個對此 __dtor 方法的掉用。如果你用 /FAs 編譯 TESTDTOR 來產生有源碼的程序集清單,你將看到 delete 語句以如下的方式編譯:
; delete pmc;
ldloc.0 ; _pmc$
call ??1ManagedClass@@[email protected]
老練的 C++ 程序員可能會弄不明白,如果調用 delete 都無法釋放對象,那調用它有干什麼?好問題。調用 delete 的唯一理由是收回任何你的類所使用的非托管資源。例如,如果你的對象打開數個文件或創建了數據庫連接,你可以寫一個關閉其資源的析構函數,然後在用完該對象時使用 delete 釋放它。在托管類中釋放資源的一個更好的方法是通過實現 Dispose 模式,IDisposable——如果你在寫托管 C++ 代碼——由 auto_dispose 來調用它。(更多的信息參見 Tomas Restrepo 在 MSDN 雜志 2002 二月刊上的文章:“Tips and Tricks to Bolster Your Managed C++ Code in Visual Studio .NET”)。
如果你實現 dispose 模式,其他的 .NET 使用者也可以使用它。如果你自己在析構函數中進行清理,其它語言便沒有辦法顯式調用你的清理代碼。因為在 C# 和 Visual Basic 中沒有 delete 操作符。
所以結果是你能調用 delete 來觸發你的析構函數,但是將清理代碼放在析構函數中可能不是一個好主意。最好是實現 IDisposable,這樣所有人都能使用。注意,在 Visual C++ 2005 中,這個行為有所改變。更多信息參見 Andy Rich 對這個問題的討論:“Deterministic Finalization IV - Benefits, part II”,以及當前的 C++/CLI 語言規范標准:“C++/CLI Language Specification Standard”
我有一個返回鏈表的非托管函數,其中有 char* 字符串:
struct blah {
int a, b;
char *a, *b;
struct blah *next;
};
struct blah *getmystruct();
因為 getmystruct() 分配內存,當用完之後,我需要調用 freemystruct(struct blah *b)。我嘗試做一個包裝器,用它來將之轉換成托管類型的集合,但我不知道當需要釋放所有這些指針的時候,該如何來處理。你能否賜教?
為什麼,的確。你不能用 dllimport 語句將你的本地鏈表轉換成托管類型集合。interop 服務固然不錯,但處理此問題也不是那麼好!你需要編寫一個包裝器,顯式地將你的鏈表轉換為托管集合,象 ArrayList。我寫了一個帶有三個模塊的程序,ListWrap,它示范了具體做法。第一個模塊,ListLib.cpp,實現一個本地 C++ 庫(DLL),其中有兩個函數,AllocateList 和 FreeList。分別用來分配和釋放本地 C++ 結構鏈表。它們模仿你程序中的 getmystruct 和 freemystruct 函數。第二個模塊是一個托管 C++ 文件,ListWrap.cpp,它實現托管類 ManagedNode,該類包裝本地 C++ 實現(參見 Figure 3)。第三個模塊是 C# 測試程序,它調用包裝器來展示它如何工作。詳情請下載源代碼。
ListLib.cpp 實現兩個本地函數,AllocateList 和 FreeList,這兩個函數用來分配和釋放 NativeNode 結構鏈表:
// from ListLib.h
struct NativeNode {
int a, b;
TCHAR *str;
struct NativeNode *next;
};
ListWrap.cpp 中的包裝器類 ManagedNode 模仿用 NativeNode 的定義,只是有兩個小差別:本地 char* 被用托管的 String 代替,此外它沒有 next 指針,因為我將用 ArrayList 實現鏈表結構。代碼如下:
// managed equivalent of NativeNode
public __gc class ManagedNode {
public:
int a, b;
String* str;
};
有了 ManagedNode 的定義,下一步是編寫代碼將 NativeNodes 轉換到 ManagedNodes。但在開始之前,先停下來考慮一下轉換函數應該是什麼樣子,他應該有什麼樣的參數,返回什麼值。一種方法是編寫一個函數,參數是本地 NativeNodes 鏈表並返回托管的 ManagedNodes 鏈表,在這個過程中可能銷毀本地鏈表。.NET 客戶端應用程序將直接調用 ListLib DLL (或你的 getmystruct )以獲取本地鏈表,將它作為 IntPtr 類型。然後,將這個 IntPtr 傳遞給轉換函數,象下面這樣:
// call DLL directly through interop
IntPtr nativeList = AllocateList(7);
// call wrapper to convert
ArrayList amanagedList = ListWrap.Convert(nativeList);
大多數情況下,客戶端將負責調用該 DLL 來釋放本地鏈表,或者 Convert 函數自動完成。
一種不同的方法是通過在某個包裝器中包裝分配鏈表的本地函數 AllocateList 來完全隱藏這個 DLL,轉換並在作為 ArrayList 返回托管鏈表之前釋放原來的本地鏈表。哪種方法更好的呢?第一種策略的優點是你只需要編寫單一的轉換函數,它便可以在任何有本地鏈表的地方使用。第二個方法需要對每個創建鏈表的函數進行包裝。如果有多個創建鏈表的函數,則工作量稍大一些。但是其優點是它向 .NET 客戶端完全隱藏了所有的本地處理邏輯和細節。客戶端不再需要去處理 IntPtrs 或甚至是導入此 DLL,因為 ListWrap 隱藏了一切。這是我將要采用的方法,同時也是我鼓勵你在自己的程序中使用的方法。盡管對庫進行完全的包裝需要更多的努力,但是結果卻更加可靠和徹底的封裝。
有了 ManagedNode,剩下的事情便是包裝 AllocateList。這個過程非常簡單直白。首先,調用 AllocateList 分配本地鏈表,然後創建一個空的 ArrayList,接著將所有 NativeNodes 拷貝到 ManagedNodes 並將它們添加到托管鏈表中,最有離開時刪除它們。Figure 3 展示了所有的細節。托管 C++ 的優美之處在於即便是在處理混合對象時,所有的代碼看起來都很簡樸優雅。將本地 char* 拷貝到托管 String 用一個賦值即可,就像下面這行代碼:
mn->str = nn->str; // String = char*: it just works!
不需要調用轉換函數;編譯器知道該怎麼做。離開 CreateList 時刪除本地節點。這樣做比在末尾刪除它們存儲更有效。