C++ 虚函数表与多态 —— 虚函数表的内存布局
1. 多态的本质:
形式上,使用统一的父类指针做一般性处理。但是实际执行时,这个指针可能指向子类对象。
形式上,原本调用父类的方法,但是实际上会调用子类的同名方法。
坦白的说,多态就是为了通过使用父类的指针,能够调用父类与子类他们各自的方法。如果不使用多态,用父类指针调用子类的方法时,也会调用到父类的方法。
【注意】
程序执行时,父类指针指向父类对象,或子类对象时,在形式上是无法分辨的。只有通过多态机制,才能执行真正对应的方法。
2. 虚函数:
在父类的方法函数前,增加 virtual 便可以使这个函数变为虚函数,如:
需要注意一点,例子用的是内联函数,封装到外部时,具体方法实现前不用加 virtual,用了会出错。
1 class Father
2 {
3 public:
4 virtual void play() //父类的 play() 方法前增加 virtual 关键字,这个函数便成为了虚函数
5 {
6 std::cout << "这是个父类的play" << std::endl;
7 }
8 };
9
10 class Son : public Father
11 {
12 public:
13 void play()
14 {
15 std::cout << "这是个子类的Play" << std::endl;
16 }
17 };
3. 虚函数的继承:
如果某个成员函数被声明为虚函数,那么它的子类【派生类】中所继承的成员函数,也会变为虚函数。
如果在子类中重写这个虚函数,可以不用再写 virtual ,但是仍建议写上 virtual,这样会使代码更可读,如13行:
1 class Father
2 {
3 public:
4 virtual void play() //父类的 play() 方法前增加 virtual 关键字,这个函数便为虚函数
5 {
6 std::cout << "这是个父类的play" << std::endl;
7 }
8 };
9
10 class Son : public Father
11 {
12 public:
13 virtual void play() //派生类继承的虚函数前,可以不加 virtual,但加上会使代码更加可读
14 {
15 std::cout << "这是个子类的Play" << std::endl;
16 }
17 };
4. 虚函数表的原理 & 对象内存空间:
虚函数的原理是通过虚函数表来实现的,虚函数表是编译器搞出来的东西他并不存在于对象中,先看下边代码:
1 #include <iostream>
2 using namespace std;
3
4 class Father
5 {
6 public:
7 virtual void func_1() { cout << "Father::func_1" << endl; }
8 virtual void func_2() { cout << "Father::func_2" << endl; }
9 virtual void func_3() { cout << "Father::func_3" << endl; }
10 };
12
13 int main(void)
14 {
15 Father father_1; //虚函数表就保存在这个 father 对象里边
16
17 cout << "sizeof(father_1)=="<< sizeof(father_1) << endl;
18
19 }
运行后打印一下,看看 father 对象占用多大内存空间。
运行结果:sizeof(father_1)==4
3个虚函数为什么只占4个字节?因为他存的是一张表,他没有占用对象的内存空间,对象中只存在一个指针,指向一个虚函数表,如下方示意图:

不管你有多少个虚函数,他都在虚函数表里,并且同类下多个对象也会指向同一个虚函数表。
对象内,首先存储的是“虚函数表指针”,又称为“虚表指针”。
然后存储的是非静态数据成员。
对象的非虚函数保存在类的代码中。
对象的内存,只储存虚函数表和数据成员。(类的静态数据成员保存在数据区中,和对象是分开储存的)
添加虚函数后,对象的内存空间不变,仅虚函数表表中添加条目,同类下的多个对象,共享同一个虚函数表。
下面用代码打印对象中的各个元素的地址来了解下:
1 #include <iostream>
2 using namespace std;
3
4 class Father
5 {
6 public:
7 virtual void func_1() { cout << "Father::func_1" << endl; }
8 virtual void func_2() { cout << "Father::func_2" << endl; }
9 virtual void func_3() { cout << "Father::func_3" << endl; }
10 void func_4() { cout << "非虚函数:Father::func_4" << endl; } //它不存在与对象中
11
12 public:
13 int x = 666;
14 int y = 888;
15 };
16
17 typedef void(*func_t)(void); //定义一个函数指针类型,返回类型void,参数也是void,给 33 行进行函数类型转换
18
19 int main(void)
20 {
21 Father father; //虚函数表就保存在这个 father 对象里边
22
23 cout << "sizeof(father)=="<< sizeof(father) << endl;
24
25 cout << "对象地址:" << (int*)&father << endl; //转换为int类型的指针,会打印出十六进制的地址
26
27 int* vptr = (int*)*(int*)(&father); //取到虚函数表的地址
28 //第一个 (int*) 仅仅是为了让编译器通过,因为 *(int*)(&father) 取出来的是一个整数,而接受类型是 int*
29 //中间的 * 号,取 father 对象地址中的内容
30 //第二个 (int*) 是强转为 int* 后取地址,不强转类型会不匹配
31
32 cout << "通过虚函数表指针调用第一个虚函数:";
33 ((func_t) * (vptr + 0))(); //vptr 是虚函数表的地址,加*号取内容,访问到第一个虚函数,但这时他是一个地址,我们需要给他强转为函数
34
35 cout << "\n通过虚函数表指针调用第二个虚函数:";
36 ((func_t) * (vptr + 1))();
37
38 cout << "\n通过虚函数表指针调用第三个虚函数:";
39 ((func_t) * (vptr + 2))();
40
41 cout << "\n查看其他成员地址:" << endl;
42 cout << "访问方式一:数据成员 x 的地址:" << &father.x << endl;
43 cout << "访问方式二:数据成员 x 的地址:" << std::hex << (int)&father + 4 << endl;
44
45 cout << "\n\n第一个数据成员地址与对象地址相差:" << (char)&father.x - (char)(int*)&father << endl;
46
47 //方式二:取father的地址,转成int类型后+4个字节访问对象的第2个数据成员,然后再把地址值转成指针,访问里边的数据
48 cout << "\n第一个数据成员 x 的值:" << endl;
49 cout << "访问方式一:" << std::dec << father.x << endl;
50 cout << "访问方式二:" << *(int*)((int)&father + 4) << endl;
51
52 cout << "\n第二个数据成员 y 的值:" << endl;
53 cout << "访问方式一:" << std::dec << father.y << endl;
54 cout << "访问方式二:" << *(int*)((int)&father + 8) << endl;
55 }
打印结果:
sizeof(father)==12
对象地址:0033F994
通过虚函数表指针调用第一个虚函数:Father::func_1
通过虚函数表指针调用第二个虚函数:Father::func_2
通过虚函数表指针调用第三个虚函数:Father::func_3
查看其他成员地址:
访问方式一:数据成员 x 的地址:0033F998
访问方式二:数据成员 x 的地址:33f998
第一个数据成员地址与对象地址相差:4
第一个数据成员 x 的值:
访问方式一:666
访问方式二:666
第二个数据成员 y 的值:
访问方式一:888
访问方式二:888
如果觉得上边方法太过于麻烦,那么你可以使用VS编译器来打印内存布局,方法如下:
项目的命令行配置中添加: /d1 reportSingleClassLayoutFather
项目属性 -> 配置属性 -> C/C++ -> 命令行
编译代码后的输出打印:

