C++ 虚函数的内部实现

虚函数看起来是个玄之又玄的东西,但其实特别简单!了解了虚函数的内部实现,关于虚函数的各种问题都不在话下啦!

1. 知识储备

阅读这篇文章,你需要事先了解以下几个概念:

  • 什么是继承?

  • 什么是虚函数?

    在C++中,在基类的成员函数声明前加上关键字 virtual 即可让该函数成为 虚函数,派生类中对此函数的不同实现都会继承这一修饰符。

  • 为什么需要虚函数?

这涉及到面向对象程序设计中多态动态绑定的概念。

如果你已经完全了解上述概念,那么这篇文章很适合你去深入了解虚函数~

2. C++中类的memory Layout

为了更好地理解虚函数的内部实现,我们首先需要知道,C++的类中成员函数和成员变量在内存中的空间分配。

1. 我们从最普通的一个类说起~
  1. class X {
  2. int x;
  3. float xx;
  4. static int count;
  5. public:
  6. X() {}
  7. ~X() {}
  8. void printInt() {}
  9. void printFloat() {}
  10. static void printCount() {}
  11. };

如果我们在这个程序中定义了这个类的一个对象,那么这个类的内存分布如下图所示:

类的非静态成员变量会被保存在栈上,类的静态成员变量被保存在数据段,而类的成员函数被保存在代码段。

  1. class X {
  2. int x;
  3. float xx;
  4. static int count;
  5. public:
  6. X() {}
  7. ~X() {}
  8. void printInt() {}
  9. void printFloat() {}
  10. static void printCount() {}
  11. };

2. 含有虚函数的基类的内存分布

如果一个类中含有虚函数,那么为了实现动态绑定,编译器会在原来的代码中插入(augment)一个新的成员变量--一个成员指针 vptr, 这个指针指向一张包含所有虚函数的函数指针表 vtable. 当我们调用虚函数时,实际上是通过vptr这个指针来调用函数指针表vtable里面的某个函数指针来实现函数调用。

一般而言,这张vtable会在数据段,是静态的,每一个类仅有一张表。但是这不是死规定,这是由编译器的实现决定的。vptr这个指针和成员变量一致,存在在堆栈段,是每一个对象都会有的。

vtable中的第一个entry包含了当前类及其基类的相关信息,其余entry是函数指针。

现在来看一个例子~

  1. class X {
  2. int x;
  3. float xx;
  4. static int count;
  5. public:
  6. X() {}
  7. virtual ~X() {}
  8. virtual void printAll() {}
  9. void printInt() {}
  10. void printFloat() {}
  11. static void printCount() {}
  12. };

3. 含有虚函数的子类的内存分布

此时,基类的成员变量和成员函数相当于派生类的子对象,也就是说派生类会继承基类的vptr。这时会先为基类的成员函数和成员对象分配内存空间,然后再为派生类的自己的成员变量和成员函数分配空间。vptr会指向Y这个类的vtable

如果派生类写了一个不在基类里的新的虚函数,那么这个vtable会多出一行,行内的内容是指向这个新虚函数的函数指针。

  1. class X {
  2. int x;
  3. public:
  4. X() {}
  5. virtual ~X() {}
  6. virtual void printAll() {}
  7. };
  1. class Y : public X {
  2. int y;
  3. public:
  4. Y() {}
  5. ~Y() {}
  6. void printAll() {}
  7. };

4. 含有虚函数、有多继承的子类的内存分布

有多个基类的派生类会有多个vptr, 用来指向继承自不同基类的vtable。也就是说,每一个有虚函数的基类都会有一个虚函数指针表。

我们来看一个Z类继承自X类和Y类的例子。

  1. class X {
  2. public:
  3. int x;
  4. virtual ~X() {}
  5. virtual void printX() {}
  6. };
  1. class Y {
  2. public:
  3. int y;
  4. virtual ~Y() {}
  5. virtual void printY() {}
  6. };
  1. class Z : public X, public Y {
  2. public:
  3. int z;
  4. ~Z() {}
  5. void printX() {}
  6. void printY() {}
  7. void printZ() {}
  8. };

3. 虚函数的内部实现

了解了虚函数在内存中的分配方式后,理解虚函数的实现以及动态绑定就变得非常简单了。

