本人同意他人對我的文章引用,但請在引用時注明出處,謝謝.作者:蔣志強 前幾次的學習中,我們知道了MFC程序結構的大致流程,知道MFC本質上和傳統的SDK API編程是完全一樣的.這次我們要了解MFC結構中很重要的兩個技術: 運行時類型識別(RTTI)和動態對象創建.
RTTI是RunTime Type Indentificaion的英文縮寫,也是運行時類型識別的意思.所謂運行時類型識別就是說,程序在運行的過程中具備識別內存中的對象是哪個類的實例的能力.
比如說,我們定義了一個類Class A1,並在程序中生成了這個類的一個對象A1 a1.那麼,我們如果希望有這個a1對象時,可以在程序運行中知道a1是A1類的對象,這種能力就是運行時類型識別(RTTI).
而動態對象創建是指程序在運行時,動態產生某個類的對象的能力.在C++中,我們可以在程序中方便的使用關鍵字new來動態對象創建.但是,這要求我們知道我們要new的對象是哪種類型的.也就是說我們知道需要new一個CVIEw還是需要new一個CWnd.如果我們只知道要new一個對象,而不知道要new哪種類型的對象,我們肯定無法動態創建對象.但我們有RTTI的能力的話,我們就知道需要創建哪種類型的對象了,再來做動態對象創建就是很容易的一個new語句了.所以動態對象創建和RTTI是緊密聯系的.在MFC中,RTTI和動態對象創建這兩種能力是通過MFC代碼中的幾個設計巧妙的宏來完成的,下面我們來看看MFC是怎麼使用這幾個宏完成這樣的功能的.
我們新建一個基於單文檔結構的工程,名字叫mfcstudy001,我們來看看MFC是怎麼做的,我們找到CMfcstudy001Doc這個類的定義,我們可以看到在該類的定義中有這樣的代碼:
protected: // create from serialization only
CMfcstudy001Doc();
DECLARE_DYNCREATE(CMfcstudy001Doc)
其中的DECLARE_DYNCREATE是MFC預定義的宏,在VC6的環境中我們選中這個宏,直接按F12轉到該宏的定義處,可以發現這個宏的定義如下:
#define DECLARE_DYNCREATE(class_name)
DECLARE_DYNAMIC(class_name)
static CObject* PASCAL CreateObject();
所以說進行一次宏替換後CMfcstudy001Doc類的定義中的宏展開應該是這樣的:
protected: // create from serialization only
DECLARE_DYNAMIC(CMfcstudy001Doc)
static CObject* PASCAL CreateObject();而該宏中又有一個叫做DECLARE_DYNAMIC的宏,我們再按F12找到這個宏的定義如下:
宏裡面的##符號表示將該符號前後的字符串連接起來,所以在CMfcstudy001Doc類的定義中的宏完全展開後,應該是下面這樣的代碼:
protected: // create from serialization only
protected:
static CRuntimeClass* PASCAL _GetBaseClass();
public:
static const AFX_DATA CRuntimeClass classCMfcstudy001Doc;
virtual CRuntimeClass* GetRuntimeClass() const;
static CObject* PASCAL CreateObject();
這個宏替換後的代碼添加了3個方法和一個CRuntimeClass類型的變量.這個CRuntimeClass類型是MFC預定義的結構體類型,該類型的變量是實現RTTI的關鍵所在.因為該結構體成員變量被聲明為static的,也就是靜態的.我們知道在C++中,全局變量和靜態變量都會在程序進入入口函數以前(main函數或者是WinMain函數)進行分配空間實例化.所以這個CRuntimeClass類型的結構體變量成員,在程序執行第一條語句之前就生成了.如果每一個類都有這樣的一個靜態成員,並且該結構記錄該類的類型信息的話,這就可以作為RTTI的基礎,來判斷類型了!實際上MFC就是這樣做的,我們先按F12來看看CRuntimeClass這種結構體的定義:
struct CRuntimeClass
...{
// Attributes
LPCSTR m_lpszClassName;
int m_nObjectSize;
UINT m_wSchema; // schema number of the loaded class
CObject* (PASCAL* m_pfnCreateObject)(); // NULL => abstract class
#ifdef _AFXDLL
CRuntimeClass* (PASCAL* m_pfnGetBaseClass)();
#else
CRuntimeClass* m_pBaseClass;
#endif
// Operations
CObject* CreateObject();
BOOL IsDerivedFrom(const CRuntimeClass* pBaseClass) const;
// Implementation
void Store(CArchive& ar) const;
static CRuntimeClass* PASCAL Load(CArchive& ar, UINT* pwSchemaNum);
// CRuntimeClass objects linked together in simple list
CRuntimeClass* m_pNextClass; // linked list of registered classes
};
我們從代碼的定義和注釋可以知道,這個結構體記錄了類的名字,類的大小等信息,還有一個指向CRuntimeClass類型的指針.這個指針的名字是m_pBaseClass,所以我們猜測是指向該類的基類中的這個結構體,從而按照類的繼承關系組織,成為一個根節點為CObject類中名為classCObject的CRuntimeClass結構體的多叉樹.我們會在後面的分析中驗證我們的猜測是正確的.結構體中CreateObject方法很明顯是用於動態創建該類型的對象,我們都可以在後面的分析中驗證.至於後面的Store是和序列化相關的內容,這和我們今天討論的主題無關.我們先忽略跳過.
現在我們到CMfcstudy001Doc類的代碼文件CMfcstudy001Doc.cpp中去看看.我們在文件的頭部可以看到這樣的一個宏: IMPLEMENT_DYNCREATE(CMfcstudy001Doc, CDocument)
按F12跳轉到定義處,我們看到這個宏是這樣定義的:
#define IMPLEMENT_DYNCREATE(class_name, base_class_name)
CObject* PASCAL class_name::CreateObject()
...{ return new class_name; }
IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, 0xFFFF,
class_name::CreateObject)
這個宏裡又嵌套了一個名為IMPLEMENT_RUNTIMECLASS的宏,我們直接按F12去看看IMPLEMENT_RUNTIMECLASS,如下:
#define IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, pfnNew)
CRuntimeClass* PASCAL class_name::_GetBaseClass()
...{ return RUNTIME_CLASS(base_class_name); }
AFX_COMDAT const AFX_DATADEF CRuntimeClass class_name::class##class_name = ...{
#class_name, sizeof(class class_name), wSchema, pfnNew,
&class_name::_GetBaseClass, NULL };
CRuntimeClass* class_name::GetRuntimeClass() const
...{ return RUNTIME_CLASS(class_name); }
經過兩次宏替換後,以前CMfcstudy001Doc.cpp文件中IMPLEMENT_DYNCREATE(CMfcstudy001Doc,CDocument)在預編譯後,變成下面的代碼:
CObject* PASCAL CMfcstudy001Doc::CreateObject() ...{ return new CMfcstudy001Doc; } CRuntimeClass* PASCAL CMfcstudy001Doc::_GetBaseClass()
...{ return RUNTIME_CLASS(CDocument); }
AFX_COMDAT const AFX_DATADEF CRuntimeClass CMfcstudy001Doc::class CMfcstudy001Doc = ...{
“CMfcstudy001Doc”, sizeof(class CMfcstudy001Doc), wSchema, pfnNew,
& CMfcstudy001Doc::_GetBaseClass, NULL };
CRuntimeClass* class_name::GetRuntimeClass() const
...{ return RUNTIME_CLASS(CMfcstudy001Doc); }
在該代碼中,首先實現CreateObject方法,產生該類型的對象,第二部分是填充CMfcstudy001Doc類中的CRuntimeClass類型的結構體,我們看到由於這個宏設計得非常巧妙,在填充結構體的倒數第二個字段時,使用& CMfcstudy001Doc::_GetBaseClass,這樣就按照類的繼承結構由下向上將這些CRuntimeClass組織成為了以為CObject為根的多叉樹.對於CObject比較特殊,因為它是最上級的類,它沒有父類了,所以只要將CObject中的名為classCObject的CRuntimeClass結構體中m_pBaseClass指針設置為NULL,就可以很完美的建立這個結構體了!
實際上對於所以需要做動態類型識別和動態類型創建的類,MFC會自動的在該類的定義加上# DECLARE_DYNCREATE宏,在該類的實現代碼加上#IMPLEMENT_DYNCREATE宏.有了這兩個宏以後,根據我們上面的分析,我們可以知道MFC是這樣做的工作.
所有這些被VC自動加上這兩個宏的類,都定義了名字為class然後加上該類名的一個CRuntimeClass靜態結構,以及_GetBaseClass()和GetRuntimeClass()這兩個方法.
程序被雙擊運行後,加載到系統的內存中,在程序進入入口函數以前,所有的這些有靜態的CRuntimeClass成員的類,它們中的該靜態結構體被分配空間,並且被結構體內容被填充.並且由於MFC的IMPLEMENT_DYNCREATE的巧妙設計,這些靜態結構體相互按照繼承關系,組織成一個多叉樹的結構.這些結構體中記錄了對應該類的信息.可以根據類的名字字符串字段來判斷類型,並且向上搜索直到頂層的CObject類中的靜態結構體,實現RTTI.在這些結構體中還有CreateObject方法,該方法調用對應類中定義的靜態方法CreateObject實現動態創建.做所有這些事情,我們所需要的只是得到該類的CRuntimeClass類型靜態結構體classXXX(XXX表示該類的名字).而由於該結構體是靜態的,所以我們可以在任何時候,使用類的名字來訪問它,實現所有的這些功能.
還有一個宏,我們沒有看, RUNTIME_CLASS,它的定義是這樣的:
#define RUNTIME_CLASS(class_name) ((CRuntimeClass*)(&class_name::class##class_name))
也就是說RUNTIME_CLASS(CMfcstudy001Doc)替換後
就變成((CRuntimeClass*)(& CMfcstudy001Doc::class CMfcstudy001Doc)),我們給出類的名字,通過這個宏替換後,就是該類中CRuntimeClass類型的靜態結構的地址.
有了這個結構體的地址,我們可以用這個結構體做RTTI,動態對象創建.真是感歎MFC的設計者的巧妙構思啊!!!
回憶我們以前的討論中有提到,在文檔/視圖結構的MFC程序中,CWinApp中使用一個CDocManger類型的對象管理文檔模版對象,並組織成一個隊列,而文檔模版對象從邏輯上組織視圖對象,文檔對象和主窗口對象.單文檔的情況下CDocManager只能在隊列中加入一個文檔模版對象,多文檔可以在隊列中加入多個文檔模版對象.
但是文檔模版對象是如何從”邏輯”上進行組織的呢?其實就是通過的各個類中靜態的CRuntimeClass類型變量來做的,我們來看看CWinApp的InitInstance方法,這部分代碼我們以前分析過,我們明白RTTI和動態對象創建機制後,在看一下:
BOOL CMfcstudy001App::InitInstance()
...{
AfxEnableControlContainer();
// Standard initialization
// If you are not using these features and wish to reduce the size
// of your final executable, you should remove from the following
// the specific initialization routines you do not need.
#ifdef _AFXDLL
Enable3dControls(); // Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif
// Change the registry key under which our settings are stored.
// TODO: You should modify this string to be something appropriate
// such as the name of your company or organization.
SetRegistryKey(_T("Local AppWizard-Generated Applications"));
LoadStdProfileSettings(); // Load standard INI file options (including MRU)
// Register the application's document templates. Document templates
// serve as the connection between documents, frame Windows and vIEws.
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CMfcstudy001Doc),
RUNTIME_CLASS(CMainFrame), // main SDI frame window
RUNTIME_CLASS(CMfcstudy001VIEw));
AddDocTemplate(pDocTemplate);
// Parse command line for standard shell commands, DDE, file open
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
// Dispatch commands specifIEd on the command line
if (!ProcessShellCommand(cmdInfo))
return FALSE;
// The one and only window has been initialized, so show and update it.
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
return TRUE;
}
在該函數中創建了一個CSingleDocTemplate的對象,構造函數中傳遞的參數中使用了RUNTIME_CLASS宏,現在我們知道這個宏實際上就是得到給定的類中的CRuntimeClass靜態結構體的指針.既然是CSingleDocTemplate的構造函數中,傳遞了主窗口類,視圖類,文檔類中的CRuntimeClass靜態結構體的指針.那麼就可以使用該文檔模版對象中得到的這些CRuntimeClass靜態結構體去實現主窗口類,視圖類,文檔類的動態對象創建了!!!
我們只分析了MFC實現RTTI和動態對象創建中最關鍵的部分,還有一些MFC中涉及這兩個技術的細節,我們沒有繼續深入.在對我們現在所理解的基礎上去看這兩個技術的代碼細節是很容易的事情了,我不寫出來,主要是由於我都寫了的話,內容會太多了,而我現在真的很疲倦了L.但有了上面的理解,這些內容就都很簡單了.
OK,很開心吧.MFC的設計者,微軟的工程師的設計真是不禁讓我拍案驚歎,實在是太巧妙了,太漂亮了.It is more than a skill, but a kind of art.下次我們再繼續研究MFC的設計,內容會越來越精彩的.
To be continued......