深入探索 C++ 多态 ① - 虚函数调用链路

news2024/9/19 11:10:02

前言

最近翻阅侯捷先生的两本书:(翻译)《深度探索 C++ 对象模型》 和 《C++ 虚拟与多态》,获益良多。

要理解多态的工作原理,得理解这几个知识点的关系:虚函数虚函数表虚函数指针、以及对象的 内存布局

  • 深入探索 C++ 多态 ① - 虚函数调用链路
  • 深入探索 C++ 多态 ② - 继承关系
  • 深入探索 C++ 多态 ③ - 虚析构

1. 概述

1.1. 概念

本章主要探索 C++ 动态多态,我们先了解一下它的一些相关概念:

  • 多态 是 C++ 中的一个重要概念,它允许在派生类中 重写 基类中的函数,并以不同的方式处理相同的数据类型;多态的实现依赖于 虚函数动态绑定

  • 虚函数 是一种特殊的成员函数,它允许在派生类中重写基类中的函数。当一个函数被声明为虚函数时,编译器会在该类的虚函数表中添加一个条目,该条目指向该虚函数的地址。如果一个类继承了另一个类的虚函数,那么它将继承该类的虚函数表,并在其中添加自己的虚函数。

  • 虚函数表 是一个包含虚函数地址的表格,每个类都有一个虚函数表。虚函数表中的每个条目都是一个指向虚函数的指针。当一个类包含虚函数时,编译器会在该类的虚函数表中添加一个条目,该条目指向该虚函数的地址。如果一个类继承了另一个类的虚函数,那么它将继承该类的虚函数表,并在其中添加自己的虚函数。

  • 虚函数指针 是一个指向虚函数表的指针,它存储在每个对象的内存中。当一个对象被创建时,它的虚函数指针被初始化为指向该类的虚函数表。当一个虚函数被调用时,编译器会使用虚函数指针来查找该函数在虚函数表中的地址,并调用该函数。

  • 动态绑定 是一种在运行时确定函数调用的机制。当一个函数被声明为虚函数时,编译器会使用动态绑定来确定该函数的实际地址。当一个虚函数被调用时,编译器会使用虚函数指针来查找该函数在虚函数表中的地址,并调用该函数。

部分文字来源:ChatGPT


1.2. 实例

概念比较抽象,写个 demo,配合图片凑合着理解~

在这里插入图片描述

  • 源码。
/* g++ -std='c++11' test.cpp -o t && ./t */
#include <iostream>
#include <memory>

class Model {
   public:
    virtual void face() { std::cout << "model's face!" << std::endl; }
};

class Gril : public Model {
   public:
    virtual void face() { std::cout << "girl's face!" << std::endl; }
};

class Man : public Model {
   public:
    virtual void face() { std::cout << "man's face!" << std::endl; }
};

class Boy : public Model {
   public:
    virtual void face() { std::cout << "boy's face!" << std::endl; }
};

void take_photo(const std::unique_ptr<Model>& m) { m->face(); }

int main() {
    auto model = std::unique_ptr<Model>(new Model);
    auto girl = std::unique_ptr<Model>(new Gril);
    auto man = std::unique_ptr<Model>(new Man);
    auto boy = std::unique_ptr<Model>(new Boy);
    take_photo(model);
    take_photo(girl);
    take_photo(man);
    take_photo(boy);
    return 0;
}
  • 运行结果。
model's face!
girl's face!
man's face!
boy's face!

2. 工作环境

2.1. 系统

# cat /etc/redhat-release
CentOS Linux release 7.9.2009 (Core)
# cat /proc/version
Linux version 3.10.0-1127.19.1.el7.x86_64 (mockbuild@kbuilder.bsys.centos.org) 
(gcc version 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC) )

2.2. 工具

文章分析多态原理,会用到下面的一些工具:

工具描述
gdbgdb 是 GNU 调试器的缩写,是一个用于调试程序的工具。
-fdump-class-hierarchy-fdump-class-hierarchy 是 GCC 的一个编译器选项,用于在编译过程中生成类层次结构的信息。它会将类的继承关系以文本形式输出到一个文件中,以便开发人员可以查看和分析类之间的关系。——这个选项在调试和理解代码中的类继承关系时非常有用。
c++filtc++filt 是一个用于解析 C++ 符号的工具。它可以将由 C++ 编译器生成的符号进行反解析,以便更容易理解和阅读。它可以将由C++编译器生成的符号转换为可读的函数名、类名和变量名。
objectdumpobjectdump 是一个用于分析目标文件的工具。它可以显示目标文件的各个节(section)的内容,包括代码、数据、符号表等。它还可以反汇编目标文件的机器码,以便更深入地了解程序的执行过程。