===========================================================================================================================
C++ 虚函数表与多态 —— 虚函数表的内存布局的更多相关文章
- C++ | 虚函数表内存布局
虚表指针 虚函数有个特点.存在虚函数的类会在类的数据成员中生成一个虚函数指针 vfptr,而vfptr 指向了一张表(简称,虚表).正是由于虚函数的这个特性,C++的多态才有了发生的可能. 其中虚函数 ...
- 继承虚函数浅谈 c++ 类,继承类,有虚函数的类,虚拟继承的类的内存布局,使用vs2010打印布局结果。
本文笔者在青岛逛街的时候突然想到的...最近就有想写几篇关于继承虚函数的笔记,所以回家到之后就奋笔疾书的写出来发布了 应用sizeof函数求类巨细这个问题在很多面试,口试题中很轻易考,而涉及到类的时候 ...
- Linux Debugging(四): 使用GDB来理解C++ 对象的内存布局(多重继承,虚继承)
前一段时间再次拜读<Inside the C++ Object Model> 深入探索C++对象模型,有了进一步的理解,因此我也写了四篇博文算是读书笔记: Program Transfor ...
- 从零开始学C++之虚函数与多态(一):虚函数表指针、虚析构函数、object slicing与虚函数
一.多态 多态性是面向对象程序设计的重要特征之一. 多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为. 多态的实现: 函数重载 运算符重载 模板 虚函数 (1).静态绑定与动态绑 ...
- 虚函数表-C++多态的实现原理
目录 1.说明 2.虚函数表 3.代码示例 参考:http://c.biancheng.net/view/267.html 1.说明 我们都知道多态指的是父类的指针在运行中指向子类,那么它的实现原理是 ...
- C++ 虚函数表与多态 —— 多重继承的虚函数表 & 内存布局
多重继承的虚函数表会有两个虚表指针,分别指向两个虚函数表,如下代码中的 vptr_s_1.vptr_s_2,Son类继承自 Father 和 Mather 类,并且改写了 Father::func_1 ...
- C++对象的内存布局以及虚函数表和虚基表
C++对象的内存布局以及虚函数表和虚基表 本文为整理文章, 参考: http://blog.csdn.net/haoel/article/details/3081328 http://blog.csd ...
- c++基础之虚函数表指针和虚函数表创建时机
虚函数表指针 虚函数表指针随对象走,它发生在对象运行期,当对象创建的时候,虚函数表表指针位于该对象所在内存的最前面. 使用虚函数时,虚函数表指针指向虚函数表中的函数地址即可实现多态. 虚函数表 虚函数 ...
- vs查看虚函数表和类内存布局
虚继承和虚基类 虚继承:在继承定义中包含了virtual关键字的继承关系: 虚基类:在虚继承体系中的通过virtual继承而来的基类,需要注意的是:class CSubClass : publ ...
随机推荐
- python执行rados命令例子
前言 我们以前的管理平台在python平台下面做的,内部做的一些操作采用的是命令执行,然后解析的方式去做的,ceph自身有python的rados接口,可以直接调用原生接口,然后直接解析json的方式 ...
- Python 单表查询数据
import pymssql #引入pymssql模块# -----------------------------------------------------import pymssql #引入 ...
- 面试老被问LinkedList源码?看看阿里技术官是怎么深度剖析的吧!
前言 LinkedList底层是基于双向链表,链表在内存中不是连续的,而是通过引用来关联所有的元素,所以链表的优点在于添加和删除元素比较快,因为只是移动指针,并且不需要判断是否需要扩容,缺点是查询和遍 ...
- FL Studio通道预设之采样预览
FL Studio采样预览栏在采样设置窗口的最底端,它能很好地显示 出载入采样的波形也可以将波形显示改为频谱显示.它里面显示出的是经过预处理效果栏处理后的波形或频谱图.我们在波形显示器下面还可以看到波 ...
- 如何在Visio 中插入各种数学公式?
在Visio 2007老版本中,插入公式可以直接在插入图片中选择,但是在后来的Visio2013中却无法直接通过插入图片的方法插入,那么该如何在visio 2013中插入公式呢? 具体的操作步骤如下: ...
- zabbix地图显示全国延迟
Zabbix 地图显示全国延迟 1. 效果图 2. 实现方法 将地图.png上传到zabbix为背景,上传红绿点.png为图标.然后新建主机关联模板为ICMP Ping,新建一个拓扑图调用地图为背 ...
- C语言新手入门
include<stdio.h> int main() { //输入一个数 输出它的反序列 c int a,b=0; scanf("%d",&a);//输入一个 ...
- Linux无法新增用户
1.查看当前用户是否有权限创建用户 2.磁盘空间不足,vi打开/etc/passwd 报: E297: Write error in swap file"adduser.sh" 1 ...
- (1)Consul在linux环境的集群部署
1.Consul概念 1.1什么是Consul? Consul是一种服务网格解决方案,是HashiCorp公司推出的开源组件,由Go语言开发,部署起来很容易,只需要极少的可执行程序和配置.同时Cons ...
- 第一次UML编程作业
博客班级 https://edu.cnblogs.com/campus/fzzcxy/2018SE2/ 作业要求 https://edu.cnblogs.com/campus/fzzcxy/2018S ...