C++中的虚函数(Virtual Function)是用来实现动态多态性的,指的是当基类指针指向其派生类实例时,可以用基类指针调用派生类中的成员函数。如果基类指针指向不同的派生类,则它调用同一个函数就可以实现不同的逻辑,这种机制可以让基类指针有“多种形态”,它的实现依赖于虚函数表。虚函数表(Virtual Table)是指在每个包含虚函数的类中都存在着一个函数地址的数组。本文将详细介绍虚函数表的实现及其内存布局。

1. 虚函数表概述

首先我们要知道虚函数表的地址总是存在于对象实例中最前面的位置,其后依次是对象实例的成员。下图中vtptr就是虚函数表的地址,可看出虚函数表中的每个成员都对应类中的一个虚函数的地址。据图所述,我们可以使用对象实例的地址来得到虚函数表的地址,进而获得具体的虚函数的地址,然后进行调用。

假如有如下定义 Base b; 那么虚函数表的地址vtptr的值就是:(int*)*(int*)&b,第一个虚函数vfunc1的地址就是:*(int*)*(int*)&b,vfunc2的地址是:*( (int*)*(int*)&b + 1 ),详见本节后文所附代码。

下文为验证代码,其中Base类包含3个虚函数 vfunc1~vfunc3和两个数据成员m_iMem1, m_iMem2,该类与上图中的保持一致。在main中,详细描述了怎么获取虚表的地址,怎么获取成员变量,怎么通过虚表地址获取虚函数的地址

class Base
{
public:
Base(int mem1 = 1, int mem2 = 2) : m_iMem1(mem1), m_iMem2(mem2){ ; } virtual void vfunc1() { std::cout << "In vfunc1()" << std::endl; }
virtual void vfunc2() { std::cout << "In vfunc2()" << std::endl; }
virtual void vfunc3() { std::cout << "In vfunc3()" << std::endl; } private:
int m_iMem1;
int m_iMem2;
}; int _tmain(int argc, _TCHAR* argv[])
{
Base b; // 对象b的地址
int *bAddress = (int *)&b; // 对象b的vtptr的值
int *vtptr = (int *)*(bAddress + 0);
printf("vtptr: 0x%08x\n", vtptr); // 对象b的第一个虚函数的地址
int *pFunc1 = (int *)*(vtptr + 0);
int *pFunc2 = (int *)*(vtptr + 1);
int *pFunc3 = (int *)*(vtptr + 2);
printf("\t vfunc1addr: 0x%08x \n"
"\t vfunc2addr: 0x%08x \n"
"\t vfunc3addr: 0x%08x \n",
pFunc1,
pFunc2,
pFunc3); // 对象b的两个成员变量的值(用这种方式可轻松突破private不能访问的限制)
int mem1 = (int)*(bAddress + 1);
int mem2 = (int)*(bAddress + 2);
printf("m_iMem1: %d \nm_iMem2: %d \n\n",mem1, mem2); // 调用虚函数
(FUNC(pFunc1))();
(FUNC(pFunc2))();
(FUNC(pFunc3))();
return 0;
}

程序运行结果如下面两幅图所示,其中左边部分是程序运行结果,右边部分为调试窗口中显示的类中各成员的值,可以发现两者结果一致。同时在运行结果窗口中可见直接使用地址调用虚函数的方法也是正确的,这就验证了我们本节开始部分的阐述。

2. 单继承下的虚函数表

2.1 派生类未覆盖基类虚函数

下面我们来看下派生类没有覆盖基类虚函数的情况,其中Base类延用上一节的定义。从图中可看出虚函数表中依照声明顺序先放基类的虚函数地址,再放派生类的虚函数地址。

其对应的代码如下所示:

class Derived : public Base
{
public:
Devired(int mem = 3) : m_iDMem1(mem){ ; } virtual void vdfunc1() { std::cout << "In Devired vfunc3()" << std::endl; } void dfunc1() { std::cout << "In Devired dfunc1" << std::endl; } private:
int m_iDMem1;
}; int _tmain(int argc, _TCHAR* argv[])
{
Derived d;
int *dAddress = (int*)&d; /* 1. 获取对象的内存布局信息 */
// 虚表地址
int *vtptr = (int*)*(dAddress + 0); // 数据成员的地址
int mem1 = (int)*(dAddress + 1);
int mem2 = (int)*(dAddress + 2);
int dmem1 = (int)*(dAddress + 3); /* 2. 输出对象的内存布局信息 */
int *pFunc1 = (int *)*(vtptr + 0);
int *pFunc2 = (int *)*(vtptr + 1);
int *pFunc3 = (int *)*(vtptr + 2);
int *pdFunc1 = (int *)*(vtptr + 3); (FUNC(pFunc1))();
(FUNC(pFunc2))();
(FUNC(pFunc3))();
(FUNC(pdFunc1))(); printf("\t vfunc1addr: 0x%08x \n"
"\t vfunc2addr: 0x%08x \n"
"\t vfunc3addr: 0x%08x \n"
"\t vdfunc1addr: 0x%08x \n\n",
pFunc1,
pFunc2,
pFunc3,
pdFunc1
); printf("m_iMem1: %d, m_iMem2: %d, m_iDMem3: %d \n", mem1, mem2, dmem1);
return 0;
}