部分文字来源:ChatGPT


3. 多态重要数据结构

我在调试 dynamic_cast 内部源码时,发现一些数据结构:__class_type_infovtable_prefix,它们有利于我更好地理解多态的工作原理。

在这里插入图片描述

调试方式请参考:《(ubuntu) vscode + gdb 调试 c++》


3.1. 类型信息

type_info 是一个类的类型信息的数据结构,用于在运行时获取对象的类型信息;多态工作机制根据不同应用场景,从 type_info 派生各个类型信息结构类。

  • 基础类型信息结构。
/* /usr/include/c++/4.8.2/typeinfo */
// The type_info class describes type information generated by an implementation.
class type_info {
 protected:
    const char* __name;
};

/* /usr/include/c++/4.8.2/cxxabi.h */
// Type information for a class.
class __class_type_info : public std::type_info {
 public:
    ...
};
  • 单一继承类型信息结构。
/* /usr/include/c++/4.8.2/cxxabi.h */
// Type information for a class with a single non-virtual base.
class __si_class_type_info : public __class_type_info {
   public:
    const __class_type_info *__base_type;
    ...
};
  • 多重继承或虚拟继承类型信息结构。
/* /usr/include/c++/4.8.2/cxxabi.h */
// Helper class for __vmi_class_type.
class __base_class_type_info {
   public:
    const __class_type_info *__base_type;  // Base class type.
#ifdef _GLIBCXX_LLP64
    long long __offset_flags;  // Offset and info.
#else
    long __offset_flags;  // Offset and info.
#endif
    ...
};

// Type information for a class with multiple and/or virtual bases.
class __vmi_class_type_info : public __class_type_info {
   public:
    unsigned int __flags;       // Details about the class hierarchy.
    unsigned int __base_count;  // Number of direct bases.

    // The array of bases uses the trailing array struct hack so this
    // class is not constructable with a normal constructor. It is
    // internally generated by the compiler.
    __base_class_type_info __base_info[1];  // Array of bases.
    ...
};

3.2. 虚表描述结构

vtable_prefix:虚表描述结构,用于表示虚函数表的前缀。一个对象可能有多个虚指针,多个虚表描述结构;对象的每个虚指针指向对应的 vtable_prefix.origin

  1. whole_object:我认为改为:top_offset 会更贴切一点。对象内存中的当前虚指针位置,离顶端的偏移位置,因为对象有可能有多个虚表,通过偏移量可以找到对象内存布局上对应的虚指针。
  2. whole_type: 类的类型信息。
  3. origin:虚指针指向虚表的位置。
/* /usr/src/debug/gcc-4.8.5-20150702/libstdc++-v3/libsupc++/tinfo.h */
// Initial part of a vtable, this structure is used with offsetof, so we don't
// have to keep alignments consistent manually.
struct vtable_prefix {
    // Offset to most derived object.
    ptrdiff_t whole_object;

    // Additional padding if necessary.
#ifdef _GLIBCXX_VTABLE_PADDING
    ptrdiff_t padding1;
#endif

    // Pointer to most derived type_info.
    const __class_type_info *whole_type;

    // Additional padding if necessary.
#ifdef _GLIBCXX_VTABLE_PADDING
    ptrdiff_t padding2;
#endif

    // What a class's vptr points to.
    const void *origin;
};

我们可以参考下一章将会讲到的多重继承多态对象的内存布局,去理解虚表描述结构。

在这里插入图片描述


4. 虚函数调用链路

C++ 多态是一个比较复杂的特性,从易到难,我们先了解一下 无继承关系 的多态类对象的虚函数调用工作流程。

  • 链路。
this -> vptr -> vbtl -> virtual function
  • 测试源码。
// g++ -g -O0 -std=c++11 -fdump-class-hierarchy test_virtual.cpp -o t
#include <iostream>

class A {
   public:
    int m_a = 0;
    virtual void vfuncA1() {}
    virtual void vfuncA2() {}
};

