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. Hive 报错 Failed to load class "org.slf4j.impl.StaticLoggerBinder".

    打开hive报错 SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". SLF4J: Defaultin ...

  2. txt文件覆盖恢复

    1.txt文件恢复到之前保存的版本 2.电脑未重启 方式:如果你使用系统还原可以用"还原以前的版本"功能来轻松找回. 右击.txt文件-还原以前的版本-选择时间点-还原

  3. 【Camtasia教学】如何添加光标效果

    随着网络技术的快速发展,手机等移动工具越来越普及,我们的生活也发生了很大的变化,例如我们以前必须去到学校才能学习知识,但是现在躺在床上都可以看国外的教学视频.所以在网上录制教学或者演示视频变得越来越常 ...

  4. Improving Commonsense Question Answering by Graph-based Iterative Retrieval over Multiple Knowledge Sources —— 基于多知识库迭代检索的常识问答系统

    基于多知识库迭代检索的问答系统 论文地址 背景 常识问答任务需要引入外部知识来帮助模型更好地理解自然语言问题,现有的解决方案大都采用两阶段框架: 第一阶段 -- 从广泛的知识来源中找到与给定问题相关的 ...

  5. Python GUI之Tkiner实战

    前言 Tkinter 是 Python 的标准 GUI 库.Python 使用 Tkinter 可以快速的创建 GUI 应用程序. 由于 Tkinter 是内置到 python 的安装包中.只要安装好 ...

  6. python批量爬取猫咪图片

    不多说直接上代码 首先需要安装需要的库,安装命令如下 pip install BeautifulSoup pip install requests pip install urllib pip ins ...

  7. Java集合【8】-- ArrayList源码分析

    目录 1. ArrayList 1.1 ArrayList特点介绍 1.2 实现的接口和继承的类 2. 成员变量 3. 构造方法 4. 常用增删改查方法 添加元素 查询元素 更新元素 删除元素 5.自 ...

  8. 基于HAL库的STM32的DSP库详解(附FFT应用)

    1 . 建立工程,生成代码时选择包含所有库.   2. 打开 option for target 选择 Target 标签,在code generatio中,将floating point hardw ...

  9. 自动化运维工具之Puppet常用资源(一)

    前文我们聊到了puppet的架构,单机模型和master/agent模型的工作流程以及puppet的基础使用,回顾请参考https://www.cnblogs.com/qiuhom-1874/p/14 ...

  10. Bootstrap(修改中)

    表格 斑马表格 <table class="table-striped"> </table> 鼠标经过表格的hover效果 <table class= ...