这里以多继承的子类的代码为例,上代码~

  1. class X {
  2. public:
  3. int x;
  4. virtual ~X() {}
  5. virtual void printX() { cout<<"printX() in X"<<endl; }
  6. };
  7. class Y {
  8. public:
  9. int y;
  10. virtual ~Y() {}
  11. virtual void printY() { cout<<"printY() in Y"<<endl; }
  12. };
  13. class Z : public X, public Y {
  14. public:
  15. int z;
  16. ~Z() {}
  17. void printX() { cout<<"printX() in Z"<<endl; }
  18. void printY() { cout<<"printY() in Z"<<endl; }
  19. void printZ() { cout<<"printZ() in Z"<<endl; }
  20. };
  21. int main(){
  22. Y *y_ptr = new Z();
  23. y_ptr->printY(); // OK
  24. y_ptr->printZ(); // Not OK, Y类的虚函数表中没有printZ()函数
  25. y_ptr->y = 3; // OK
  26. y_ptr->z = 3;// not OK, Y类的空间中没有变量z
  27. }

所以在上述代码中,y_ptr指向的是在Z类对象中的子对象,即Y类对象在Z类中函数与变量。

注意️ 此时y_ptr中的_vptr指向的是Z类对象的vtable

y_ptr->printY()这行代码,其实会被编译器翻译成如下伪代码

  1. ((y_ptr->_vptr)->_vtbl[2])();

其中y_ptr->_vptr指向Y类对象的vptr指针,vptr指针再指向虚函数表中对应的函数指针项,即((y_ptr->_vptr)->_vtbl[2]), 最后通过函数指针来实现函数调用。

由于这个_vptr指向的是Z类对象的虚函数表,所以调用的printY()函数实际上是Z类中实现的printY(),即输出"printY() in Z"。 动态绑定就这样实现了。

4. 用几个问题加深理解

沿用3中的例子,我们来看接下来的几个问题。

Q1. 如果将Z类对象赋值给Y类变量,动态绑定还会发生吗?

即如下代码中,输出是"printY() in Z"还是"printY() in Y"

  1. Z zz;
  2. Y yy = zz;
  3. yy.printY();

答案是不会发生,输出的结果是"printY() in Y"

首先我们需要明确一个很重要的概念,对_vptr这个指针的赋值操作是在构造类对象的过程中发生的。换一句话说,当一个类的实例被创建的时候_vptr被赋值,指向该类的vtable。一旦类的实例被创建,一个类对象里面的_vptr永远不会变,永远都会指向所属类型的虚函数表。

不论是赋值操作还是赋值构造时, 只会处理成员变量,即把zz中的成员变量赋值给yy, 但是_vptr还是指向Y类的虚函数表。

Q2. 如果在基类中不声明某个函数是虚函数,在子类中重写了这个函数,动态绑定还会发生吗?

即如下代码中,输出是"printX() in Z"还是"printX() in X"

  1. class X {
  2. public:
  3. int x;
  4. virtual ~X() {}
  5. void printX() { cout<<"printX() in X"<<endl; }
  6. };
  7. class Z : public X {
  8. public:
  9. int z;
  10. ~Z() {}
  11. void printX() { cout<<"printX() in Z"<<endl; }
  12. void printZ() { cout<<"printZ() in Z"<<endl; }
  13. };
  14. int main(){
  15. X *x_ptr = new Z();
  16. x_ptr->printX(); // OK
  17. }

答案是不会发生,输出的结果是"printX() in X"。没有声明为虚函数的函数,不会被放入虚函数表中,即vtable不会保存该函数的函数指针。这时,动态绑定肯定不会发生了。

5. 总结

  1. 一般而言,虚函数表是属于一个类的(one vtable per class), 位于静态数据区,而虚函数表指针_vptr是属于一个类的对象的(one vptr per object).
  2. 一个由多继承关系的类会有多个虚函数指针。
  3. 虚函数指针的赋值操作是在构造类对象的过程中发生的,之后的赋值操作不会改变vptr的值
  4. C++标准没有定义动态绑定的具体实现方式,只是陈述了动态绑定的行为。具体的实现与编译器相关。

以上为个人学习总结,如有错误欢迎指出!

这篇博文参考了如下文章:

http://www.vishalchovatiya.com/memory-layout-of-cpp-object/

http://www.vishalchovatiya.com/part-1-all-about-virtual-keyword-in-cpp-how-virtual-function-works-internally/

https://www.learncpp.com/cpp-tutorial/the-virtual-table/

https://www.cnblogs.com/yinheyi/p/10525543.html