int main(int argc, char** argv) {    
    A* a = new A;
    a->vfuncA2();
    return 0;
}
  • 汇编源码。通过汇编代码观察虚函数的调用流程:
int main(int argc, char** argv) {
  ;...
    A* a = new A;
  ;...
  40071d:       e8 8e 00 00 00          callq  4007b0 <_ZN1AC1Ev>
  ; 将 a 的对象(this)指针压栈到 -0x18(%rbp)400722:       48 89 5d e8             mov    %rbx,-0x18(%rbp)
    a->vfuncA2();
  ; 找到虚指针。
  400726:       48 8b 45 e8             mov    -0x18(%rbp),%rax
  ; 通过虚指针,找到虚表保存虚函数的起始位置。
  40072a:       48 8b 00                mov    (%rax),%rax
  ; 通过上面起始位置进行偏移,找到虚表存放某个虚函数的地址。
  40072d:       48 83 c0 08             add    $0x8,%rax
  ; 找到对应的虚函数。
  400731:       48 8b 00                mov    (%rax),%rax
  ; 通过寄存器传递 a 指针作为参数,传给虚函数使用
  400734:       48 8b 55 e8             mov    -0x18(%rbp),%rdx
  400738:       48 89 d7                mov    %rdx,%rdi
  ; 调用虚函数
  40073b:       ff d0                   callq  *%rax
    return 0;
  ;...
}
  1. 找到虚指针,a 对象的内存首位存放的是指向虚表的 虚指针 地址。
  2. 通过虚指针,找到虚表保存虚函数的起始位置。
  3. 通过上面虚表保存虚函数的起始位置进行偏移,找到虚表存放对应虚函数的地址,从而找到对应的虚函数。
  4. 将 a(this)指针写入 rdi 寄存器,作为参数传递给虚函数调用。
  5. call 命令调用虚函数(A::vfuncA2(this))。
    在这里插入图片描述

5. 内存布局

请问 虚函数表虚函数 分别在内存的哪个数据分区?我们可以使用 objdump 工具进行分析。

使用上面的测试 demo 进行分析:虚函数表在内存的 文字常量区,虚函数在内存的 程序代码区

参考:程序变量内存分布(Linux),深入探索 C++ 多态 ② - 继承关系,深入探索 C++ 多态 ③ - 虚析构

  • 数据分区。
区域描述变量类型
stack栈区临时变量
heap堆区malloc 分配空间的变量
.data,.bss全局数据区全局变量/静态变量
.rodata文字常量区只读数据,常量等
.text程序代码区程序代码
  • objdump 工具使用。
# 编译测试代码。
g++ -std=c++11 test.cpp -o test

# 使用 objdump 导出执行文件的信息。
objdump -CdStT test > asm.log

# 获取程序代码区信息。
cat asm.log| grep '\.text'

0000000000400610 l    d .text  0000000000000000    .text
0000000000400610 g    F .text  0000000000000000    _start
00000000004007b0 w    F .text  0000000000000020    A::A()
# 虚函数。
000000000040079c w    F .text  000000000000000a    A::vfuncA1()
00000000004007a6 w    F .text  000000000000000a    A::vfuncA2()
00000000004007d0 g    F .text  0000000000000065    __libc_csu_init
00000000004007b0 w    F .text  0000000000000020    A::A()
00000000004006fd g    F .text  000000000000004c    main

# 获取程序文字常量区信息。
cat asm.log| grep '\.rodata'
0000000000400860 l    d .rodata    0000000000000000    .rodata
# 虚表。
0000000000400880 w    O .rodata    0000000000000020    vtable for A
00000000004008a0 w    O .rodata    0000000000000003    typeinfo name for A
00000000004008b0 w    O .rodata    0000000000000010    typeinfo for A

6. 参考

  • 《深度探索 C++ 对象模型》
  • 《C++ 虚拟与多态》
  • 多态及其基本原理
  • C++ 多态的实现原理分析
  • 再议内存布局
  • C++:从技术实现角度聊聊RTTI
  • c++对象内存布局
  • C++ 对象的内存布局(上)
  • C++ 对象的内存布局(下)
  • 如何在vscode中编写汇编语言并在终端进行调试(保姆级别)
  • C++ Virtual Table Tables(VTT)
  • godbolt.org
  • What is the VTT for a class?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1153863.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【骑行贝丘渔场】一场与海的邂逅,一段难忘的旅程

