C++ 虚函数表与多态 —— 虚函数表的内存布局

 
 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个字节?因为他存的是一张表,他没有占用对象的内存空间,对象中只存在一个指针,指向一个虚函数表,如下方示意图:

    C++ 虚函数表与多态 —— 虚函数表的内存布局

不管你有多少个虚函数,他都在虚函数表里,并且同类下多个对象也会指向同一个虚函数表。

对象内,首先存储的是“虚函数表指针”,又称为“虚表指针”。

然后存储的是非静态数据成员。

对象的非虚函数保存在类的代码中。

对象的内存,只储存虚函数表和数据成员。(类的静态数据成员保存在数据区中,和对象是分开储存的)

添加虚函数后,对象的内存空间不变,仅虚函数表表中添加条目,同类下的多个对象,共享同一个虚函数表。

 

下面用代码打印对象中的各个元素的地址来了解下:

 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 << "nn第一个数据成员地址与对象地址相差:" << (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++ 虚函数表与多态 —— 虚函数表的内存布局

 

 

 

 

 

 

 

 

 

===========================================================================================================================

原文链接: https://www.cnblogs.com/CooCoChoco/p/12549792.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    C++ 虚函数表与多态 —— 虚函数表的内存布局

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/337069

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月1日 下午10:59
下一篇 2023年3月1日 下午10:59

相关推荐