在托管 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@@$$FQ$AAM@XZ
老練的 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 時刪除本地節點。這樣做比在末尾刪除它們存儲更有效。
通過將整個鏈表轉換到托管對象(而不是用 interop 和 StructLayout 將它導出),使托管客戶端不用離開托管世界,此謂入鄉隨俗也!畢竟,某些程序員選擇 .NET 的一個主要理由是其自動的垃圾收集。如果你用 interop 直接導出鏈表,那麼你也必須導出 FreeList,從而必須讓使用基於 .NET 語言的其他程序員記得調用它。
一般來說,如果你要導出到托管世界,最好將數據盡可能多地轉換成托管對象。否則你的客戶端也必須用 C++ 編寫代碼。當然,這個規則並不總是適用。有時直接導出結構並讓客戶端釋放它們更好——例如,如果拷貝動作會引發核心不可接受的性能問題或內存沖突。那麼你必須做出判斷以決定是走托管之路還是使用本地機制。
我正在使用 C++ 托管擴展(Managed Extension for C++)包裝現存的 C++ 庫,以便基於 .NET 的語言能訪問它。在 托管 C++ 中,我可以寫如下代碼:
String* s = new String();
s = _T("Hello, world");
但我如何才能將一個托管 String 轉換回本地的 TCHAR*?
一旦你知道了這個神奇的方法,它便很簡單。你必須調用 PtrToStringChars 並對結果進行 pin (銷連接)操作。代碼可以這樣你寫:
String __gc* s = S"Hello";
const wchar_t __pin* p = PtrToStringChars(s);
不要忘了對 PtrToStringChars 返回的指針進行 __pin 操作。銷連接是必不可少的,因為 PtrToStringChars 返回指向托管內存中 String 對象第一個字符的托管(__gc)指針,垃圾收集器可能在任何高興的時候移走托管內存,除非你顯示地對之進行 __pin 操作。一般來講,你必須在將 __gc 指針傳遞給某個本地(非托管)函數的任何時候使用用 __pin。
Figure 4 展示了一個簡短的程序,它將托管 String 轉換為寬字符和 ANSI 字符串兩者。為了轉換到 ANSI,要用到你寵愛的轉換函數,象 wcstombs 或 ATL W2A 宏。如果你使用 MFC CString,你不必任何事情,因為 CString 具備針對 char* 和 wchar_t 的賦值操作:
// both will work
CString s1 = "hello, world";
CString s2 = L"Hello, world";
我想在自己的應用程序中改變標簽控件的背景顏色,將它從灰色改成白色。我嘗試建立一個 CTabCtrl 的派生類並使用其全部功能,但沒有成功,你能幫我一把嗎?
改變標簽控件中標簽的顏色十分簡單,但要想讓屬性頁充滿某種顏色,這個改造涉及相當大的工作量,對於一個膽小的人來說,是不敢輕舉妄動的。對於標簽來說,基本思路讓該控件是自繪控件,然後處理 WM_DRAWITEM 消息。如果使用 MFC,你可以改寫虛擬函數 DrawItem。
在 1998 年三月坎的 Microsoft Systems Journal 中,我示范了如何實現一個標簽控件類 CTabCtrlWithDisable,這個類支持標簽禁用。作為禁用標簽的一部份,當標簽被禁用時, CTabCtrlWithDisable 將標簽文本顏色改成了淺灰色,本文我借用了 CTabCtrlWithDisable 中的一些代碼實現了一個新類 CColorTablCtrl,使你能改變標簽的顏色。(參見 Figure 5)
為了使用 CColorTablCtrl, 在你的屬性頁中創建一個實例:
class CMyPropSheet : public CPropertySheet {
protected:
CColorTabCtrl m_tabCtrl;
};
你必須在屬性頁的 OnInitDialog 處理例程(這樣 MFC 將使用它)中子類化標簽控件,然後按照自己的意願設置前景色和背景色:
// in CMyPropSheet::OnInitDialog()
HWND hWndTab = (HWND)SendMessage(PSM_GETTABCONTROL);
m_tabCtrl.SubclassDlgItem(::GetDlgCtrlID(hWndTab), this);
m_tabCtrl.SetColor(WHITE, RED);
這裡 WHITE 和 RED 是標准的 COLORREF 值,也就是 RGB(255, 255, 255) 和 RGB(255,0,0)。一旦你實例化並初始化 CColorTabCtrl,顏色標簽控件便負責其余的事情。(參見 Figure 6)
Figure 6 帶顏色的標簽控件
CColorTabCtrl 有一個改寫的 SubclassDlgItem,它調用 ModifyStyle 將式樣改變為 TCS_OWNERDRAWFIXED。比較適合這項工作的地方是 PreSubclassWindow 函數中,因為不論控件被子類化,還是用 CreateWindow 創建(但在此雜志中,我得收縮代碼,所以我采用的是簡版),它都要被調用。注意 SubclassDlgItem 只是簡單地改寫,不是虛擬函數。為了設置顏色,SetColor 保存在兩個成員變量 m_clrBackground 和 m_clrForeground.中傳遞的顏色。
一旦 將式樣設置為自繪方式,只要到了繪制該標簽時,Windows 便會發送 WM_DRAWITEM 消息。MFC 捕獲這個消息並調用標簽控件的虛擬 DrawItem 函數,它由 CColorTabCtrl 實現,用新的顏色繪制文本:
// in CColorTabCtrl::DrawItem
dc.FillSolidRect(rc, m_clrBackground);
dc.SetBkColor(m_clrBackground);
dc.SetTextColor(m_clrForeground);
dc.DrawText(...);
就這麼簡單直白,具體細節請看源代碼。由於你可能在頁面顏色沒有改變的情況下也不想改變標簽顏色,所以我還實現了一個 CColorPropertyPage 類,使你能改變屬性頁的背景色以便和標簽顏色匹配,如果 Figure 6 所示,對於屬性頁而言,改變背景色最容易的方法是處理 WM_ERASEBKGND:
BOOL CColorPropertyPage::OnEraseBkgnd(CDC* pDC)
{
CRect rc;
GetClientRect(&rc);
pDC->FillSolidRect(rc, m_clrBackground);
return TRUE;}
如果你只是嘗試,你會發現各種惱人的問題。首先,如果你改變頁面顏色,所有控件的背景色都是錯誤的,所以你必須還要解決這個問題。為此,你不得不處理處理 WM_CTLCOLOR 和 WM_ERASEBKGND。具體細節請參考我在 MSJ May 1997 專欄的文章。
另外一個問題是標簽控件仍然以系統3D顏色繪制出邊緣和四個角。這樣肯定不行,為解決這個問題,除了處理 WM_PAINT 消息別無選擇,並且要自己負責整個的繪制操作。它包括繪制選中時標簽之間的偏移,以便使它看起來是在前面。至此,可以說你已開始重新發明標簽控件,每一個使用 Windows 的程序員都知道,改變控件的顏色最痛苦的一件事情,並且一旦你開始走上這條路,便會覺得要做完它真是遙遙無期。不久標准的顏色看起來將比起初的顏色更好,或者你將會覺得為什麼不轉到 .NET 框架上來,在那上面改變顏色實在簡單,用如下代碼即可:
ctl.BackColor = Color.Aquamarine;
編程愉快!