浅谈C++ 虚函数分析

 更新时间:2020年02月07日 09:12:08   作者:小胖西瓜  
这篇文章主要介绍了浅谈C++ 虚函数分析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

虚函数调用属于运行时多态,在类的继承关系中,通过父类指针来调用不同子类对象的同名方法,而产生不同的效果。

C++ 中的多态是通过晚绑定(对象构造时)来实现的。

用法

在函数之前声明关键字 virtual 表示这是一个虚函数,在函数后增加一个 = 0 表示这是一个纯虚函数,纯虚函数的类不能创建具体实例。

该示例作后文分析使用,一个包含纯虚函数的父类,一个重写了父类方法的子类,一个无继承的类。

struct Base {
  Base() : val(7777) {}
  virtual int fuck(int a) = 0;
  int val;
};

struct Der : public Base {
  Der() = default;
  int fuck(int a) override { return val + 4396; }
};

struct A {
  A() = default;
  void funny(int a) {}
};

int main() {
  Der der;
  Base *pbase = &der;
  pbase->fuck(sizeof(Der)); // 调用 Der::fuck(int a);

  A a;
  a.funny(sizeof(A)); // A::funny(int a);

  return 3;
}

实现

原来就了解虚函数是通过虚表的偏移来获取实际调用函数地址来实现的,但是在何时确定这个偏移和具体的偏移细节也没有说明,今儿个来探探究竟。

拿上面的代码进行反汇编获提取部分函数,main,Base::Base(), Base::fuck(), Der::Der(), Der::fuck, A::funny() 如下:

_ZN4BaseC2Ev:
.LFB1:
  .cfi_startproc
  pushq  %rbp
  .cfi_def_cfa_offset 16
  .cfi_offset 6, -16
  movq  %rsp, %rbp
  .cfi_def_cfa_register 6
  movq  %rdi, -8(%rbp)  // 还是 main 函数的栈帧 -32(%rpb) 的地址
  leaq  16+_ZTV4Base(%rip), %rdx // 关键点来了,取虚表偏移 16 的地址也就是 __cxa_pure_virtual,这里是没有意义的
  movq  -8(%rbp), %rax
  movq  %rdx, (%rax)   // 将 __cxa_pure_virtual 的地址存放在 地址rax 的内存中(这个例子中也就是main 函数的栈帧 -32(%rpb) 的地方), 
  movq  -8(%rbp), %rax  // 然后往后偏移 8 个字节,也就是跳过虚表指针,对成员变量 val 初始化。
  movl  $7777, 8(%rax)
  nop           // 注:上面是用这个示例中实际的地址带入的,实际上对于一个有的类的处理是一个通用逻辑的,构造函数传入的第一个参数 rdi 是 this 指针,由于有虚表存在的影响,这里会修改 this 指针所在地址的内容,也就是虚表的偏移地址(非起始地址)
  popq  %rbp
  .cfi_def_cfa 7, 8
  ret
  .cfi_endproc
.LFE1:
  .size  _ZN4BaseC2Ev, .-_ZN4BaseC2Ev
  .weak  _ZN4BaseC1Ev
  .set  _ZN4BaseC1Ev,_ZN4BaseC2Ev
  .section  .text._ZN3Der4fuckEi,"axG",@progbits,_ZN3Der4fuckEi,comdat
  .align 2
  .weak  _ZN3Der4fuckEi
  .type  _ZN3Der4fuckEi, @function
_ZN3Der4fuckEi:
.LFB3:
  .cfi_startproc
  pushq  %rbp
  .cfi_def_cfa_offset 16
  .cfi_offset 6, -16
  movq  %rsp, %rbp
  .cfi_def_cfa_register 6
  movq  %rdi, -8(%rbp)
  movl  %esi, -12(%rbp)
  movq  -8(%rbp), %rax
  movl  8(%rax), %eax  // 成员变量 val,val 是从 rdi 中偏移 8 字节取的值
  addl  $4396, %eax   // val + 4396
  popq  %rbp
  .cfi_def_cfa 7, 8
  ret
  .cfi_endproc
.LFE3:
  .size  _ZN3Der4fuckEi, .-_ZN3Der4fuckEi
  .section  .text._ZN1A5funnyEi,"axG",@progbits,_ZN1A5funnyEi,comdat
  .align 2
  .weak  _ZN1A5funnyEi
  .type  _ZN1A5funnyEi, @function
_ZN1A5funnyEi:
.LFB4:
  .cfi_startproc
  pushq  %rbp
  .cfi_def_cfa_offset 16
  .cfi_offset 6, -16
  movq  %rsp, %rbp
  .cfi_def_cfa_register 6
  movq  %rdi, -8(%rbp)
  movl  %esi, -12(%rbp)
  nop
  popq  %rbp
  .cfi_def_cfa 7, 8
  ret
  .cfi_endproc
.LFE4:
  .size  _ZN1A5funnyEi, .-_ZN1A5funnyEi
  .section  .text._ZN3DerC2Ev,"axG",@progbits,_ZN3DerC5Ev,comdat
  .align 2
  .weak  _ZN3DerC2Ev
  .type  _ZN3DerC2Ev, @function
_ZN3DerC2Ev:
.LFB7:
  .cfi_startproc
  pushq  %rbp
  .cfi_def_cfa_offset 16
  .cfi_offset 6, -16
  movq  %rsp, %rbp
  .cfi_def_cfa_register 6
  subq  $16, %rsp
  movq  %rdi, -8(%rbp)  // rdi 是取的 main 栈帧 -32(%rbp) 的地址
  movq  -8(%rbp), %rax
  movq  %rax, %rdi
  call  _ZN4BaseC2Ev   // Base 的构造函数,并且又把传进来的参数作为实参传进去了,这里跟踪进去
  leaq  16+_ZTV3Der(%rip), %rdx // 取虚表偏移16字节 _ZN3Der4fuckEi 的地址 
  movq  -8(%rbp), %rax
  movq  %rdx, (%rax)   // rax 在之前的 Base构造函数中是被修改了的,这里将继续修改内容,前一次的修改失效。
  nop
  leave
  .cfi_def_cfa 7, 8
  ret
  .cfi_endproc
.LFE7:
  .size  _ZN3DerC2Ev, .-_ZN3DerC2Ev
  .weak  _ZN3DerC1Ev
  .set  _ZN3DerC1Ev,_ZN3DerC2Ev
  .text
  .globl main
  .type  main, @function
main:
.LFB5:
  .cfi_startproc
  pushq  %rbp
  .cfi_def_cfa_offset 16
  .cfi_offset 6, -16
  movq  %rsp, %rbp
  .cfi_def_cfa_register 6
  subq  $48, %rsp
  leaq  -32(%rbp), %rax // 取 -32(%rbp) 的地址,对应 Base *pbase;
  movq  %rax, %rdi
  call  _ZN3DerC1Ev   // 调用了构造函数,并且以-32(%rbp) 的地址作为参数,这里跟踪进去
  leaq  -32(%rbp), %rax // -32(%rbp) 被修改,该内存中的内容为 Der 虚表的偏移地址 
  movq  %rax, -8(%rbp)
  movq  -8(%rbp), %rax
  movq  (%rax), %rax  // rax = M[rax],取出虚表偏移中的地址
  movq  (%rax), %rdx  // rdx = M[rax] , 取出虚表偏移的内容(也就是函数地址),算上上面这是做了两次解引用
  movq  -8(%rbp), %rax
  movl  $16, %esi    // sizeof(Der) = 16, 包含一个虚表指针和 int val;
  movq  %rax, %rdi   // 虚表偏移中的地址
  call  *%rdx      // 调用函数
  leaq  -33(%rbp), %rax
  movl  $1, %esi
  movq  %rax, %rdi
  call  _ZN1A5funnyEi  // 普通成员函数,实现简单
  movl  $3, %eax
  leave
  .cfi_def_cfa 7, 8
  ret
  .cfi_endproc
.LFE5:
  .size  main, .-main
  .weak  _ZTV3Der
  .section  .data.rel.ro.local._ZTV3Der,"awG",@progbits,_ZTV3Der,comdat
  .align 8
  .type  _ZTV3Der, @object
  .size  _ZTV3Der, 24
_ZTV3Der:
  .quad  0
  .quad  _ZTI3Der
  .quad  _ZN3Der4fuckEi // Der::fuck(int a);
  .weak  _ZTV4Base
  .section  .data.rel.ro._ZTV4Base,"awG",@progbits,_ZTV4Base,comdat
  .align 8
  .type  _ZTV4Base, @object
  .size  _ZTV4Base, 24
_ZTV4Base:
  .quad  0
  .quad  _ZTI4Base
  .quad  __cxa_pure_virtual // 纯虚函数,无对应符号表
  .weak  _ZTI3Der
  .section  .data.rel.ro._ZTI3Der,"awG",@progbits,_ZTI3Der,comdat
  .align 8
  .type  _ZTI3Der, @object
  .size  _ZTI3Der, 24

现在是一个纯虚函数,类中也没有虚析构函数,通过反汇编来看一些这个实现。

