本文涵蓋三個核心 C++/.NET 主題:建構函式中呼叫虛擬函式的 C++/C# 差異、檔案開啟對話方塊視圖模式持久化、POD(Plain Old Data)型別定義與應用。
C++ 中,基底類別建構函式執行時,衍生類別尚未建構,虛擬函式表(vtable)尚未完成初始化。因此從基底建構函式呼叫虛擬函式,會解析至基底類別的實作,而非最底層衍生類別。同理,解構函式執行時衍生部分已銷毀,呼叫虛擬函式同樣解析至基底實作。
class Base {
public:
Base() { virtualFunc(); } // 呼叫 Base::virtualFunc
virtual void virtualFunc() { cout << "Base"; }
};
class Derived : public Base {
public:
Derived() { }
void virtualFunc() override { cout << "Derived"; }
};
// new Derived() 輸出 "Base"
此行為雖為 C++ 基本常識,但常導致微妙錯誤,公認屬不良實踐。
C#(及所有 .NET 語言)中,托管物件一創建即為其最終型別。建構函式呼叫虛擬函式會解析至最底層衍生實作,即使衍生建構函式尚未執行。解構函式(Finalize)同理。
class Base {
public Base() { VirtualMethod(); }
public virtual void VirtualMethod() { Console.WriteLine("Base"); }
}
class Derived : Base {
public Derived() { }
public override void VirtualMethod() { Console.WriteLine("Derived"); }
}
// new Derived() 輸出 "Derived"
.NET 垃圾收集器需知物件大小以回收記憶體。若採 C++ 分階段建構,同一型別物件在建構途中大小可能不同,GC 需額外邏輯追蹤部分建構物件,增加複雜度與效能損耗。為簡化 GC、提升速度,.NET 採用「一次建構為最終型別」模型。詳細討論見 Raymond Chen 部落格《The Old New Thing》。
問題:如何讀取/儲存使用者上次選擇的視圖模式(圖示、縮圖、清單、詳細資料)?
透過 FindWindowEx 找到 SysListView32 控制項,呼叫 CListCtrl::GetView() 取得 LV_VIEW_* 常數。但圖示模式與縮圖模式皆回傳 LV_VIEW_ICON,需進一步區分。
發送 LVM_GETITEMSPACING 訊息取得圖示間距:
CSize sz = CSize(plc->SendMessage(LVM_GETITEMSPACING));
if (sz.cx > GetSystemMetrics(SM_CXICONSPACING))
// 縮圖模式(間距大於系統預設)
else
// 圖示模式
CListViewShellWnd::OnDestroy 儲存模式至成員變數。CPersistOpenDlg 解構函式呼叫 WriteProfileInt 寫入使用者設定檔(或登錄檔)。CPersistOpenDlg 發送初始化訊息,GetProfileInt 讀取並 SetViewMode 還原。PostMessage 延遲初始化,因 WM_INITDIALOG/CDN_INITDONE 觸發時對話方塊尚未完全就緒。建議使用 CWinApp::SetRegistryKey 導向登錄檔儲存,而非 INI 檔。
POD = Plain Old Data。指與 C 相容的純資料型別:無虛擬函式、無基底類別、無使用者定義建構/複製/指派/解構函式。例如 struct RECT、int、POINT 皆為 POD;CString、CPoint(有建構函式)則非 POD。
memcpy 逐位元複製。struct RECT r; 值未定義)。托管類別(__value、__gc)僅能內嵌原生 POD 型別。嘗試內嵌 CPoint 會產生 C3633 錯誤:「因 CPoint 有預設建構函式,無法定義為托管類別成員」。原因:POD 可安全逐位元複製,無需擔心建構函式、虛擬表初始化等機制。
__gc class Circle {
Point m_center; // OK:POD
// CPoint m_center; // Error C3633
};
理解 POD 對跨語言互通、序列化、高效能資料結構設計至關重要。