重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
这篇文章给大家分享的是有关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++中虚函数怎么用”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!