千家信息网

C++中的虚函数如何用

发表于:2025-11-08 作者:千家信息网编辑
千家信息网最后更新 2025年11月08日,本篇内容主要讲解"C++中的虚函数如何用",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"C++中的虚函数如何用"吧!虚函数调用属于运行时多态,在类的继承关系
千家信息网最后更新 2025年11月08日C++中的虚函数如何用

本篇内容主要讲解"C++中的虚函数如何用",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"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, @functionmain:.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++ 内存 运行 多态 参数 对象 成员 这是 不同 关键 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 app开发九零部落软件开发 数据库技术学习 链接数据库报10060 算法工程师和软件开发 中国网络安全公司三零所 英国学术论文数据库 山西it软件开发欢迎来电 狗爹服务器带宽 北京高亿联合网络技术有限公司 华为服务器自带是什么系统 网络安全管理员技能大赛 华为g服务器与ai交汇 打发票时提示服务器无法创建对象 买个游戏服务器主机吗 软件开发类人才去哪找 数据库表中插入新字段 证券行业图数据库的应用 SNP数据库都有什么 网络安全手抄报精美优秀作品 linux怎么远程另一台服务器 广州传视网络技术有限公司 北京易程商旅网络技术有限公司 航海王拼图软件开发 内网服务器开放了很多端口安全吗 欧拉系统服务器怎么看身份鉴别 一个网站可以连接几个数据库 重庆万盛网上生鲜软件开发 ibeat下载出现服务器错误 海光服务器cpu上市公司 北京沃顿在线网络安全
0