CObject是大多數MFC類的根類或基類。CObject類有很多有用的特性:對運行時類信息的支持,對動態創建的支持,對串行化的支持,對象診斷輸出,等等。MFC從CObject派生出許多類,具備其中的一個或者多個特性。程序員也可以從CObject類派生出自己的類,利用CObject類的這些特性。
本章將討論MFC如何設計CObject類的這些特性。首先,考察CObject類的定義,分析其結構和方法(成員變量和成員函數)對CObject特性的支持。然後,討論CObject特性及其實現機制。
CObject的結構
以下是CObject類的定義:
class CObject
{
public:
//與動態創建相關的函數
virtual CRuntimeClass* GetRuntimeClass() const;
析構函數
virtual ~CObject(); // virtual destructors are necessary
//與構造函數相關的內存分配函數,可以用於DEBUG下輸出診斷信息
void* PASCAL operator new(size_t nSize);
void* PASCAL operator new(size_t, void* p);
void PASCAL operator delete(void* p);
#if defined(_DEBUG) && !defined(_AFX_NO_DEBUG_CRT)
void* PASCAL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
#endif
//缺省情況下,復制構造函數和賦值構造函數是不可用的
//如果程序員通過傳值或者賦值來傳遞對象,將得到一個編譯錯誤
protected:
//缺省構造函數
CObject();
private:
//復制構造函數,私有
CObject(const CObject& objectSrc); // no implementation
//賦值構造函數,私有
void operator=(const CObject& objectSrc); // no implementation
// Attributes
public:
//與運行時類信息、串行化相關的函數
BOOL IsSerializable() const;
BOOL IsKindOf(const CRuntimeClass* pClass) const;
// Overridables
virtual void Serialize(CArchive& ar);
// 診斷函數
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
// Implementation
public:
//與動態創建對象相關的函數
static const AFX_DATA CRuntimeClass classCObject;
#ifdef _AFXDLL
static CRuntimeClass* PASCAL _GetBaseClass();
#endif
};
由上可以看出,CObject定義了一個CRuntimeClass類型的靜態成員變量:
CRuntimeClass classCObject
還定義了幾組函數:
構造函數析構函數類,
診斷函數,
與運行時類信息相關的函數,
與串行化相關的函數。
其中,一個靜態函數:_GetBaseClass;五個虛擬函數:析構函數、GetRuntimeClass、Serialize、AssertValid、Dump。這些虛擬函數,在CObject的派生類中應該有更具體的實現。必要的話,派生類實現它們時可能要求先調用基類的實現,例如Serialize和Dump就要求這樣。
靜態成員變量classCObject和相關函數實現了對CObjet特性的支持。
CObject類的特性
下面,對三種特性分別描述,並說明程序員在派生類中支持這些特性的方法。
對運行時類信息的支持
該特性用於在運行時確定一個對象是否屬於一特定類(是該類的實例),或者從一個特定類派生來的。CObject提供IsKindOf函數來實現這個功能。
從CObject派生的類要具有這樣的特性,需要:
定義該類時,在類說明中使用DECLARE_DYNAMIC(CLASSNMAE)宏;
在類的實現文件中使用IMPLEMENT_DYNAMIC(CLASSNAME,BASECLASS)宏。
對動態創建的支持
前面提到了動態創建的概念,就是運行時創建指定類的實例。在MFC中大量使用,如前所述框架窗口對象、視對象,還有文檔對象都需要由文檔模板類(CDocTemplate)對象來動態的創建。
從CObject派生的類要具有動態創建的功能,需要:
定義該類時,在類說明中使用DECLARE_DYNCREATE(CLASSNMAE)宏;
定義一個不帶參數的構造函數(默認構造函數);
在類的實現文件中使用IMPLEMENT_DYNCREATE(CLASSNAME,BASECLASS)宏;
使用時先通過宏RUNTIME_CLASS得到類的RunTime信息,然後使用CRuntimeClass的成員函數CreateObject創建一個該類的實例。
例如:
CRuntimeClass* pRuntimeClass = RUNTIME_CLASS(CNname)
//CName必須有一個缺省構造函數
CObject* pObject = pRuntimeClass->CreateObject();
//用IsKindOf檢測是否是CName類的實例
Assert( pObject->IsKindOf(RUNTIME_CLASS(CName));
對序列化的支持
“序列化”就是把對象內容存入一個文件或從一個文件中讀取對象內容的過程。從CObject派生的類要具有序列化的功能,需要:
定義該類時,在類說明中使用DECLARE_SERIAL(CLASSNMAE)宏;
定義一個不帶參數的構造函數(默認構造函數);
在類的實現文件中使用IMPLEMENT_SERIAL(CLASSNAME,BASECLASS)宏;
覆蓋Serialize成員函數。(如果直接調用Serialize函數進行序列化讀寫,可以省略前面三步。)
對運行時類信息的支持、動態創建的支持、串行化的支持層(不包括直接調用Serailize實現序列化),這三種功能的層次依次升高。如果對後面的功能支持,必定對前面的功能支持。支持動態創建的話,必定支持運行時類信息;支持序列化,必定支持前面的兩個功能,因為它們的聲明和實現都是後者包含前者。
綜合示例:
定義一個支持串行化的類CPerson:
class CPerson : public CObject
{
public:
DECLARE_SERIAL( CPerson )
// 缺省構造函數
CPerson(){}{};
CString m_name;
WORD m_number;
void Serialize( CArchive& archive );
// rest of class declaration
};
實現該類的成員函數Serialize,覆蓋CObject的該函數:
void CPerson::Serialize( CArchive& archive )
{
// 先調用基類函數的實現
CObject::Serialize( archive );
// now do the stuff for our specific class
if( archive.IsStoring() )
archive << m_name << m_number;
else
archive >> m_name >> m_number;
}
使用運行時類信息:
CPerson a;
ASSERT( a.IsKindOf( RUNTIME_CLASS( CPerson ) ) );
ASSERT( a.IsKindOf( RUNTIME_CLASS( CObject ) ) );
動態創建:
CRuntimeClass* pRuntimeClass = RUNTIME_CLASS(CPerson)
//Cperson有一個缺省構造函數
CObject* pObject = pRuntimeClass->CreateObject();
Assert( pObject->IsKindOf(RUNTIME_CLASS(CPerson));
實現CObject特性的機制
由上,清楚了CObject的結構,也清楚了從CObject派生新類時程序員使用CObject特性的方法。現在來考察這些方法如何利用CObjet的結構,CObject結構如何支持這些方法。
首先,要揭示DECLARE_DYNAMIC等宏的內容,然後,分析這些宏的作用。
DECLARE_DYNAMIC等宏的定義
MFC提供了DECLARE_DYNAMIC、DECLARE_DYNCREATE、DECLARE_SERIAL聲明宏的兩種定義,分別用於靜態鏈接到MFC DLL和動態鏈接到MFC DLL。對應的實現宏IMPLEMNET_XXXX也有兩種定義,但是,這裡實現宏就不列舉了。
MFC對這些宏的定義如下:
#ifdef _AFXDLL //動態鏈接到MFC DLL
#define DECLARE_DYNAMIC(class_name)
protected:
static CRuntimeClass* PASCAL _GetBaseClass();
public:
static const AFX_DATA CRuntimeClass class##class_name;
virtual CRuntimeClass* GetRuntimeClass() const;
#define _DECLARE_DYNAMIC(class_name)
protected:
static CRuntimeClass* PASCAL _GetBaseClass();
public:
static AFX_DATA CRuntimeClass class##class_name;
virtual CRuntimeClass* GetRuntimeClass() const;
#else
#define DECLARE_DYNAMIC(class_name)
public:
static const AFX_DATA CRuntimeClass class##class_name;
virtual CRuntimeClass* GetRuntimeClass() const;
#define _DECLARE_DYNAMIC(class_name)
public:
static AFX_DATA CRuntimeClass class##class_name;
virtual CRuntimeClass* GetRuntimeClass() const;
#endif
// not serializable, but dynamically constructable
#define DECLARE_DYNCREATE(class_name)
DECLARE_DYNAMIC(class_name)
static CObject* PASCAL CreateObject();
#define DECLARE_SERIAL(class_name)
_DECLARE_DYNCREATE(class_name)
friend CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb);
由於這些聲明宏都是在CObect派生類的定義中被使用的,所以從這些宏的上述定義中可以看出,DECLARE_DYNAMIC宏給所在類添加了一個CRuntimeClass類型的靜態數據成員class##class_name(類名加前綴class,例如,若類名是CPerson,則該變量名稱是classCPerson),且指定為const;兩個(使用MFC DLL時,否則,一個)成員函數:虛擬函數GetRuntimeClass和靜態函數_GetBaseClass(使用MFC DLL時)。
DECLARE_DYNCREATE宏包含了DECLARE_DYNAMIC,在此基礎上,還定義了一個靜態成員函數CreateObject。
DECLARE_SERIAL宏則包含了_DECLARE_DYNCREATE,並重載了操作符“>>”(友員函數)。它和前兩個宏有所不同的是CRuntimeClass數據成員class##class_name沒有被指定為const。
對應地,MFC使用三個宏初始化DECLARE宏所定義的靜態變量並實現DECLARE宏所聲明的函數:IMPLEMNET_DYNAMIC,IMPLEMNET_DYNCREATE,IMPLEMENT_SERIAL。
首先,這三個宏初始化CRuntimeClass類型的靜態成員變量class#class_name。IMPLEMENT_SERIAL不同於其他兩個宏,沒有指定該變量為const。初始化內容在下節討論CRuntimeClass時給出。
其次,它實現了DECLARE宏聲明的成員函數:
_GetBaseClass()
返回基類的運行時類信息,即基類的CRuntimeClass類型的靜態成員變量。這是靜態成員函數。
GetRuntimeClass()
返回類自己的運行類信息,即其CRuntimeClass類型的靜態成員變量。這是虛擬成員函數。
對於動態創建宏,還有一個靜態成員函數CreateObject,它使用C++操作符和類的缺省構造函數創建本類的一個動態對象。
操作符的重載
對於序列化的實現宏IMPLEMENT_SERIAL,還重載了操作符<<和定義了一個靜態成員變量
static const AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name));
比如,對CPerson來說,該變量是_init_Cperson,其目的在於靜態成員在應用程序啟動之前被初始化,使得AFX_CLASSINIT類的構造函數被調用,從而通過AFX_CLASSINIT類的構造函數在模塊狀態的CRuntimeClass鏈表中插入構造函數參數表示的CRuntimeClass類信息。至於模塊狀態,在後文有詳細的討論。
重載的操作符函數用來在序列化時從文檔中讀入該類對象的內容,是一個友員函數。定義如下:
CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb)
{
pOb = (class_name*) ar.ReadObject(
RUNTIME_CLASS(class_name));
return ar;
}
回顧CObject的定義,它也有一個CRuntimeClass類型的靜態成員變量classCObject,因為它本身也支持三個特性。
以CObject及其派生類的靜態成員變量classCObject為基礎,IsKindOf和動態創建等函數才可以起到作用。
這個變量為什麼能有這樣的用處,這就要分析CRuntimeClass類型變量的結構和內容了。下面,在討論了CRuntimeClass的結構之後,考察該類型的靜態變量被不同的宏初始化之後的內容。
CruntimeClass類的結構與功能
從上面的討論可以看出,在對CObject特性的支持上,CRuntimeClass類起到了關鍵作用。下面,考查它的結構和功能。
CRuntimeClass的結構
CruntimeClass的結構如下:
Struct CRuntimeClass
{
LPCSTR m_lpszClassName;//類的名字
int m_nObjectSize;//類的大小
UINT m_wSchema;
CObject* (PASCAL* m_pfnCreateObject)();
//pointer to function, equal to newclass.CreateObject()
//after IMPLEMENT
CRuntimeClass* (PASCAL* m_pfnGetBaseClass)();
CRumtieClass* m_pBaseClass;
//operator:
CObject *CreateObject();
BOOL IsDerivedFrom(const CRuntimeClass* pBaseClass) const;
...
}
CRuntimeClass成員變量中有兩個是函數指針,還有幾個用來保存所在CruntimeClass對象所在類的名字、類的大小(字節數)等。
這些成員變量被三個實現宏初始化,例如:
m_pfnCreateObject,將被初始化指向所在類的靜態成員函數CreateObject。CreateObject函數在初始化時由實現宏定義,見上文的說明。
m_pfnGetBaseClass,如果定義了_AFXDLL,則該變量將被初始化指向所在類的成員函數_GetBaseClass。_GetBaseClass在聲明宏中聲明,在初始化時由實現宏定義,見上文的說明。
下面,分析三個宏對CObject及其派生類的CRuntimeClass類型的成員變量class##class_name初始化的情況,然後討論CRuntimeClass成員函數的實現。
成員變量class##class_name的內容
IMPLEMENT_DYNCREATE等宏將初始化類的CRuntimeClass類型靜態成員變量的各個域,表3-1列出了在動態類信息、動態創建、序列化這三個不同層次下對該靜態成員變量的初始化情況:
表3-1 靜態成員變量class##class_name的初始化
CRuntimeClass成員變量
動態類信息
動態創建
序列化
m_lpszClassName
類名字符串
類名字符串
類名字符串
m_nObjectSize
類的大小(字節數)
類的大小(字節數)
類的大小(字節數)
m_wShema
0xFFFF
0xFFFF
1、2等,非0
m_pfnCreateObject
NULL
類的成員函數
CreateObject
類的成員函數
CreateObject
m_pBaseClass
基類的CRuntimeClass變量
基類的CRuntimeClass變量
基類的CRuntimeClass變量
m_pfnGetBaseClass
類的成員函數
_GetBaseClass
類的成員函數
_GetBaseClass
類的成員函數
_GetBaseClass
m_pNextClass
NULL
NULL
NULL
m_wSchema類型是UINT,定義了序列化中保存對象到文檔的程序的版本。如果不要求支持序列化特性,該域為0XFFFF,否則,不能為0。
Cobject類本身的靜態成員變量classCObject被初始化為:
{ "CObject", sizeof(CObject), 0xffff, NULL, &CObject::_GetBaseClass, NULL };
對初始化內容解釋如下:
類名字符串是“CObject”,類的大小是sizeof(CObject),不要求支持序列化,不支持動態創建。
成員函數CreateObject
回顧3.2節,動態創建對象是通過語句pRuntimeClass->CreateObject完成的,即調用了CRuntimeClass自己的成員函數,CreateObject函數又調用m_pfnCreateObject指向的函數來完成動態創建任務,如下所示:
CObject* CRuntimeClass::CreateObject()
{
if (m_pfnCreateObject == NULL) //判斷函數指針是否空
{
TRACE(_T("Error: Trying to create object which is not ")
_T("DECLARE_DYNCREATE or DECLARE_SERIAL: %hs. "),
m_lpszClassName);
return NULL;
}
//函數指針非空,繼續處理
CObject* pObject = NULL;
TRY
{
pObject = (*m_pfnCreateObject)(); //動態創建對象
}
END_TRY
return pObject;
}
成員函數IsDerivedFrom
該函數用來幫助運行時判定一個類是否派生於另一個類,被CObject的成員函數IsKindOf函數所調用。其實現描述如下:
如果定義了_AFXDLL則,成員函數IsDerivedFrom調用成員函數m_pfnGetBaseClass指向的函數來向上逐層得到基類的CRuntimeClass類型的靜態成員變量,直到某個基類的CRuntimeClass類型的靜態成員變量和參數指定的CRuntimeClass變量一致或者追尋到最上層為止。
如果沒有定義_AFXDLL,則使用成員變量m_pBaseClass基類的CRuntimeClass類型的靜態成員變量。
程序如下所示:
BOOL CRuntimeClass::IsDerivedFrom(
const CRuntimeClass* pBaseClass) const
{
ASSERT(this != NULL);
ASSERT(AfxIsValidAddress(this, sizeof(CRuntimeClass), FALSE));
ASSERT(pBaseClass != NULL);
ASSERT(AfxIsValidAddress(pBaseClass, sizeof(CRuntimeClass), FALSE));
// simple SI case
const CRuntimeClass* pClassThis = this;
while (pClassThis != NULL)//從本類開始向上逐個基類搜索
{
if (pClassThis == pBaseClass)//若是參數指定的類信息
return TRUE;
//類信息不符合,繼續向基類搜索
#ifdef _AFXDLL
pClassThis = (*pClassThis->m_pfnGetBaseClass)();
#else
pClassThis = pClassThis->m_pBaseClass;
#endif
}
return FALSE; // 搜索完畢,沒有匹配,返回FALSE。
}
由於CRuntimeClass類型的成員變量是靜態成員變量,所以如果兩個類的CruntimeClass成員變量相同,必定是同一個類。這就是IsDerivedFrom和IsKindOf的實現基礎。
RUNTIME_CLASS宏
RUNTIME_CLASS宏定義如下:
#define RUNTIME_CLASS(class_name) (&class_name::class##class_name)
為了方便地得到每個類(Cobject或其派生類)的CRuntimeClass類型的靜態成員變量,MFC定義了這個宏。它返回對類class_name的CRuntimeClass類型成員變量的引用,該成員變量的名稱是“class”加上class_name(類的名字)。例如:
RUNTIME_CLASS(CObject)得到對classCObject的引用;
RUNTIME_CLASS(CPerson)得到對class CPerson的引用。
動態類信息、動態創建的原理
MFC對Cobject動態類信息、動態創建的實現原理:
動態類信息、動態創建都建立在給類添加的CRuntimeClass類型的靜態成員變量基礎上,總結如下。
C++不支持動態創建,但是支持動態對象的創建。動態創建歸根到底是創建動態對象,因為從一個類名創建一個該類的實例最終是創建一個以該類為類型的動態對象。其中的關鍵是從一個類名可以得到創建其動態對象的代碼。
在一個類沒有任何實例之前,怎麼可以得到該類的創建動態對象的代碼?借助於C++的靜態成員數據技術可達到這個目的:
靜態成員數據在程序的入口(main或WinMain)之前初始化。因此,在一個靜態成員數據裡存放有關類型信息、動態創建函數等,需要的時候,得到這個成員數據就可以了。
不論一個類創建多少實例,靜態成員數據只有一份。所有的類的實例共享一個靜態成員數據,要判斷一個類是否是一個類的實例,只須確認它是否使用了該類的這個靜態數據。
從前兩節的討論知道,DECLARE_CREATE等宏定義了一個這樣的靜態成員變量:類型是CRuntimeClass,命名約定是“calss”加上類名;IMPLEMENT_CREATE等宏初始化該變量;RUNTIME_CLASS宏用來得到該成員變量。
動態類信息的原理在分析CRuntimeClass的成員函數IsDerivedFrom時已經作了解釋。
動態創建的過程和原理了,用圖表示其過程如下:
注:下面一個方框內的逐級縮進表示逐層調用關系。
序列化的機制
由上所述可知,一個類要支持實現序列化,使得它的對象可以保存到文檔中或者可以從文檔中讀入到內存中並生成對象,需要使用動態類信息,而且,需要覆蓋基類的Serialize虛擬函數來完成其對象的序列化。
僅僅有類的支持是不夠的,MFC還提供了一個歸檔類CArchive來支持簡單類型的數據和復雜對象的讀寫。
CArchive 在文件和內存對象之間充當一個代理者的角色。它負責按一定的順序和格式把內存對象寫到文件中,或者讀出來,可以被看作是一個二進制的流。它和文件類CFile的關系如圖3-2所示:
一個CArchive對象在要序列化的對象和存儲媒體(storage medium,可以是一個文件或者一個Socket)之間起了中介作用。它提供了系列方法來完成序列化,不僅能夠把int、float等簡單類型數據進行序列化,而且能夠把復雜的數據如string等進行序列化,更重要的是它能把復雜的對象(包括復合對象)進行序列化。這些方法就是重載的操作符>>和<<。對於簡單類型,它針對不同類型直接實現不同的讀寫操作;對於復雜的對象,其每一個支持序列化的類都重載了操作符>>,從前幾節可以清楚地看到這點:IMPLEMENT_SERIAL給所在類重載了操作符>>。至於<<操作,就不必每個序列化類都重載了。
復雜對象的“<<”操作,先搜索本模塊狀態的CRuntimeClass鏈表看是否有“<<”第二個參數指定的對象類的運行類信息(搜索過程涉及到模塊狀態,將在9.5.2節描述),如果有(無,則返回),則先使用這些信息動態的創建對象(這就是是序列化類必須提供動態類信息、支持動態創建的原因),然後對該對象調用Serilize函數從存儲媒體讀入對象內容。
復雜對象的“>>”操作先把對象類的運行類信息寫入存儲媒體,然後對該對象調用Serilize函數把對象內容寫入存儲媒體。
在創建CArchive對象時,必須有一個CFile對象,它代表了存儲媒介。通常,程序員不必做這個工作,打開或保存文檔時MFC將自動的創建CFile對象和CArchive對象並在適當的時候調用序列化類的Serialize函數。在後面討論打開(5.3.3.2節)或者關閉(6.1節)文檔時將會看到這樣的流程。
CArchive對象被創建時,需要指定它是用來讀還是用來寫,即指定序列化操作的方向。Serialize函數適用CArchive的函數IsStoring判定CArchive是用於讀出數據還是寫入數據。
在解釋實現序列化的方法時,曾經提到如果程序員直接調用Serilize函數完成序列化,而不借助CArchive的>>和<<操作,則可以不需要動態類信息和動態創建。從上文的論述可以看出,沒有CArchive的>>和<<操作,的確不需要動態類信息和動態創建特性。