c++多态的进一步了解,类的虚函数表,对象的虚函数表指针,虚函数调用过程

在之前的文章中我们详细介绍了关于虚函数的相关知识,比如说虚函数的分类,普通虚函数和纯虚函数,抽象类,虚函数的重写,继承+虚函数实现的c++中的多态。

但是这些知识相对来说还是比较基础的知识,在c++相关的面试过程中很可能对于虚函数有着更加详细问法,所以这里我们需要对虚函数的底层实现有着更加深入的理解。

如果对于这些知识还不是很了解的朋友可以先看我的文章:

c++中虚函数(virtual),重写(override),多态(重点介绍动态多态)_c++ 重写父类方法加virtual-CSDN博客

相信了解完上面关于多态和虚函数的基础知识的话,对于本篇文章的食用效果更佳!!!

c++多态的进一步了解,类的虚函数表,对象的虚函数表指针,虚函数调用过程_第1张图片

虚函数表&虚函数表指针介绍

每个包含虚函数的类都有自己的虚函数表: 虚函数表是类级别的,而不是对象级别的。对于每个声明了虚函数的类,编译器都会创建一个与之关联的虚函数表。

虚函数表的内容: 虚函数表中存储的是该类的所有虚函数的地址。如果派生类覆盖了基类的虚函数,那么派生类的虚函数表中相应位置的函数地址会被替换为派生类中该虚函数的地址

对象中的虚函数表指针:当创建一个包含虚函数的类的对象时,编译器会在该对象占用空间内部(通常是对象的起始位置)添加一个隐藏的指针,这个指针就是虚函数表指针(vptr)。这个虚函数表指针指向该对象所属类的虚函数表,更具体来说指向的是该对象所属类的虚函数表(vtable)的起始地址。

一个单一继承的类的对象最多只会有一个虚函数表指针

一个单一继承的类的对象最多只会有一个虚函数表指针(如果该类或其基类有虚函数),即使它包含多个其他类的对象作为成员变量。成员对象可能会有它们自己的虚函数表指针,但这些是属于成员对象自身的,并不是属于这个类的对象的。

运行时多态的实现: 当通过基类指针或引用调用虚函数时,程序会首先通过对象的虚函数表指针找到对应的虚函数表,然后根据要调用的虚函数在表中的偏移量找到实际要执行的函数地址,指向函数地址对应的代码段,从而实现运行时多态。

由对象的虚函数表指针来获取虚函数地址

我们已经知道:编译器会在对象占用空间内部(通常是对象的起始位置)添加一个隐藏的指针,这个指针就是虚函数表指针(vptr)。

一个类中可以拥有多个虚函数,通过对象的虚函数表指针,我们可以获取到该虚函数表(vtable)中所有虚函数的地址。

那么如何通过虚函数表指针来获取到具体的某个虚函数地址呢?

当一个类声明或继承了多个虚函数时,这些虚函数的地址都会按照特定的顺序存储在该类的虚函数表中。

对象的虚函数表指针(vptr)指向的就是这个虚函数表的起始位置。通过 vptr 找到虚函数表后,你可以将这个表看作是一个数组,数组中的每个元素(表项)都存储着一个虚函数的地址。虚函数在表中的顺序通常与它们在类定义中声明的顺序(以及在继承层次中的顺序)有关。

例如,如果一个类(没有继承其他类的虚函数)定义了三个虚函数,那么它的虚函数表就会包含三个表项,分别存储着这三个虚函数的地址。通过对象的 vptr,你可以访问到这个表,并从中获取到这三个地址。

根据你调用的虚函数名字,可以确定拿到它在 vtable 中的索引。

结论:因此,通过一个对象的虚函数表指针 + 虚函数在表中的索引(或者偏移量),你可以访问到其虚函数表中特定的虚函数的地址

虚函数地址的存放顺序规则

当一个子类继承父类时,子类的虚函数表(vtable)会:

  1. 继承父类的虚函数表前部内容,即父类中声明的虚函数的地址顺序。

  2. 如果子类重写(override)了父类的虚函数,那么子类的虚函数表中对应位置的函数指针会被替换为子类中重写后的函数地址

  3. 子类新增的虚函数会被追加到父类虚函数表末尾。

