深入浅出MFC 笔记一


晴天霹雳

我们渐渐接触问题的核心。上述C++ 性质使真实生活经验的确在计算机语言中仿真了出 来,但是万里无云的日子里却出现了一个晴天霹雳:

如果你以一个「基础类别之指针」 指向一个「衍生类别之对象」,那么经由此指针,你就只能够调用基础类别(而不是衍 生类别)所定义的函数。

因此:

CSales aSales("侯俊杰")
CSales* pSales; CWage* pWager; pSales = &aSales; pWager = &aSales;

// 以「基础类别之指针」指向「衍生类别之对象」

pWager->setSales(800.0); // 错误(编译器会检测出来),

// 因为CWage 并没有定义setSales 函数。

pSales->setSales(800.0); // 正确,

调用CSales::setSales 函数。 虽然pSales 和pWager 指向同一个对象,但却因指针的原始类型而使两者之间有了差异。

延续此例,我们看另一种情况:

pWager->computePay(); // 调用

CWage::computePay()

pSales->computePay(); // 调用CSales::computePay()

虽然pSales 和pWager 实际上都指向CSales 对象,但是两者调用的computePay 却不相同。到底调用到哪个函数,必须视指针的原始类型而定,与指针实际所指之对象无关。

三个结论

我们得到了三个结论:

1. 如果你以一个「基础类别之指针」指向「衍生类别之对象」,那么经由该指针 你只能够调用基础类别所定义的函数。

2. 如果你以一个「衍生类别之指针」指向一个「基础类别之对象」,你必须先做 明显的转型动作(explicit cast)。这种作法很危险,不符合真实生活经验,在 程序设计上也会带给程序员困惑。

3. 如果基础类别和衍生类别都定义了「相同名称之成员函数」,那么透过对象指针调用成员函数时,到底调用到哪一个函数,必须视该指针的原始型别而定, 而不是视指针实际所指之对象的型别而定。这与第1点其实意义相通。

CBase* pBase;

CDerived* pDeri;

不论你把这两个指针指向何方,由于它们的原始类型, 使它们在调用同名的CommFunc() 时有着无可改变的宿命:

? pBase->CommFunc() 永远是指 CBase::CommFunc

? pDeri->CommFunc() 永远是指 CDerived::CommFunc


Object slicing 与虚拟函数 我要在这里说明虚拟函数另一个极重要的行为模式。假设有三个类别,阶层关系如下:

CObject        virtual void Serialize()

CDocument virtual void Serialize()

CMyDoc   virtual void Serialize()

以程序表现如下:
#0001 #include 
#0002
#0003 class CObject
#0004 {
#0005 public:
#0006 virtual void Serialize() { cout << "CObject::Serialize() \n\n"; }
#0007 };
#0008
#0009 class CDocument : public CObject
#0010 {
#0011 public:
#0012 int m_data1;
#0013 void func() { cout << "CDocument::func()" << endl;
#0014 Serialize();
#0015 }
#0016
#0017 virtual void Serialize() { cout << "CDocument::Serialize() \n\n"; }
#0018 };
#0019
#0020 class CMyDoc : public CDocument
#0021 {
#0022 public:
#0023 int m_data2;
#0024 virtual void Serialize() { cout << "CMyDoc::Serialize() \n\n"; }
#0025 };
#0026 //---------------------------------------------------------------
#0027 void main()
#0028 {
#0029 CMyDoc mydoc;
#0030 CMyDoc* pmydoc = new CMyDoc;
#0031
#0032 cout << "#1 testing" << endl;
#0033 mydoc.func();
#0034
#0035 cout << "#2 testing" << endl;
#0036 ((CDocument*)(&mydoc))->func();
#0037
#0038 cout << "#3 testing" << endl;
#0039 pmydoc->func();
#0040
#0041 cout << "#4 testing" << endl;
#0042 ((CDocument)mydoc).func();
#0043 }

由于CMyDoc 自己没有func 函数,而它继承了CDocument 的所有成员,所以main 之中 的四个调用动作毫无问题都是调用CDocument::func。

但,CDocument::func 中所调用的 Serialize 是哪一个类别的成员函数呢?如果它是一般(non-virtual)函数,毫无问题应该 是CDocument::Serialize。

但因为这是个虚拟函数,情况便有不同。

以下是执行结果:

#1 testing  //mydoc.func();

CDocument::func()

CMyDoc::Serialize()

#2 testing //((CDocument*)(&mydoc))->func();

CDocument::func()

CMyDoc::Serialize()

#3 testing //pmydoc->func();

CDocument::func()

CMyDoc::Serialize()

#4 testing    //((CDocument)mydoc).func();

CDocument::func()

CDocument::Serialize() <-- 注意

前三个测试都符合我们对虚拟函数的期望:既然衍生类别已经改写了虚拟函数Serialize, 那么理当调用衍生类别之Serialize 函数。

这种行为模式非常频繁地出现在application framework 身上。后续当我追踪MFC 源代码时,遇此情况会再次提醒你。

第四项测试结果则有点出乎意料之外。你知道,衍生对象通常都比基础对象大(我是指 内存空间),因为衍生对象不但继承其基础类别的成员,又有自己的成员。

那么所谓 的upcasting(向上强制转型): (CDocument)mydoc,将会造成对象的内容被切割(object slicing):

当我们调用: ((CDocument)mydoc).func(); mydoc 已经是一个被切割得剩下半条命的对象,而func 内部调用虚拟函数Serialize;后 者将使用的「mydoc 的虚拟函数指针」虽然存在,它的值是什么呢?你是不是隐隐觉得 有什么大灾难要发生? 幸运的是,由于((CDocument)mydoc).func() 是个传值而非传址动作,编译器以所谓 的拷贝构造式(copy constructor)把CDocument 对象内容复制了一份,使得mydoc 的 vtable 内容与CDocument 对象的vtable 相同。本例虽没有明显做出一个拷贝构造式, 编译器会自动为你合成一个。 说这么多,总结就是,经过所谓的data slicing,本例的mydoc 真正变成了一个完完全全