_ZTV3Der_ZTV4Base 是两个虚表,大小为 24, 8 字节对齐,分别对应 Der 子类和 Base 父类。虚表中偏移 16 字节(偏移大小可能和实现相关)为虚函数地址,每次构造函数的被调用的时候,会将该偏移地址存储到父类指针所在内存中,所以在上代码中看到,在 Base 和 Der 类的构函数中都出现了设置偏移地址的操作,但是子类构造函数会覆盖父类的修改。这样一来,实际的函数运行地址依赖构造函数,子类对象被构造就调用子类的方法,父类构造就调用父类的方法(非纯虚函数),实现了运行时多态。

增加一个虚函数后, 后面的虚函数地址就添加到虚表之中,如下

virtual void Base::shit() {}
void Der::shit() override {}

_ZTV3Der:
  .quad  0
  .quad  _ZTI3Der
  .quad  _ZN3Der4fuckEi
  .quad  _ZN3Der4shitEv
  .weak  _ZTV4Base
  .section  .data.rel.ro._ZTV4Base,"awG",@progbits,_ZTV4Base,comdat
  .align 8
  .type  _ZTV4Base, @object
  .size  _ZTV4Base, 32
_ZTV4Base:
  .quad  0
  .quad  _ZTI4Base
  .quad  __cxa_pure_virtual
  .quad  _ZN4Base4shitEv
  .weak  _ZTI3Der
  .section  .data.rel.ro._ZTI3Der,"awG",@progbits,_ZTI3Der,comdat
  .align 8
  .type  _ZTI3Der, @object
  .size  _ZTI3Der, 24

再调用另外一个虚函数就简单很多了,直接地址进行偏移(这里shit在fuck之后,所以+8)

 movq  -8(%rbp), %rax
  movq  (%rax), %rax
  addq  $8, %rax
  movq  (%rax), %rdx
  movq  -8(%rbp), %rax
  movq  %rax, %rdi
  call  *%rdx

简单画了一下虚函数运行的内存结构图

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • C++作用域与函数重载的实现

    C++作用域与函数重载的实现

    本文主要介绍了C++作用域与函数重载的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-02-02
  • C++ 互斥锁原理以及实际使用介绍

    C++ 互斥锁原理以及实际使用介绍

    本文主要聊一聊如何使用互斥锁以及都有哪几种方式实现互斥锁。实现互斥,可以有以下几种方式:互斥量(Mutex)、递归互斥量(Recursive Mutex)、读写锁(Read-Write Lock)、条件变量(Condition Variable)。感兴趣的同学可以参考一下
    2023-04-04
  • C语言实现字符转unix时间戳的简单实例

    C语言实现字符转unix时间戳的简单实例

    下面小编就为大家带来一篇C语言实现字符转unix时间戳的简单实例。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-06-06
  • C++ 中

    C++ 中"emplace_back" 与 "push_back" 的区别

    这篇文章主要介绍了C++ 中"emplace_back" 与 "push_back" 的区别的相关资料,需要的朋友可以参考下
    2017-04-04
  • c++中map的基本用法和嵌套用法实例分析

    c++中map的基本用法和嵌套用法实例分析

    这篇文章主要介绍了c++中map的基本用法和嵌套用法,以实例形式分析了map容器的基本使用技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-01-01
  • C语言选择、循环、函数、数组与操作符

    C语言选择、循环、函数、数组与操作符

    这篇文章主要介绍了C语言选择、循环、函数、数组与操作符,文章基于C语言展开对主题的详细介绍,下文内容需要的小伙伴可以参考一下
    2022-04-04
  • C语言实现静态版通讯录的代码分享

    C语言实现静态版通讯录的代码分享

    这篇文章主要为大家详细介绍了如何利用C语言实现一个简单的静态版通讯录,主要运用了结构体,一维数组,函数,分支与循环语句等等知识,需要的可以参考一下
    2023-01-01
  • C++实现LeetCode(112.二叉树的路径和)

    C++实现LeetCode(112.二叉树的路径和)

    这篇文章主要介绍了C++实现LeetCode(112.二叉树的路径和),本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-07-07
  • 一文读懂C++中Protobuf

    一文读懂C++中Protobuf

    Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化、或者说序列化,本文详解了Protobuf的使用,具有一定的参考价值,感兴趣的可以了解一下
    2023-05-05
  • Win10+VS2017新CUDA项目配置教程

    Win10+VS2017新CUDA项目配置教程

    这篇文章主要为大家详细介绍了Win10+VS2017新CUDA项目配置教程,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-09-09

最新评论