学习笔记之深入浅出MFC 第8章 C++重要特性---类与对象大解剖(虚拟函数的实现方式)

在上面几节讲到虚拟函数和多态的时候,我们只是提到虚拟函数就是在函数前面加上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();

};

如上面图中所示,Class2继承了Class1的虚拟函数vfunc1,vfunc2和vfunc3,其中只有vfunc2在子函数中改写了,所以vtable中指针指向中,只有vfunc2指针指向了子类Class2的地址,其他两个没有在子类中改变的虚拟函数仍然指向基类Class1。

动态绑定机制,在执行期,根据虚拟函数表,做出了正确的选择。至此,我们就解开了虚拟函数实现方式的谜团。


附加:

虚拟函数还有一个极重要的行为模式。假设有三个类,层次关系如下:

程序表示如下:

#include <iostream.h>

class  CObject      //基类

{

public:

virtual  void  Serialize()   {  cout  <<  "CObject::Serialize()   \n\n";   }

};

class CDocument  :   public  CObject

{

public:

int   m_data1;

void   func()    {  cout  <<  "CDocument  :  :  func()"   << endl;

Serialize();

}

virtual    void   Serialize()    {cout  <<  "CDocument : :Serialize()   \n \n";   }

};


class  CMyDoc   :   public  CDocument

{

public:

int  m_data2;

virtual   void   Serialize()    {  cout  <<  "CMyDoc  : : Serialize()  \n \n" ;  }

};

//-----------------------------------------------------------------------------------------

void  main()

{

CMyDoc   mydoc;

CMyDoc*  pmydoc  = new  CMyDoc;


cout  <<  "#1  testing"  <<endl;

mydoc.func();


cout << "#2 testing" << endl;

((CDocument*)(&mydoc))  ->func();

cout<<"#3 testing"<< endl;

pmydoc ->func();


cout<<"#4 testing"  <<endl;

((CDocument)mydoc).func();

}


执行结果如下:

由于CMyDoc自己没有func函数,而它继承了CDocument的所有成员,所以main之中的四个调用操作毫无疑问都是调用CDocument::func。但是,CDocument::func中所调用的Serialize是那一类的成员函数呢?如果它是一般的(non-virtual)函数,毫无疑问是CDocument::Serialize。但是因为这是个虚拟函数,情况就有不同了。

前三个测试都符合我们对虚拟函数的期望:既然派生类已经改写了虚拟函数Serialize,那么理所当然调用派生类之Serialize函数。第四项测试结果就有些不同了。派生类对象通常都比基类对象内存空间大,因为派生对象不但继承其基类的成员,又有自己的成员。那么向上强制转型将会造成对象的内容被切割。

当我们调用((CDocument)mydoc).func();  mydoc已经是一个被切割的剩下半条命的对象,而func内部调用虚拟函数Serialize;后者将使用的“mydoc的虚拟函数指针”虽然存在,它的值是什么呢?

由于((CDocument)mydoc).func();  是传值而非传址操作,编译器以所谓的拷贝函数把CDocument对象的内容复制了一份,使得mydoc的vtable内容与CDocument对象的vtable相同。本例虽没有明显做出一个拷贝构造函数,但编译器会自动为你合成一个。

总结来说,经过所谓的data slicing,本例的mydoc真正变成了一个完完全全的CDocument对象,所以本例的第四项测试结果也就水落石出了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值