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. 查询OSD运行在哪些cpu上

    前言 在看CPU相关的文章的时候,想起来之前有文章讨论是否要做CPU绑定,这个有说绑定的也有说不绑定的,然后就想到一个问题,有去观测这些OSD到底运行在哪些CPU上面么,有问题就好解决了,现在就是要查 ...

  2. C语言设计模式(应用)

    #ifndef QUEUE_H #define QUEUE_H #define QUEUE_SIZE 10 typedef struct queue { int buffer[QUEUE_SIZE]; ...

  3. 单线程的Redis有哪些慢动作?

    持续原创输出,点击上方蓝字关注我 目录 前言 为什么 Redis 这么火? 键和值的保存形式? 为什么哈希表操作变慢了? 集合的操作效率? 有哪些数据结构? 不同操作的复杂度? 总结 前言 现在一提到 ...

  4. tp5获取当前域名

    //头部引入 use think\Request; //获取当前域名 $request = Request::instance(); $domain = $request->domain();

  5. vue项目中echarts属性总结

    <div id="echarts" style="width: 600px;height: 400px;margin-top: 100px;margin-left: ...

  6. css中渐变的分割线和自定义滚动条样式

    css中渐变的分隔线: <div style="background:linear-gradient(to left,#efefef,#b6b6b6,#efefef);height:1 ...

  7. html页面转PDF、图片操作记录

    前言 日常开发中,我们有可能会碰到从系统中导出数据并打印的需要,打印的格式是常规的表格形式,例如: 本文记录使用js库html2canvas + jspdf实现html转PDF.图片,并下载 画出页面 ...

  8. Razorpay支付对接,JAVA对接篇

    Razorpay 作为印度本土的一家支付公司,类似中国的支付宝 微信,本篇记录一下对接印度第三方支付公司 准备工作: 注册公司 申请Razorpay账号 申请正式环境 Razorpay工作台: 获取k ...

  9. python基本输入输出函数

    python程序设计中有三个重要的基本输入.输出函数,用于输入.转换和输出,分别是input(),eval(),print() 1,input()函数 """ input ...

  10. 《技术男征服美女HR》—Fiber、Coroutine和多线程那些事

    1.起点 我叫小白,坐在这间属于华夏国超一流互联网公司企鹅巴巴的小会议室里,等着技术面试官的到来. 令我感到不舒服的,是坐在我对面的那位HR美女一个劲儿的盯着我打量!虽说本人帅气,但是也不能这么毫无顾 ...