虚拟函数正是为了对「如果你以一个基础类别之指针指向一个衍生类别之对象,那么透 过该指针你就只能够调用基础类别所定义之成员函数」这条规则反其道而行的设计。
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 与虚函数
#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 }
结果:
#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 相同。