实现多态的底层过程

当你通过基类指针或引用调用一个虚函数时,实际上发生的是以下过程

  1. 程序会获取到该对象(实际可能是派生类对象)的虚函数表指针 (vptr)。
  2. 通过 vptr 找到该对象类型对应的虚函数表 (vtable)。
  3. 根据你调用的虚函数在基类中声明的顺序,确定它在 vtable 中的索引。
  4. 从 vtable 的相应索引处获取存储的虚函数的地址。
  5. 最终,程序会跳转到这个地址开始执行函数代码。

c++多态的进一步了解,类的虚函数表,对象的虚函数表指针,虚函数调用过程_第2张图片

  • 首先在主函数的栈帧上有一个 A 类型的指针指向堆里面分配好的对象 A 实例。
  • 对象 A 实例的头部是一个 vtable 指针,紧接着是 A 对象按照声明顺序排列的成员变量。(当我们创建一个对象时,便可以通过实例对象的地址,得到该实例的虚函数表,从而获取其函数指针。)
  • vtable 指针指向的是代码段中的 A 类型的虚函数表中的第一个虚函数起始地址

注意

在实际的软件开发中,我们没有必要关心具体调用的虚函数的地址是什么,而且在绝大多数情况下也用不到

底层调试和分析: 在进行非常底层的调试,例如分析程序的内存布局、排查非常难以追踪的错误时,开发者可能会使用调试器查看虚函数表和其中的地址。一般我们不会直接在程序中通过代码来获取虚函数地址进行查看,即使是底层调试的话也是通过调试器来查看。下面是调试器的一些常见的作用

调试器的作用:

调试器是一种用于测试和调试计算机程序的重要工具。它允许开发者在受控的环境下运行程序,并提供了多种功能来帮助理解程序的执行过程,从而找到并修复错误(bug)。调试器并不是编译器的一部分,他们是相互独立的两个部分,调试器和编译器一般都集成于开发环境。以下是调试器的一些主要作用:

  • 受控运行程序: 调试器可以启动并控制程序的运行。你可以随时暂停、单步执行或恢复程序的执行。

  • 设置断点 (Breakpoint): 你可以在代码的特定位置(例如某一行代码)设置断点。当程序执行到断点时,会暂停运行,让你检查程序的状态。你也可以设置条件断点,当满足特定条件时才暂停。

  • 单步执行 (Stepping): 调试器允许你一行一行地执行代码。这可以帮助你跟踪程序的执行流程,查看每一行代码执行后的结果。通常有“步入”(step into,进入函数调用)、“步过”(step over,跳过函数调用)、“步出”(step out,从当前函数返回)等不同的单步执行方式。

  • 检查变量 (Inspect Variables): 当程序暂停时,你可以查看当前作用域内所有变量的值(包括局部变量、全局变量和成员变量)。这有助于你了解程序的数据状态是否符合预期。

  • 查看内存 (Examine Memory): 调试器通常允许你查看程序使用的原始内存,这对于理解数据结构在内存中的布局以及诊断内存相关的问题(如内存溢出、野指针等)非常有用。

  • 监视表达式 (Watch Expressions): 你可以设置需要监视的变量或表达式。调试器会在程序执行过程中跟踪这些变量或表达式的值,并在值发生变化时通知你,或者在满足特定条件时暂停程序。

  • 查看调用堆栈 (Call Stack): 调用堆栈显示了当前函数是如何被调用的(即函数调用的层级关系)。这对于理解程序的控制流程以及追踪错误发生的路径非常有帮助。

  • 修改变量 (Modify Variables): 一些调试器允许你在程序运行时修改变量的值。这可以用于快速测试不同的场景或验证修复方案。

如果大家真的想用调试器,来实际调试查看虚函数的地址的话,可以参考文章:

C++虚函数与多态的底层原理探究-CSDN博客

C++多态(超级详细版)-CSDN博客

你可能感兴趣的:(c++,虚函数,虚函数表,多态,虚函数指针)