c 中的虚函数如何用-mile米乐体育
c 中的虚函数如何用
本篇内容主要讲解“c 中的虚函数如何用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“c 中的虚函数如何用”吧!
虚函数调用属于运行时多态,在类的继承关系中,通过父类指针来调用不同子类对象的同名方法,而产生不同的效果。
c 中的多态是通过晚绑定(对象构造时)来实现的。
用法
在函数之前声明关键字virtual
表示这是一个虚函数,在函数后增加一个= 0
表示这是一个纯虚函数,纯虚函数的类不能创建具体实例。
该示例作后文分析使用,一个包含纯虚函数的父类,一个重写了父类方法的子类,一个无继承的类。
structbase{base():val(7777){}virtualintfuck(inta)=0;intval;};structder:publicbase{der()=default;intfuck(inta)override{returnval 4396;}};structa{a()=default;voidfunny(inta){}};intmain(){derder;base*pbase=&der;pbase->fuck(sizeof(der));//调用der::fuck(inta);aa;a.funny(sizeof(a));//a::funny(inta);return3;}
实现
原来就了解虚函数是通过虚表的偏移来获取实际调用函数地址来实现的,但是在何时确定这个偏移和具体的偏移细节也没有说明,今儿个来探探究竟。
拿上面的代码进行反汇编获提取部分函数,main,base::base(), base::fuck(), der::der(), der::fuck, a::funny() 如下:
_zn4basec2ev:.lfb1:.cfi_startprocpushq%rbp.cfi_def_cfa_offset16.cfi_offset6,-16movq%rsp,%rbp.cfi_def_cfa_register6movq%rdi,-8(%rbp)//还是main函数的栈帧-32(%rpb)的地址leaq16 _ztv4base(%rip),%rdx//关键点来了,取虚表偏移16的地址也就是__cxa_pure_virtual,这里是没有意义的movq-8(%rbp),%raxmovq%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_cfa7,8ret.cfi_endproc.lfe1:.size_zn4basec2ev,.-_zn4basec2ev.weak_zn4basec1ev.set_zn4basec1ev,_zn4basec2ev.section.text._zn3der4fuckei,"axg",@progbits,_zn3der4fuckei,comdat.align2.weak_zn3der4fuckei.type_zn3der4fuckei,@function_zn3der4fuckei:.lfb3:.cfi_startprocpushq%rbp.cfi_def_cfa_offset16.cfi_offset6,-16movq%rsp,%rbp.cfi_def_cfa_register6movq%rdi,-8(%rbp)movl%esi,-12(%rbp)movq-8(%rbp),%raxmovl8(%rax),�x//成员变量val,val是从rdi中偏移8字节取的值addl$4396,�x//val 4396popq%rbp.cfi_def_cfa7,8ret.cfi_endproc.lfe3:.size_zn3der4fuckei,.-_zn3der4fuckei.section.text._zn1a5funnyei,"axg",@progbits,_zn1a5funnyei,comdat.align2.weak_zn1a5funnyei.type_zn1a5funnyei,@function_zn1a5funnyei:.lfb4:.cfi_startprocpushq%rbp.cfi_def_cfa_offset16.cfi_offset6,-16movq%rsp,%rbp.cfi_def_cfa_register6movq%rdi,-8(%rbp)movl%esi,-12(%rbp)noppopq%rbp.cfi_def_cfa7,8ret.cfi_endproc.lfe4:.size_zn1a5funnyei,.-_zn1a5funnyei.section.text._zn3derc2ev,"axg",@progbits,_zn3derc5ev,comdat.align2.weak_zn3derc2ev.type_zn3derc2ev,@function_zn3derc2ev:.lfb7:.cfi_startprocpushq%rbp.cfi_def_cfa_offset16.cfi_offset6,-16movq%rsp,%rbp.cfi_def_cfa_register6subq$16,%rspmovq%rdi,-8(%rbp)//rdi是取的main栈帧-32(%rbp)的地址movq-8(%rbp),%raxmovq%rax,%rdicall_zn4basec2ev//base的构造函数,并且又把传进来的参数作为实参传进去了,这里跟踪进去leaq16 _ztv3der(%rip),%rdx//取虚表偏移16字节_zn3der4fuckei的地址movq-8(%rbp),%raxmovq%rdx,(%rax)//rax在之前的base构造函数中是被修改了的,这里将继续修改内容,前一次的修改失效。nopleave.cfi_def_cfa7,8ret.cfi_endproc.lfe7:.size_zn3derc2ev,.-_zn3derc2ev.weak_zn3derc1ev.set_zn3derc1ev,_zn3derc2ev.text.globlmain.typemain,@functionmain:.lfb5:.cfi_startprocpushq%rbp.cfi_def_cfa_offset16.cfi_offset6,-16movq%rsp,%rbp.cfi_def_cfa_register6subq$48,%rspleaq-32(%rbp),%rax//取-32(%rbp)的地址,对应base*pbase;movq%rax,%rdicall_zn3derc1ev//调用了构造函数,并且以-32(%rbp)的地址作为参数,这里跟踪进去leaq-32(%rbp),%rax//-32(%rbp)被修改,该内存中的内容为der虚表的偏移地址movq%rax,-8(%rbp)movq-8(%rbp),%raxmovq(%rax),%rax//rax=m[rax],取出虚表偏移中的地址movq(%rax),%rdx//rdx=m[rax],取出虚表偏移的内容(也就是函数地址),算上上面这是做了两次解引用movq-8(%rbp),%raxmovl$16,%esi//sizeof(der)=16,包含一个虚表指针和intval;movq%rax,%rdi//虚表偏移中的地址call*%rdx//调用函数leaq-33(%rbp),%raxmovl$1,%esimovq%rax,%rdicall_zn1a5funnyei//普通成员函数,实现简单movl$3,�xleave.cfi_def_cfa7,8ret.cfi_endproc.lfe5:.sizemain,.-main.weak_ztv3der.section.data.rel.ro.local._ztv3der,"awg",@progbits,_ztv3der,comdat.align8.type_ztv3der,@object.size_ztv3der,24_ztv3der:.quad0.quad_zti3der.quad_zn3der4fuckei//der::fuck(inta);.weak_ztv4base.section.data.rel.ro._ztv4base,"awg",@progbits,_ztv4base,comdat.align8.type_ztv4base,@object.size_ztv4base,24_ztv4base:.quad0.quad_zti4base.quad__cxa_pure_virtual//纯虚函数,无对应符号表.weak_zti3der.section.data.rel.ro._zti3der,"awg",@progbits,_zti3der,comdat.align8.type_zti3der,@object.size_zti3der,24
现在是一个纯虚函数,类中也没有虚析构函数,通过反汇编来看一些这个实现。
_ztv3der
和_ztv4base
是两个虚表,大小为 24, 8 字节对齐,分别对应 der 子类和 base 父类。虚表中偏移 16 字节(偏移大小可能和实现相关)为虚函数地址,每次构造函数的被调用的时候,会将该偏移地址存储到父类指针所在内存中,所以在上代码中看到,在 base 和 der 类的构函数中都出现了设置偏移地址的操作,但是子类构造函数会覆盖父类的修改。这样一来,实际的函数运行地址依赖构造函数,子类对象被构造就调用子类的方法,父类构造就调用父类的方法(非纯虚函数),实现了运行时多态。
增加一个虚函数后, 后面的虚函数地址就添加到虚表之中,如下
virtualvoidbase::shit(){}voidder::shit()override{}_ztv3der:.quad0.quad_zti3der.quad_zn3der4fuckei.quad_zn3der4shitev.weak_ztv4base.section.data.rel.ro._ztv4base,"awg",@progbits,_ztv4base,comdat.align8.type_ztv4base,@object.size_ztv4base,32_ztv4base:.quad0.quad_zti4base.quad__cxa_pure_virtual.quad_zn4base4shitev.weak_zti3der.section.data.rel.ro._zti3der,"awg",@progbits,_zti3der,comdat.align8.type_zti3der,@object.size_zti3der,24
再调用另外一个虚函数就简单很多了,直接地址进行偏移(这里shit在fuck之后,所以 8)
movq-8(%rbp),%raxmovq(%rax),%raxaddq$8,%raxmovq(%rax),%rdxmovq-8(%rbp),%raxmovq%rax,%rdicall*%rdx
简单画了一下虚函数运行的内存结构图
到此,相信大家对“c 中的虚函数如何用”有了更深的了解,不妨来实际操作一番吧!这里是恰卡编程网网站,更多相关内容可以进入相关频道进行查询,关注mile米乐体育,继续学习!