晴天霹雳
我们渐渐接触问题的核心。上述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 <iostream.h> #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 真正变成了一个完完全全