说明:C++的多态是通过一张虚函数表(Virtual Table)来实现的,简称为V-Table。在这个表中,主要为一个类的虚函数的地址表,这张表解决了继承、覆写的问题,保证其真实反应实际的虚函数调用过程。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

下面介绍一下与这张虚函数表有关的几个问题:

1.普通成员函数不占存储空间,而所有虚函数入口地址存储在一张虚函数表中,由一个指针指向该虚函数表;

2.指向该虚函数表的指针位于类实例对象内存的最前面,占四个字节;

3.若子类覆写了父类的虚函数,则父类的虚函数被覆盖,即虚函数表中只存在子类的虚函数地址;否则,父类和子类的虚函数都存在于虚函数表中(当然,没有覆写父类的虚函数是毫无意义的),这就是多态形成的原因。

通过上面的介绍,我们对虚函数表有了大致的了解,下面通过一个实例来加深一下认识:

 #include <iostream>
using namespace std; class base
{
public:
virtual void f(){cout<<"base::f()"<<endl;}
virtual void g(){cout<<"base::g()"<<endl;}
virtual void h(){cout<<"base::h()"<<endl;}
private:
int a;
}; //定义一个函数指针,并别名为pfunc,用时不需再加*,
typedef void (*pfunc)(void); int main()
{
base b; //C++编译器使虚函数表的指针存在于对象实例中的最前面(四个字节)
cout<<"sizeof(base) = "<<sizeof(base)<<'\t'<<"sizeof(b) = "<<sizeof(b)<<endl<<'\n'; //分别打印对象b的起始地址和虚函数表中首个函数指针指向的地址
//对象实例最前面的四个字节为指向虚函数表的指针,取内容后才为虚函数表
cout<<"&b = "<<&b<<"\t\t"<<"&VTable = "<<(int **)*(int *)(&b)<<endl<<"\n\n"; pfunc pf;
//定义一个函数指针
void(*p)(void);
//还可以这样定义一个函数指针 //虚函数表里面存放的是指向各个虚函数的指针,取内容后才是各个相应的虚函数
pf = (pfunc)*((int **)*(int *)(&b)+0);
pf();
pf = (pfunc)*((int **)*(int *)(&b)+1);
pf();
pf = (void(*)())*((int **)*(int *)(&b)+2);
pf(); cout<<"\n\n"; p = (pfunc)*((int **)*(int *)(&b)+0);
p();
p = (void(*)())*((int **)*(int *)(&b)+1);
p();
p = (void(*)())*((int **)*(int *)(&b)+2);
p(); return 0;
}

程序运行结果:

通过以上示例,我们把类实例对象b取址,然后将&b强转成int*型,然后对其取内容,取得虚函数表的地址,然后再对其取内容,就得到了第一个虚函数的地址了,然后再将其通过(int**)强转成步长为4的指针,通过加1来得到虚函数表中不同的虚函数的地址,最终强转成为函数指针,再通过该函数指针访问相应的虚函数.

5.下面我们将通过几个例子来解释一下虚函数表的存在形式,在这部分,主要弄清楚虚函数表是怎么一回事,至于程序运行结果,读者自行实验。

a.在父子类中,若子类没有对父类的虚函数进行覆写(当然,前面提到过,没有覆写父类的虚函数是毫无意义的。之所以要讲述没有覆写的情况,主要目的是为了给一个对比,在比较之下,我们可以更加清楚地知道其内部的具体实现),如下代码,

 #include<iostream>
using namespace std;
class base
{
public:
virtual void func(){};
virtual void foo(){};
};
class derive:public base
{
public:
virtual void func1(){};
virtual void foo1(){};
};
int main()
{
derive d;
return 0;
}

则其虚函数表如下所示:

注意:

1.上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。

2.虚函数是按照其声明顺序放于表中的。

3.父类的虚函数在子类的虚函数前面。

b.在父子类中,若子类对父类的虚函数进行了覆写(为了对比,假设只覆写父类一个虚函数),如下代码,

 #include<iostream>
using namespace std;
class base
{
public:
virtual void func(){};
virtual void foo(){};
virtual ~base(){}
};
class derive:public base
{
public:
virtual void func(){cout<<"___"<<endl;};
virtual void foo1(){};
virtual ~derive(){}
};
int main()
{
base *p = new derive;
p->func();
delete p;
return 0;
}

则其虚函数表如下所示:

由此,可得覆写的子类func()放在了虚函数表中原来父类func()的位置,没有覆写的虚函数依旧原样存放。这样,在上述代码中,由于p所指的func()的位置已经被derive::func()的函数地址所取代,因此在发生实际调用的时候,调用的是子类的func(),这就实现了多态。

最新文章

  1. html5学习笔记一
  2. 369. Plus One Linked List
  3. epoll的LT模式缺点
  4. JDBC之——一个单线程JDBC基类和一些注意事项
  5. unity3d插件Daikon Forge GUI 中文教程7-高级控件slider的使用
  6. python4delphi 安装
  7. linux查看服务器型号
  8. 后缀自动机(SAM):SPOJ Longest Common Substring II
  9. c语言,指针与数组--指针与二维数组2
  10. ThreadPoolExecutor的运转机制
  11. bugku web 变量1
  12. 代理模式-JDK Proxy(Java实现)
  13. 本地搭建Apache Tomcat服务器
  14. Flex 项目属性:flex 布局示例
  15. EOJ2018.10 月赛(B 数学+思维题)
  16. 以结算价交易TAS和以市价交易TAM
  17. java-学习6
  18. UVA 1508 - Equipment 状态压缩 枚举子集 dfs
  19. android绝对布局
  20. Codeforces 156 A——Message——————【思维题】

热门文章

  1. 记录 mysql sql limit 0,100问题
  2. MongoDB 开机自启动
  3. YTU 2705:用重载求距离
  4. [Swift通天遁地]五、高级扩展-(4)快速生成Invert、Mix、Tint、Shade颜色及调整饱和度阶
  5. HttpClient Get请求实例
  6. IOS上微信在输入框弹出键盘后,页面不恢复,下方有留白,有弹窗弹出时页面内容感应区域错位
  7. redis+php实现秒杀
  8. display:none 和 hidden 区别
  9. 【BZOJ1939】[Croatian2010] Zuma(动态规划)
  10. 简单 android popupwindow 实现