说明: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(),这就实现了多态。

(C/C++学习)4.C++类中的虚函数表Virtual Table的更多相关文章

  1. C++中的虚函数表是什么时期建立的?

    虚函数表是在什么时期建立的? 最近参加阿里巴巴公司的内推,面试官问了“虚函数表是在什么时期建立的?”.因为以前对虚函数表的理解不够多,所以就根据程序构建(Build)的四个过程(预编译.编译.汇编和链 ...

  2. C++中的虚函数表

    (感谢http://blog.csdn.net/haoel/article/details/1948051/) C++中的虚函数的作用主要是实现了多态的机制. 多态,简而言之就是用父类型别的指针指向其 ...

  3. C++ 中的虚函数表及虚函数执行原理

    为了实现虚函数,C++ 使用了虚函数表来达到延迟绑定的目的.虚函数表在动态/延迟绑定行为中用于查询调用的函数. 尽管要描述清楚虚函数表的机制会多费点口舌,但其实其本身还是比较简单的. 首先,每个包含虚 ...

  4. 在基类中的析构函数声明为virtual

    #include <iostream> using namespace std; class Father { public: ~Father() { cout << &quo ...

  5. c++ 继承类强制转换时的虚函数表工作原理

    本文通过简单例子说明子类之间发生强制转换时虚函数如何调用,旨在对c++继承中的虚函数表的作用机制有更深入的理解. #include<iostream> using namespace st ...

  6. 深入理解类成员函数的调用规则(理解成员函数的内存为什么不会反映在sizeof运算符上、类的静态绑定与动态绑定、虚函数表)

    本文转载自:http://blog.51cto.com/9291927/2148695 总结: 一.成员函数的内存为什么不会反映在sizeof运算符上?             成员函数可以被看作是类 ...

  7. C++ 类中有虚函数(虚函数表)时 内存分布

    虚函数表 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的.简称为V-Table.在这个表中,主是要一个类的虚函数的地址表 ...

  8. C++ 类的存储方式以及虚函数表

    一.C++成员函数在内存中的存储方式 用类去定义对象时,系统会为每一个对象分配存储空间.如果一个类包括了数据和函数,要分别为数据和函数的代码分配存储空间.按理说,如果用同一个类定义了10个对象,那么就 ...

  9. C++虚函数表解析(图文并茂,非常清楚)( 任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法)good

    C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有“多种形态”,这是一种泛型技术 ...

随机推荐

  1. Swift 进阶

    iOS开发系列--Swift进阶 2015-09-21 00:01 by KenshinCui, 3072 阅读, 12 评论, 收藏, 编辑 概述 上一篇文章<iOS开发系列--Swift语言 ...

  2. springmvc20170322

    <?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" ...

  3. There was a conflict between

    解读,首先搜索到第一个5>的开头的那一行,确认是在编译哪一个项目. 那么后面的冲突,就是在和这个项目冲突. There was a conflict between "log4net, ...

  4. P2657 [SCOI2009]windy数 数位dp

    数位dp之前完全没接触过,所以NOIP之前搞一下.数位dp就是一种dp,emm……用来求解区间[L,R]内满足某个性质的数的个数,且这个性质与数的大小无关. 在这道题中,dp[i][j]代表考虑了i位 ...

  5. codeforces round #427 div2

    A:读懂题,乘一下判断大小就行了 #include<bits/stdc++.h> using namespace std; int main() { int s, v1, v2, t1, ...

  6. poj Code(组合数)

    Code Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 9918   Accepted: 4749 Description ...

  7. akka设计模式系列-Aggregate模式

    所谓的Aggregate模式,其实就是聚合模式,跟masterWorker模式有点类似,但其出发点不同.masterWorker模式是指master向worker发送命令,worker完成某种业务逻辑 ...

  8. 简单认识http协议

    1.什么是TCP/IP  如果要了解一个人,可以从他归属的集体聊起来.我们的HTTP协议就属于TCP/IP协议家族中的一员,了解HTTP协议再整个网络流程中的地位,也能更加充分的理解HTTP协议. 要 ...

  9. IIS Express 错误提示汇总

    在做WEB Service开发中,要经常用到IIS Express服务器,有些自己遇到的问题做一个汇总以待后续查找. 错误类型: 问题来源:直接在项目上选择调试运行. 解决方案: 1.直接找到那个文件 ...

  10. JVM中线程状态转换图

    JVM中线程的状态转换图 线程在一定条件下,状态会发生变化.线程一共有以下几种状态: 1.新建状态(New):新创建了一个线程对象. 2.就绪状态(Runnable):线程对象创建后,其他线程调用了该 ...