在这个渐凉的秋日&#xff0c;我们校长骑行队一行人骑着自行车&#xff0c;从大观公园门口出发&#xff0c;开始了一段别开生面的海滩之旅。沿途穿越草海隧道湿地公园、迎海路、海埂公园西门&#xff08;第二集合点&#xff09;、宝丰湿地公园、斗南湿地公园、蓝光城&#xff0…

代理模式代理模式

目录 1、使用场景 2、静态代理 3、动态代理 JDK动态代理 CGlib 动态代理实现 1、使用场景 使用代理模式主要有两个目的&#xff1a;一是保护目标对象&#xff0c;二是增强目标对象。 2、静态代理 NO.1 抽象接口&#xff1a;定义视频播放器接口Player public interface P…

大数据之路-日志采集

数据采集作为大数据体系中的第一环节&#xff0c;对如何全面、高性能、规范完成海量数据的采集&#xff0c;并将其传输到大数据平台。 1.浏览器的页面日志采集 1.1 页面浏览日志采集流程 页面浏览日志是最基础的互联网日志&#xff0c;其中页面浏览量&#xff08;PageView&am…

短期经济波动:均衡国民收入决定理论(一)

宏观经济学讲义 10 短期经济波动&#xff1a;均衡国民收入决定理论(一) 文章目录 10 短期经济波动&#xff1a;均衡国民收入决定理论(一)[toc]1 均衡国民收入决定1.1 均衡国民收入决定的不同理论1.2 两部门经济&#xff1a;有效需求原理和框架1.2.1 模型假设1.2.2 模型推导1.2…

【零基础抓包】Fiddler超详细教学(一)

​Fiddler 1、什么是 Fiddler? Fiddler 是一个 HTTP 协议调试代理工具&#xff0c;它能够记录并检查所有你的电脑和互联网之间的 HTTP 通讯。Fiddler 提供了电脑端、移动端的抓包、包括 http 协议和 https 协议都可以捕获到报文并进行分析&#xff1b;可以设置断点调试、截取…

期中成绩发布一对一查询

亲爱的老师们&#xff01;我知道您在期中考试后忙碌而又紧张&#xff0c;不仅要准备试卷分析&#xff0c;还要面对学生和家长的询问。现在&#xff0c;我为您带来了一款一对一成绩查询系统&#xff0c;让您的工作变得更简单、更高效&#xff01; 什么是成绩查询系统&#xff1f…

一个小妙招从Prompt菜鸟秒变专家!加州大学提出PromptAgent,帮你高效使用ChatGPT!

夕小瑶科技说 原创 作者 | 谢年年、王二狗 有了ChatGPT、GPT4之后&#xff0c;我们的工作学习效率得到大大提升&#xff08;特别在凑字数方面୧(๑•̀◡•́๑)૭&#xff09;。 作为一个工具&#xff0c;有人觉得好用&#xff0c;自然也有人觉得难用。 要把大模型用得6&am…

518抽奖软件,为什么说比别的抽奖软件更美观精美?

518抽奖软件简介 518抽奖软件&#xff0c;518我要发&#xff0c;超好用的年会抽奖软件&#xff0c;简约设计风格。 包含文字号码抽奖、照片抽奖两种模式&#xff0c;支持姓名抽奖、号码抽奖、数字抽奖、照片抽奖。(www.518cj.net) 精致美观功能 字体平滑无锯齿图片放大后清晰…

SCADA在污水和供水系统解决方案

1. 引言 随着城市化的不断发展&#xff0c;污水和供水系统的管理变得越来越重要。为了提高运营效率和监控系统状态&#xff0c;许多污水处理厂开始使用SCADA系统。 SCADA系统具有实时数据采集、监控和控制功能&#xff0c;可以帮助污水处理厂运营人员实时了解系统的运行情况&…

Google Play优化之如何增强应用的吸引力

视觉元素&#xff0c;在增强应用在Google Play上的吸引力方面发挥着巨大作用。应用程序的图标、屏幕截图和宣传视频&#xff0c;构成了应用程序商店优化过程的关键部分。 1、图标是用户遇到的第一个视觉元素。 精心设计的图标可以有效地代表应用程序的用途&#xff0c;可以显著…