其输出结果如下图所示,可见与本节开始介绍的结论是一致的。

2.2 派生类覆盖基类虚函数

我们再来看一下派生类覆盖了基类的虚函数的情形,可见:1. 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置  2. 派生类没有覆盖的虚函数延用基类的

代码如下所示,注意这里只给出了类的定义,main函数的测试代码与上节一样:

class Devired : public Base
{
public:
// 覆盖基类的虚函数
virtual void vfunc2() { std::cout << "In Devired vfunc2()" << std::endl; } public:
Devired(int mem = 3) : m_iDMem1(mem){ ; } virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }
void dfunc1() { std::cout << "In Devired dfunc1" << std::endl; } private:
int m_iDMem1;
};

运行结果如下所示:

3. 多继承下的虚函数表

3.1 无虚函数覆盖

如果是多重继承的话,问题就变得稍微复杂一丢丢,主要有几点:1. 有几个基类就有几个虚函数表   2. 派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后,见下图所示:

Base类延用本文之前的定义,其余部分代码如下所示:

class Base2
{
public:
Base2(int mem = 3) : m_iBase2Mem(mem){ ; }
virtual void vBase2func1() { std::cout << "In Base2 vfunc1()" << std::endl; }
virtual void vBase2func2() { std::cout << "In Base2 vfunc2()" << std::endl; } private:
int m_iBase2Mem;
}; class Base3
{
public:
Base3(int mem = 4) : m_iBase3Mem(mem) { ; }
virtual void vBase3func1() { std::cout << "In Base3 vfunc1()" << std::endl; }
virtual void vBase3func2() { std::cout << "In Base3 vfunc2()" << std::endl; } private:
int m_iBase3Mem;
}; class Devired: public Base, public Base2, public Base3
{
public:
Devired(int mem = 7) : m_iMem1(mem) { ; }
virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; } private:
int m_iMem1;
}; int _tmain(int argc, _TCHAR* argv[])
{
// Test_3
Devired d;
int *dAddress = (int*)&d; /* 1. 获取对象的内存布局信息 */
// 虚表地址一
int *vtptr1 = (int*)*(dAddress + 0);
int basemem1 = (int)*(dAddress + 1);
int basemem2 = (int)*(dAddress + 2); int *vtpttr2 = (int*)*(dAddress + 3);
int base2mem = (int)*(dAddress + 4); int *vtptr3 = (int*)*(dAddress + 5);
int base3mem = (int)*(dAddress + 6); /* 2. 输出对象的内存布局信息 */
int *pBaseFunc1 = (int *)*(vtptr1 + 0);
int *pBaseFunc2 = (int *)*(vtptr1 + 1);
int *pBaseFunc3 = (int *)*(vtptr1 + 2);
int *pBaseFunc4 = (int *)*(vtptr1 + 3); (FUNC(pBaseFunc1))();
(FUNC(pBaseFunc2))();
(FUNC(pBaseFunc3))();
(FUNC(pBaseFunc4))();
// .... 后面省略若干输出内容,可自行补充
return 0;
}

调试输出如下图,这里的展示结果与本节开始所展示的内存布局图是一致的

3.2 有虚函数覆盖

本节不再给出任何分析,读者如果想彻底搞明白可以根据本文上述内容自行画图写代码验证。

https://jocent.me/2017/08/07/virtual-table.html

