虚函数与多态的秘密

来源:互联网 发布:js 楼层滑动特效 编辑:程序博客网 时间:2024/05/02 00:07

虚拟函数正是为了对「如果你以一个基础类别之指针指向一个衍生类别之对象,那么透
过该指针你就只能够调用基础类别所定义之成员函数」这条规则反其道而行的设计。

CEmployee* pEmp;
CWage aWager("曾");
CSales aSales("侯");
CManager aManager("陳");
pEmp = &aWager;
cout << pEmp->computePay(); // 调 用 的 是 CWage::computePay
pEmp = &aSales;
cout << pEmp->computePay(); // 调 用 的 是 CSales::computePay
pEmp = &aManager;
cout << pEmp->computePay(); // 调 用 的 是CManager::computePay

不定义虚函数 就都调用CEmployee的computerPay()

 

以相同的指令却唤起了不同的函数,这种性质称为Polymorphism,意思是"the
ability to assume many forms"(多态)。编译器无法在编译时期判断pEmp->computePay
到底是调用哪一个函数,必须在执行时期才能评估之,这称为后期绑定late binding 或动
态绑定dynamic binding。至于C 函数或C++ 的non-virtual 函数,在编译时期就转换为
一个固定地址的调用了,这称为前期绑定early binding 或静态绑定static binding

 

Polymorphism 的目的,就是要让处理「基础类别之对象」的程序代码,能够完全透通地继
续适当处理「衍生类别之对象」。

 

因为这个函数根本就不应该被调用(CShape 是抽象的),我们根
本就不应该定义它。不定义但又必须保留一块空间(spaceholder)给它,于是C++ 提供
了所谓的纯虚拟函数:
class CShape
{
public:
virtual void display() = 0; // 注意"= 0"
};
纯虚拟函数不需定义其实际动作,它的存在只是为了在衍生类别中被重新定义,只是为
了提供一个多态接口。只要是拥有纯虚拟函数的类别,就是一种抽象类别,它是不能够
被具象化(instantiate)的,也就是说,你不能根据它产生一个对象

 

是对虚拟函数做结论的时候了:
■  如果你期望衍生类别重新定义一个成员函数,那么你应该在基础类别中把此函
数设为virtual。
■  以单一指令唤起不同函数,这种性质称为Polymorphism,意思是"the ability to
assume many forms",也就是多态。
■  虚拟函数是C++ 语言的Polymorphism 性质以及动态绑定的关键。

■ 既然抽象类别中的虚拟函数不打算被调用,我们就不应该定义它,应该把它设
为纯虚拟函数(在函数声明之后加上"=0" 即可)。
■ 我们可以说,拥有纯虚拟函数者为抽象类别(abstract Class),以别于所谓的
具象类别(concrete class)。
■ 抽象类别不能产生出对象实体,但是我们可以拥有指向抽象类别之指针,以便
于操作抽象类别的各个衍生类别。
■ 虚拟函数衍生下去仍为虚拟函数,而且可以省略virtual 关键词。

 

C++ 编译器对于虚函数的实现方式:

 

为了达到动态绑定(后期绑定)的目的,C++ 编译器透过某个表格,在执行时期「间接」
调用实际上欲绑定的函数(注意「间接」这个字眼)。这样的表格称为虚函数表(常
被称为vtable)。每一个「内含虚函数的类」,编译器都会为它做出一个虚函数表,
表中的每一个元素都指向一个虚函数的地址(即函数指针)。此外,编译器当然也会为类加上一项
成员变量,是一个指向该虚函数表的指针(常被称为vptr)。

 

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

 

Object slicing 与虚函数

 

结果:

#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 函数。

你知道,衍生对象通常都比基础对象大(我是指内存空间),因为衍生对象不但继承其基础类别的成员,又有自己的成员。第四项测试发生了upcasting(向上强制转型): (CDocument)mydoc,将会造成对象的内容被切割(object slicing)

由于((CDocument)mydoc).func() 是个传值而非传址动作,编译器以所谓
的拷贝构造式(copy constructor)把CDocument 对象内容复制了一份,使得mydoc 的
vtable 内容与CDocument 对象的vtable 相同。

 

原创粉丝点击