C++面试经常会被问的问题就是多态原理。如果对C++面向对象本质理解不是特别好,问到这里就会崩。 下面从基本到原理,详细说说多态的实现:虚函数 & 虚函数表。
 

1. 多态的本质:

形式上,使用统一的父类指针做一般性处理。但是实际执行时,这个指针可能指向子类对象。

形式上,原本调用父类的方法,但是实际上会调用子类的同名方法。

坦白的说,多态就是为了通过使用父类的指针,能够调用父类与子类他们各自的方法。如果不使用多态,用父类指针调用子类的方法时,也会调用到父类的方法。

具体参考:C++ 虚函数表与多态 —— 多态的简单用法

【注意】

程序执行时,父类指针指向父类对象,或子类对象时,在形式上是无法分辨的。只有通过多态机制,才能执行真正对应的方法。

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++ 虚函数表与多态 —— 虚函数表的内存布局的更多相关文章

  1. C++ | 虚函数表内存布局

    虚表指针 虚函数有个特点.存在虚函数的类会在类的数据成员中生成一个虚函数指针 vfptr,而vfptr 指向了一张表(简称,虚表).正是由于虚函数的这个特性,C++的多态才有了发生的可能. 其中虚函数 ...

  2. 继承虚函数浅谈 c++ 类,继承类,有虚函数的类,虚拟继承的类的内存布局,使用vs2010打印布局结果。

    本文笔者在青岛逛街的时候突然想到的...最近就有想写几篇关于继承虚函数的笔记,所以回家到之后就奋笔疾书的写出来发布了 应用sizeof函数求类巨细这个问题在很多面试,口试题中很轻易考,而涉及到类的时候 ...

  3. Linux Debugging(四): 使用GDB来理解C++ 对象的内存布局(多重继承,虚继承)

    前一段时间再次拜读<Inside the C++ Object Model> 深入探索C++对象模型,有了进一步的理解,因此我也写了四篇博文算是读书笔记: Program Transfor ...

  4. 从零开始学C++之虚函数与多态(一):虚函数表指针、虚析构函数、object slicing与虚函数

    一.多态 多态性是面向对象程序设计的重要特征之一. 多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为. 多态的实现: 函数重载 运算符重载 模板 虚函数 (1).静态绑定与动态绑 ...

  5. 虚函数表-C++多态的实现原理

    目录 1.说明 2.虚函数表 3.代码示例 参考:http://c.biancheng.net/view/267.html 1.说明 我们都知道多态指的是父类的指针在运行中指向子类,那么它的实现原理是 ...

  6. C++ 虚函数表与多态 —— 多重继承的虚函数表 & 内存布局

    多重继承的虚函数表会有两个虚表指针,分别指向两个虚函数表,如下代码中的 vptr_s_1.vptr_s_2,Son类继承自 Father 和 Mather 类,并且改写了 Father::func_1 ...

  7. C++对象的内存布局以及虚函数表和虚基表

    C++对象的内存布局以及虚函数表和虚基表 本文为整理文章, 参考: http://blog.csdn.net/haoel/article/details/3081328 http://blog.csd ...

  8. c++基础之虚函数表指针和虚函数表创建时机

    虚函数表指针 虚函数表指针随对象走,它发生在对象运行期,当对象创建的时候,虚函数表表指针位于该对象所在内存的最前面. 使用虚函数时,虚函数表指针指向虚函数表中的函数地址即可实现多态. 虚函数表 虚函数 ...

  9. vs查看虚函数表和类内存布局

    虚继承和虚基类 虚继承:在继承定义中包含了virtual关键字的继承关系:     虚基类:在虚继承体系中的通过virtual继承而来的基类,需要注意的是:class CSubClass : publ ...

随机推荐

  1. Linux_Python版本控制

    第1步:更新gcc,因为gcc版本太老会导致新版本python包编译不成功 复制代码代码如下: yum -y install gcc 系统会自动下载并安装或更新,等它自己结束 第2步:安装wget,这 ...

  2. [USACO14JAN]Ski Course Rating G

    题目链接:https://www.luogu.com.cn/problem/P3101 Slove 这题我们可以尝试建立一个图. 以相邻的两个点建边,边的权值为两个点高度差的绝对值,然后把边按照边权值 ...

  3. Linux下Docker容器安装与使用

    注:作者使用的环境是CentOS 7,64位,使用yum源安装. 一.Docker容器的安装 1.查看操作系统及内核版本,CentOS 7安装docker要求系统为64位.系统内核版本为 3.10及以 ...

  4. 不会吧,你连Java 多线程线程安全都还没搞明白,难怪你面试总不过

    什么是线程安全? 当一个线程在同一时刻共享同一个全局变量或静态变量时,可能会受到其他线程的干扰,导致数据有问题,这种现象就叫线程安全问题. 为什么有线程安全问题? 当多个线程同时共享,同一个全局变量或 ...

  5. 简单实用的Boom 3D基础入门教程分享

    Boom 3D可以很大限度的弥补声音设备或是环境的不足,满足您更加高级的声学体验.Boom 3D用简单明了的方式帮助您设计声音,即使您不是专业的声音编辑,也可以达到专业相似的效果. 打开Boom 3D ...

  6. 视频剪辑软件Camtasia的快捷键大全

    今天来给大家介绍一下Camtasia快捷键的相关内容,Camtasia也是一个十分好用的电脑屏幕录制与视频剪辑制作软件了,可能有些朋友用过,毕竟它在视频录制与制作上确实比较好用. 首先在菜单栏中点击& ...

  7. web自动化测试,弹出窗的操作

    弹出窗有两种: 1.alert弹窗 2.页面弹出窗 什么是alert弹窗呢,点击某一个事件后,会弹出一个弹窗,如下图所示,相信大家在测试中有遇到过,怎么操作它呢 1.1弹窗出现后,使用switch_t ...

  8. QBXT 提高组储备营 2020.夏 游记

    DAY 1 是第一天呐!老师好强!讲得好仔细!连我都全懂了![doge] 突然对后面几天充满了期待-- 复习内容:二分,排序,贪心,搜索(好评) 新知识:Huffman树及Huffman编码,对拍,二 ...

  9. 【CF983C】elevator——记忆化搜索

    (题面来自luogu) 题意翻译 题意 一个9层的楼有一个可以容纳4个人的电梯,你要管理这个电梯. 现在各层楼上有一些在排队的人,你知道他们在哪层要到哪层去.你也知道到电梯门口的顺序.根据公司的规定, ...

  10. vue跨域请求

    浏览器的同源策略 同源 协议相同 域名相同 端口相同 同源目的 保证用户信息安全,防止恶意的网站窃取数据 同源策略解决方法 jsonp cors 代理解决跨域 settings.py INSTALLE ...