深入分析C++虚函数表的更多相关文章

  1. C++ 虚函数表解析

    转载:陈皓 http://blog.csdn.net/haoel 前言 C++中 的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实 ...

  2. C++ 多态、虚函数机制以及虚函数表

    1.非virtual函数,调用规则取决于对象的显式类型.例如 A* a  = new B(); a->display(); 调用的就是A类中定义的display().和对象本体是B无关系. 2. ...

  3. C++迟后联编和虚函数表

    先看一个题目: class Base { public: virtual void Show(int x) { cout << "In Base class, int x = & ...

  4. C++ 知道虚函数表的存在

    今天翻看陈皓大大的博客,直接找关于C++的东东,看到了虚函数表的内容,找一些能看得懂的地方记下笔记. 0 引子 类中存在虚函数,就会存在虚函数表,在vs2015的实现中,它存在于类的头部. 假设有如下 ...

  5. C++虚函数和虚函数表

    前导 在上面的博文中描述了基类中存在虚函数时,基类和派生类中虚函数表的结构. 在派生类也定义了虚函数时,函数表又是怎样的结构呢? 先看下面的示例代码: #include <iostream> ...

  6. C++ Daily 《5》----虚函数表的共享问题

    问题: 包含一个以上虚函数的 class B, 它所定义的 对象是否共用一个虚函数表? 分析: 由于含有虚函数,因此对象内存包含了一个指向虚函数表的指针,但是这个指针指向的是同一个虚函数表吗? 实验如 ...

  7. C++虚函数表

    大家知道虚函数是通过一张虚函数表来实现的.在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承.覆盖的问题,其内容真是反应实际的函数.这样,在有虚函数的类的实例中,这个表分配在了这个实例的内存中 ...

  8. 对C++虚函数、虚函数表的简单理解

    一.虚函数的作用 以一个通用的图形类来了解虚函数的定义,代码如下: #include "stdafx.h" #include <iostream> using name ...

  9. 深入理解C++虚函数表

    虚函数表是C++类中存放虚函数的一张表,理解虚函数表对于理解多态很重要. 本次使用的编译器是VS2013,为了简化操作,不用去操作函数指针,我使用到了VS的CL编译选项来查看类的内存布局. CL使用方 ...

随机推荐

  1. python与图灵机器人交互(ITCHAT版本)

    #!/usr/bin/env python#-*- coding:utf-8 -*- @Author : wujf @Time:2018/9/5 17:42import requestsimport ...

  2. 在asyncio 中跳出正在执行的task

    需求描述 代码在asyncio的框架中运行, 但是一旦一个task出现了长时间的堵塞,我们要跳过这个task(代码可能是用户输入的,例如用户编写的插件) 代码如下 (其中大部分代码出自官方的 asyn ...

  3. BZOJ 3876 [AHOI/JSOI2014]支线剧情 (最小费用可行流)

    题面:洛谷传送门 BZOJ传送门 题目大意:给你一张有向无环图,边有边权,让我们用任意条从1号点开始的路径覆盖这张图,需要保证覆盖完成后图内所有边都被覆盖至少一次,求覆盖路径总长度的最小值 最小费用可 ...

  4. luogu 4884 多少个1 (BSGS)

    很有意思的一个签到题 然而考场上并没有切掉 $1111...111=K(mod\;m)$ $10^{x}=9K+1(mod\;m)$ 用$BSGS$求解即可 模数爆了$int$,需要快速乘,然而模数是 ...

  5. 经典alsa 录音和播放程序

    这里贴上虚拟机ubuntu下alsa的录音程序(capture.c)和播放程序(playback.c)的源码. 首先要测试一下自己的ubuntu是否打开了声音.这个可以打开/系统/首选项/声音  来调 ...

  6. 酒店管理系统(功能结构图、E-R图、用例图)

    功能结构图 用例图 状态图 流程图 数据字典 er图

  7. python基础:局部变量--全局变量的使用

    局部变量: 使用原则:仅在本函数内部使用的变量,其他函数无法使用本函数的变量 代码: def function1(): a = 2 #定义一个局部变量 print(a) def function2() ...

  8. spring的关于数据源的datasource接口的深入理解

    1.DataSource的接口这是一个spring接口,可以获取数据库的Connection.是标准化的,取得连接的一种方式. 默认市面上有两个数据库连接池实现了spring的datasource接口 ...

  9. [SharePoint2010开发入门经典]8集成业务线数据,使用业务联通服务

    本章概要: 1.了解office business application 2.理解商务联通服务(BCS),如何使用BCS构建OBA 3.通过BCS使用SharePoint和办公集成技术

  10. 韩国IT业是怎么走向国际我们须要学习什么

    无论从国土面积仍是从人口数量上来衡量.韩国都不能算是一个大国,而且自然资本十分缺乏,即是在这种情况下,韩国经过几十年的尽力开展变成技能大国,格外是在IT这种新经济范畴更是引人注目.并诞生了三星等国际级 ...