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. Python_字符串处理方法

    1.字符串转换 #strcpy(sStr1,sStr2) sStr1 = 'strcpy' sStr2 = sStr1 sStr1 = 'strcpy2' print sStr2 1.字符串复制 #s ...

  2. Struts2漏洞渗透笔记

    虽然这是一次失败的渗透,但是也学到不少新姿势. 目标机环境:①外网②win2012③360全家桶 一,利用Struts2终极利用工具上传一句话 jsp的一句话,有时候会出现兼容问题.比如Cknife的 ...

  3. cookie和session的关系和区别

    1.为什么会用到cookie和session 由于http请求是一种无状态的请求,一旦数据交换完毕便会关闭请求,再次交换数据则要再次发起请求,所以服务端无法通过连接追踪会话,确定用户身份,而cooki ...

  4. bWAPP----iFrame Injection

    iFrame Injection 直接上代码 1 <div id="main"> 2 3 <h1>iFrame Injection</h1> 4 ...

  5. 公司新来的小姐姐不懂java中的static关键字,这样给她描述不香吗?

    前言 static关键字是摆在刚入行编程语言的小白们面前的一道难题,为什么要用static?使用它有什么好处?修饰方法和修饰变量有什么区别?本文将就java中static关键字的使用方法及注意事项进行 ...

  6. Vim注释行的方法

    目录 一.Visual block 加注释 去注释 二.正则表达式 加注释 去注释 一.Visual block 加注释 1.首先按键盘上的ESC进入命令行模式 2.再按Ctrl+V进入VISUAL ...

  7. 【干货】linux使用nginx一个80端口部署多个项目(spring boot、vue、nuxt、微信小程序)

    本人只有一个阿里云的ip和一个已经解析过的域名,然后想用80端口部署多个项目,比如输入: www.a.com和www.b.com与www.c.com就能访问不同项目,而不用输入不同端口号区分. 1.这 ...

  8. C语言常用的一些转换工具函数!

    1.字符串转十六进制 代码实现: 2.十六进制转字符串 代码实现: 或者 效果:十六进制:0x13 0xAA 0x02转为字符串:"13AAA2" 3.字符串转十进制 代码实现: ...

  9. jmp使用

    jps -l jmap 36429 jmap -heap 36429 jmap -histo:live 36429 jmap -clstats 36429 jmap  -finalizerinfo 3 ...

  10. 创建topic

    sh kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic ...