MFC中的C++(四)

来源:互联网 发布:ibm 50量子计算机知乎 编辑:程序博客网 时间:2024/06/06 19:50

多态(Polymorphism)

你看,我们以相同的指令却唤起了不同的函数,这种性质称为Polymorphism,意思是 " the ability to assume many forms"(多态)。编译器无法在编译时期判断             pEmp->computePay 到底是调用哪一个函数,必须在执行时期才能评估之,这称为后期绑定late binding 或动态绑定dynamic binding。至于C 函数或C++ 的non-virtual 函数,在编译时期就转换为一个固定地址的调用了,这称为前期绑定early binding 或静态绑定static binding。
Polymorphism 的目的,就是要让处理”基础类别之对象“的程序代码,能够完全透通地继续适当处理“衍生类别之对象”。
可以说,虚拟函数是了解多态( Polymorphism)以及动态绑定的关键。同时,它也是了解如何使用MFC 的关键。
让我再次提示你,当你设计一套类别,你并不知道使用者会衍生什么新的子类别出来。如果动物世界中出现了新品种名曰雅虎,类别使用者势必在CAnimal 之下衍生一个CYahoo。饶是如此,身为基础类别设计者的你,可以利用虚拟函数的特性,将所有动物必定会有的行为(例如哮叫roar),规划为虚拟函数,并且规划一些一般化动作(例如“让每一种动物发出一声哮叫”)。那么,虽然,你在设计基础类别以及这个一般化动作时,无法掌握使用者自行衍生的子类别,但只要他改写了 roar 这个虚拟函数,你的一般化对象操作动作自然就可以调用到该函数。

再次回到前述的Shape 例子。我们说CShape 是抽象的,所以它根本不该有display 这个动作。但为了在各具象衍生类别中绘图,我们又不得不在基础类别CShape 加上display 虚拟函数。你可以定义它什么也不做(空函数):
class CShape
{

public:
virtual void display() { }
} ;
或只是给个消息:
class CShape
{
public:
virtual void display() { cout << "Shape \n"; }
} ;
这两种作法都不高明,因为这个函数根本就不应该被调用( CShape 是抽象的),我们根本就不应该定义它。不定义但又必须保留一块空间( spaceholder)给它,于是C++ 提供了所谓的纯虚拟函数:
class CShape
{

public:
virtual void display() = 0; // 注意"= 0"
} ;
纯虚拟函数不需定义其实际动作,它的存在只是为了在衍生类别中被重新定义,只是为了提供一个多态接口。只要是拥有纯虚拟函数的类别,就是一种抽象类别,它是不能够被具象化( instantiate)的,也就是说,你不能根据它产生一个对象(你怎能说一种形状为'Shape' 的物体呢)。如果硬要强渡关山,会换来这样的编译消息:
error : illegal attempt to instantiate abstract class.
关于抽象类别,我还有一点补充。 CCircle 继承了 CShape 之后,如果没有改写CShape 中的纯虚拟函数,那么CCircle 本身也就成为一个拥有纯虚拟函数的类别,于是它也是一个抽象类别。
是对虚拟函数做结论的时候了:

  • 如果你期望衍生类别重新定义一个成员函数,那么你应该在基础类别中把此函数设为virtual。
  • 以单一指令唤起不同函数,这种性质称为Polymorphism,意思是"the ability to assume many forms",也就是多态。
  • 虚拟函数是C++ 语言的Polymorphism 性质以及动态绑定的关键。
  • 既然抽象类别中的虚拟函数不打算被调用,我们就不应该定义它,应该把它设为纯虚拟函数(在函数声明之后加上"=0" 即可)。
  • 我们可以说,拥有纯虚拟函数者为抽象类别( abstract Class),以别于所谓的具象类别( concrete class)。
  • 抽象类别不能产生出对象实体,但是我们可以拥有指向抽象类别之指针,以便于操作抽象类别的各个衍生类别。
  • 虚拟函数衍生下去仍为虚拟函数,而且可以省略virtual 关键词。
类别与对象大解剖

你一定很想知道虚拟函数是怎么做出来的,对不对?
如果能够了解C++ 编译器对于虚拟函数的实现方式,我们就能够知道为什么虚拟函数可以做到动态绑定。
为了达到动态绑定(后期绑定)的目的, C++ 编译器透过某个表格,在执行时期”间接“调用实际上欲绑定的函数(注意“间接”这个字眼)。这样的表格称为虚拟函数表(常被称为vtable)。每一个”内含虚拟函数的类别“,编译器都会为它做出一个虚拟函数表,表中的每一笔元素都指向一个虚拟函数的地址。此外,编译器当然也会为类别加上一项成员变量,是一个指向该虚拟函数表的指针(常被称为vptr)。举个例:
class Class1 {
public :
data1;
data2;
memfunc() ;
virtual vfunc1() ;
virtual vfunc2() ;
virtual vfunc3() ;
} ;
Class1 对象实体在内存中占据这样的空间:

每一个由此类别衍生出来的对象,都有这么一个vptr。当我们透过这个对象调用虚拟函数,事实上是透过vptr 找到虚拟函数表,再找出虚拟函数的真正地址。
奥妙在于这个虚拟函数表以及这种间接调用方式。虚拟函数表的内容是依据类别中的虚拟函数声明次序,一一填入函数指针。衍生类别会继承基础类别的虚拟函数表(以及所有其它可以继承的成员),当我们在衍生类别中改写虚拟函数时,虚拟函数表就受了影响:表中元素所指的函数地址将不再是基础类别的函数地址,而是衍生类别的函数地址。看看这个例子:

class Class2 : public Class1 {
public :
data3;
memfunc();
virtual vfunc2();
};

于是,一个“指向Class1 所生对象”的指针,所调用的vfunc2 就是Class1::vfunc2,而一个“指向Class2 所生对象”的指针,所调用的vfunc2 就是Class2::vfunc2。动态绑定机制,在执行时期,根据虚拟函数表,做出了正确的选择。
我们解开了第二道神秘。

口说无凭,何不看点实际。观其地址,物焉�C哉,下面是一个测试程序:
执行结果与分析如下:

a、 b、 c 对象的內容图示如下:

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

以程序表现如下:
由于CMyDoc 自己没有func 函数,而它继承了 CDocument 的所有成员,所以main 之中的四个调用动作毫无问题都是调用CDocument::func。但, CDocument::func 中所调用的Serialize 是哪一个类别的成员函数呢?如果它是一般( non-virtual)函数,毫无问题应该是CDocument::Serialize。但因为这是个虚拟函数,情况便有不同。以下是执行结果:
#1 testing
CDocument::func()
CMyDoc::Serialize()
#2 testing
CDocument::func()
CMyDoc::Serialize()
#3 testing
CDocument::func()
CMyDoc::Serialize()
#4 testing
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 真正变成了一个完完全全的CDocumen对象。所以,本例的第四项测试结果也就水落石出了。注意,“upcasting”并不是惯用的操作,应该小心,甚至避免。

0 0
原创粉丝点击