LeetCode | 26. 删除有序数组中的重复项

LeetCode | 26. 删除有序数组中的重复项 OJ链接 这里的非递增是什么意思&#xff1f; 就是反过来的&#xff0c;递减&#xff0c;不能说是乱序~~也就是后一个比前一个小也就是和非递减等价&#xff0c;后一个比前一个大~~ 所以非递增和非严格递增是不一样的~~ 这里本质上的…

亚马逊云科技为奇点云打造全面、安全、可扩展的数据分析解决方案

刘莹奇点云联合创始人、COO&#xff1a;伴随云计算的发展&#xff0c;数据技术也在快速迭代&#xff0c;成为客户迈入DT时代、实现高质量发展的关键引擎。我们很高兴能和云计算领域的领跑者亚马逊云科技一同&#xff0c;不断为客户提供安全可靠的产品与专业的服务。 超过1500家…

外汇天眼:GOMAX──假网友热心教投资,高返利活动骗入金

在通讯科技如此发达的今日&#xff0c;人们愈来愈习惯透过网路交友&#xff0c;寻找志同道合的伙伴&#xff0c;甚至发展一段亲密关系。 然而&#xff0c;近年来假交友诈骗十分猖獗&#xff0c;至今已造成许多民众极大的财务损失&#xff0c;成为无法忽视的社会问题。 不久前&a…

kubectl资源管理命令---声明式

目录 一、yaml和json介绍 1、yuml语言介绍 2、k8s支持的文件格式 二、声明式对象管理 1、deployment.yaml文件详解 2、Pod yaml文件详解 3、Service yaml文件详解 三、编写资源配置清单 1、 编写yaml文件 2、 创建并查看pod资源 3、创建service服务对外提供访问并测试…

从零开始学习Java:如何成为一名Java开发者并找到工作

文章目录 &#x1f31f; JavaSE&#x1f31f; JavaWeb&#x1f31f; 多线程&#x1f31f; 主流框架&#x1f31f; Redis缓存&#x1f31f; 消息中间件&#x1f31f; 全文搜索&#x1f31f; MySQL&#x1f31f; Mongodb&#x1f31f; 开发工具&#x1f31f; 模板引擎&#x1f31…

2023年是5G-A标准制定关键年 华为实现5G-A重大突破

5G商用四年&#xff0c;2023年5G应用项目已经达到10万个&#xff0c;5G向千行百业渗透的同时&#xff0c;也在向5G-Advanced&#xff08;下简称5G-A&#xff09;演进。 10月20日&#xff0c;在工业和信息化部主办的2023年中国5G发展大会上&#xff0c;由IMT-2020&#xff08;5G…

Yusi技术资讯博客wordpress模板

Yusi技术资讯博客wordpress模板&#xff0c;从第一感觉看上去&#xff0c;两栏结构直接将网站的内容展现&#xff0c;以红白灰色调搭配&#xff0c;一种低调协调的风格&#xff0c;喜欢该wordpress主题的朋友可以下载试试。 下载地址&#xff1a;https://bbs.csdn.net/topics/…

在Instagram进行kol营销之后要如何去后续维护

在网红经济盛行的如今&#xff0c;学会利用网红的影响力来推广品牌是营销中很重要的一个形式。企业要把握这个风口&#xff0c;承接这些网红带来的流量之后&#xff0c;牢牢掌握&#xff0c;及时开展后续的营销活动&#xff0c;这样才能实现高转化&#xff0c;成为网红经济下的…

python按照windows或者Ubuntu的文件夹中文件的顺序读取文件

摘要 在使用python读取文件的时候&#xff0c;发现python读取文件的顺序和文件夹中的顺序不一致&#xff0c;这时候应该怎么办呢&#xff1f; 解决方法 使用os_sorted库&#xff0c;安装方式&#xff1a; pip install natsort使用方法&#xff1a; from natsort import os…

【JavaSE专栏56】Java面向对象编程:深入理解类、对象、属性和方法的核心概念

Java面向对象编程&#xff1a;深入理解类、对象、属性和方法的核心概念 &#x1f4da;&#x1f9ec;&#x1f4bb; 摘要引言1. Java中的类和对象 &#x1f4da;&#x1f9ec;1.1 什么是Java类和对象&#xff1f; &#x1f914;1.2 类和对象在面向对象编程中的作用 &#x1f3af…