C++ 虚函数的内部实现的更多相关文章

  1. C++中的多态与虚函数的内部实现

    1.什么是多态         多态性可以简单概括为“一个接口,多种行为”.         也就是说,向不同的对象发送同一个消息, 不同的对象在接收时会产生不同的行为(即方法).也就是说,每个对象可 ...

  2. C++虚函数解析(转载)

    虚函数详解第一篇:对象内存模型浅析 C++中的虚函数的内部实现机制到底是怎样的呢?     鉴于涉及到的内容有点多,我将分三篇文章来介绍.     第一篇:对象内存模型浅析,这里我将对对象的内存模型进 ...

  3. c++ 类内部函数调用虚函数

    做项目的过程中,碰到一个问题. 问题可以抽象为下面的问题: 普通人吃饭拿筷子,小孩吃饭拿勺子. class People { public: void eat() { get_util_to_eat( ...

  4. 为何JAVA虚函数(虚方法)会造成父类可以"访问"子类的假象?

      首先,来看一个简单的JAVA类,Base. 1 public class Base { 2 String str = "Base string"; 3 protected vo ...

  5. C++虚函数浅探

    C++中和虚函数(Virtual Function)密切相关的概念是"动态绑定"(Dynamic Binding),与之相对的概念是"静态绑定"(Static ...

  6. C++之虚函数和多态

    干货较多-需要自己深思理解: C++支持两种多态性: 1.编译时多态性(静态绑定-早绑定) 在程序编译阶段即可以确定下来的多态性 通过使用 重载机制(重载函数)实现 (模板)http://blog.c ...

  7. C++中的虚函数(表)实现机制以及用C语言对其进行的模拟实现

    tfref 前言 C++对象的内存布局 只有数据成员的对象 没有虚函数的对象 拥有仅一个虚函数的对象 拥有多个虚函数的对象 单继承且本身不存在虚函数的继承类的内存布局 本身不存在虚函数(不严谨)但存在 ...

  8. C++纯虚函数

    本文较为深入的分析了C++中虚函数与纯虚函数的用法,对于学习和掌握面向对象程序设计来说是至关重要的.具体内容如下: 首先,面向对象程序设计(object-oriented programming)的核 ...

  9. c# 基础(重写与覆盖:接口与抽象,虚函数与抽象函数)

    总结 1:不管是重写还是覆盖都不会影响父类自身的功能(废话,肯定的嘛,除非代码被改). 2:当用子类创建父类的时候,如 C1 c3 = new C2(),重写会改变父类的功能,即调用子类的功能:而覆盖 ...

随机推荐

  1. C++算法代码——细胞问题

    题目来自:http://218.5.5.242:9018/JudgeOnline/problem.php?id=1152 http://ybt.ssoier.cn:8088/problem_show. ...

  2. Spring中的@Enable注解

    本文转载自SpringBoot中神奇的@Enable注解? 导语 在SpringBoot开发过程,我们经常会遇到@Enable开始的好多注解,比如@EnableEurekaServer.@Enable ...

  3. 微信小程序:上滑触底加载下一页

    给商品列表页面添加一个上滑触底加载下一页的效果,滚动条触底之后就发送一个请求,来加载下一页数据, 先在getGoodsList中获取总条数 由于总页数需要再另外的一个方法中使用,所以要把总页数变成一个 ...

  4. JS驼峰与下划线互转

    1.下划线转驼峰 function underlineToHump(s){ var a = s.split("_"); var result = a[0]; for(var i=1 ...

  5. Linux系统管理--part(1)

    Linux系统管理--part(1) Linux系统安装完毕,需要对Linux系统进行管理和维护,让Linux服务器能够真正英语于企业中 Linux运维的三个步骤安装.调试.启动 通过本篇文章,将学习 ...

  6. 读懂一个中型的Django项目

    转自https://www.cnblogs.com/huangfuyuan/p/Django.html [前言]中型的项目是比较多的APP,肯会涉及多数据表的操作.如果有人带那就最好了,自己要先了解基 ...

  7. Kubernetes-2.组件

    内容主要摘自官网文档资料 官网地址 本文概述了交付正常运行的Kubernetes集群所需的各种组件. 本文编写基于kubernetes v1.17版本 目录 Kubernetes集群 Master组件 ...

  8. 一文了解Python的迭代器的实现

    本文对迭代器的解释参考自:https://www.programiz.com/python-programming/iterator 最后自己使用迭代器实现一个公平洗牌类. 博主认为,理论来自实践,假 ...

  9. Blackduck的Hub安装教程

    1 产品介绍 Black Duck 是最早进行开源代码检测工具开发的公司,其产品包括Protex 和HUB,Protex 强调检测的精度和准确性,而HUB 强调检测的速度和易用性. 1.1 Prote ...

  10. Python切换版本工具pyenv

    目录 安装pyenv 安装与查看py版本 切换py版本 结合ide使用示例 和virtualenv的一些区别 参考文献 使用了一段时间,我发现这玩意根本不是什么神器,简直就是垃圾,安装多版本总是失败, ...