在 C++ 中,無法從某個類的構造函數中調用派生的虛擬函數,因為虛表還沒有完全建立。但是在C#中好像就可以,是這樣嗎?為什麼會有這種差別呢?
確實如此,在這個方面 C# 與 C++ 是有差別的。在 C++ 中,如果你從構造函數或者析構函數中調用虛擬函數,編譯器調用的虛擬函數是定義在這個正在被構造的類實例中的(例如,如果從 Base::Base 中調用 Base::SomeVirtFn ),不是最底層派生的實例(the most derived instance),正像你說的那樣,因為在最底層派生的構造函數執行之前,虛表還沒有完全被初始化。另一種說法是派生類還沒有被創建。
Figure 2 虛擬函數 TestSimilarly
當你從析構函數中調用虛函數時,C++ 調用該基類的析構函數,因為派生類已經被銷毀(其析構已經被調用)。雖然這個行為可導致異常結果(此即為什麼從構造函數或析構函數中調用虛函數被認為是糟糕的編程實踐的原因),它是大多數 C++ 程序員必須了然於心的基本常識。
正如你所指出的那樣,在 C# 有所不同。托管對象——無論是在 C#,托管 C++ 中,還是任何其它的 .NET 兼容語言中——是作為其最終類型被創建的,也就是說,如果你從構造函數或析構函數中調用虛函數,系統調用的是最末層派生的函數。Figure 1 所示程序舉例說明了這一點。如果你編譯並運行這個程序,你會看到 Figure 2 所示輸出。
這種行為對於 C++ 程序員來說似乎有些奇特。它意味著在派生類被初始化之前,你可以調用某個派生類型的虛擬函數——也就是說在其構造函數運行之前。同樣,如果你從基類析構函數中調用虛函數,該函數是在派生類被銷毀之後運行的——也就是說在析構函數被調用之後。那麼先不說這種差別存在的原因,剛才不是還說從構造函數/析構函數中調用虛函數被認為是糟糕的實踐。
為什麼微軟的家伙們要像這樣來設計 C# 呢?因為它簡化了內存管理。垃圾收集器為了釋放內存,它需要知道對象有多大。如果 C# 像 C++ 那樣構造對象,那麼你可能會碰到這樣一種情況:有兩個對象,Obj1 和 Obj2,下面這兩條語句都為真:
typeof(Obj1)==typeof(Obj2)
sizeof(Obj1)!= sizeof(Obj2)
因為對象之一是被部分構造。(不要忘了垃圾收集器是異步運行的。)通過將對象構造成最終類型,垃圾收集器能從其類型決定對象的大小。如果 C# 像 C++ 那樣進行部分構造,則垃圾收集器將需要更多的代碼來決定部分構造對象的真實大小。這樣將帶來復雜性和性能下降,首先要解決這個問題很讓人氣餒,所以為了較快的垃圾收集利益,微軟的家伙們決定像上面那樣來實現 C#。有關這方面的討論參見 Raymond Chen 的 blog:“The Old New Thing”。
在 2004 三月的專欄中,你展示了如何改變文件打開對話框的最新視圖狀態設置,但沒有涉及到保存這個用戶使用的最新視圖設置。我遇到的問題是讀取用戶已有的打開文件對話框設置。我只找到直接讀取列表框信息的方法,但當用戶選擇縮略圖模式時,那樣做不能得到正確的信息。對此你有沒有解決辦法?
我正在用公共的 CFileDialog 類做開發,應該不是很難,但事情似乎並不是那樣。我想強制文件打開對話框的視圖模式為縮略圖。我要用 Visual C++ 來做,你能否提供一些建議?
有幾個讀者都在問文件打開對話框中的縮略圖問題。在我三月份的專欄中,我示范了如果向文件打開對話框中的 SHELLDLL_DefView 專用窗口發送 WM_COMMAND 消息以設置不同的視圖模式——但你如何知道當前所處的模式是哪一個呢?你必須獲取列表控件並調用 CListCtrl::GetView:
// in dialog class
HWND hlc = ::FindWindowEx(m_hWnd,
NULL, _T("SysListView32"), NULL);
CListCtrl* plc = (CListCtrl*)CWnd::FromHandle(hlc);
DWORD dwView = plc->GetView();
CListCtrl::GetView 返回 LV_XXX 代碼之一,但正像 Maarten 發現的那樣,Windows 對圖標模式和縮略圖模式都返回 LV_VIEW_ICON。
那麼如何區分到底是哪種視圖模式呢?我絞盡腦汁並鑽進頭文件查找,最後發現一個叫 LVM_GETITEMSPACING 的消息,該消息是作什麼用的呢——用來獲取圖標間隔。顧名思義,圖標間隔是圖標視圖模式中圖標之間的像素間隔。LVM_GETITEMSPACING 不是很好使用,以至於 MFC 都沒有對之進行包裝(比如說 MFC 中並沒有 CListCtrl::GetIconSpacing 這樣的函數)。所以在 MFC 中你得自己發送消息:
CSize sz = CSize(plc->SendMessage(LVM_GETITEMSPACING));
Windows 按照通常方式返回尺寸,在高位和低位字中編碼的 cx/cy,然後CSize很禮貌地為你進行解碼。一旦有了圖標間隔,你便可以將它與 GetSystemMetrics(SM_CXICONSPACING) 返回的系統間隔值進行比較。如果列表視圖的圖標間隔與系統的一樣,則視圖是圖標模式。如果大於系統間隔,則視圖為縮略圖模式:
if (sz.cx > GetSystemMetrics(SM_CXICONSPACING)) {
// thumbnail view
} else {
// icon view
}
講了那麼多縮略圖,接下來的問題是如何持續化不同用戶會話的視圖狀態?對此,當程序終止時,你需要用 Profile 函數在用戶配置文件中保存最後使用的模式,並在下一次啟動程序時再次恢復它。我寫了一個小示范程序,DlgTest。程序使用了一個實現持續化程序行為的類 CPersistOpenDlg。這個類又借助另外一個類 CListViewShellWnd,用它來封裝 SHELLDLL_DefView 窗口(參見三月份專欄)。CListViewShellWnd 包含獲取和設置視圖模式的函數,由這些函數來區分圖標和縮略圖模式:
CListViewShellWnd m_wndLVSW;
...
m_wndLVSW.SetViewMode(ODM_VIEW_THUMBS);
CListViewShellWnd 的 OnDestroy 處理器在某個數據成員 m_lastViewMode 中保存視圖模式。當對話框被銷毀時,CPersistOpenDlg 的析構函數調用 WriteProfileInt 將這個值寫入用戶配置文件。對話框啟動時,CPersistOpenDlg 給自己送一個初始化消息;該消息處理例程調用 GetProfileInt 從磁盤讀取存儲在配置文件中的值並設置視圖模式。PostMessage 是必須調用的,因為常規初始化消息 WM_INITDIALOG 和 CDN_INITDONE 在文件對話框被完全初始化之前就會到來——有關這一點的解釋參見三月份專欄。
順便說一下,任何時候你都應該使用 GetProfileXxx 和 WriteProfileXxx 來持續化應用程序的設置。MFC 用 CWinApp 包裝了這些函數。如果你在應用程序啟動時調用(一般都是在 InitInstance 函數中) CMyApp::SetRegistryKey("KeyName"),MFC 使用注冊表來存儲用戶配置信息,而不是 INI 文件。下面是 DlgTest 用的 INI 文件:
[settings]
ViewMode=28717
偶爾在一些文字資料和 C++ 文檔以及 Microsoft .NET 框架中看到術語“POD 類型”。這個術語是什麼意思?
你可以將 POD 類型看作是一種來自外太空的用綠色保護層包裝的數據類型,POD 意為“Plain Old Data”(譯者:如果一定要譯成中文,那就叫“徹頭徹尾的老數據”怎麼樣!)這就是 POD 類型的含義。其確切定義相當粗糙(參見 C++ ISO 標准),其基本意思是 POD 類型包含與 C 兼容的原始數據。例如,結構和整型是 POD 類型,但帶有構造函數或虛擬函數的類則不是。 POD 類型沒有虛擬函數,基類,用戶定義的構造函數,拷貝構造,賦值操作符或析構函數。
為了將 POD 類型概念化,你可以通過拷貝其比特來拷貝它們。此外, POD 類型可以是非初始化的。例如:
struct RECT r; // value undefined
POINT *ppoints = new POINT[100]; // ditto
CString s; // calls ctor ==> not POD
非 POD 類型通常需要初始化,不論是調用缺省的構造函數(編譯器提供的)還是自己寫的構造函數。
過去, POD 對於編寫編譯器或與C 兼容的 C++ 程序的人來說很重要。現在,POD 來到 .NET 的環境中。在托管 C++ 中,托管類型(包括 __value 和 __gc 兩者)能包含嵌入的原生 POD 類型。 Figure 3 展示了例舉說明代碼。托管的 Circle 類能包含 POINT,但無法包含 CPoint 類。如果你嘗試編譯 pod.cpp 會報一個 C3633 錯誤:“Cannot define ''m_center'' as a member of managed ''Circle'' because of the presence of default constructor ''CPoint::CPoint'' on class ''CPoint''.”(譯者:意思是由於類 CPoint 有缺省的構造函數‘CPoint::CPoint’,所以不能將‘m_center’定義為托管類‘Circle’的一個成員)
.NET 限定嵌入的本地對象只能為 POD 類型的理由是這樣做能安全地拷貝它們,不用擔心調用構造函數,初始化虛表,或任何非 POD 類型需要的其它機制。