amazzzzzing

导航

虚函数

虚函数

虚函数表

示例

// code of virtual function
// filename: test.cpp
#include <stdio.h>

class A
{
public:
	virtual ~A() {}
	void draw(){draw_imp();}
protected:
	virtual void draw_imp(){}
};

class B : public A
{
public:
protected:
	void draw_imp() override{}
};

int main()
{
	A *a = new B();
	a->draw();
	delete a;
	return 0;
}
g++ test.cpp --dump-lang-class
cat a-*

得到信息为

Vtable for A
A::_ZTV1A: 5 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1A)
16    (int (*)(...))A::~A
24    (int (*)(...))A::~A
32    (int (*)(...))A::draw_imp

Class A
   size=8 align=8
   base size=8 base align=8
A (0x0x7f223519eb40) 0 nearly-empty
    vptr=((& A::_ZTV1A) + 16)

Vtable for B
B::_ZTV1B: 5 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1B)
16    (int (*)(...))B::~B
24    (int (*)(...))B::~B
32    (int (*)(...))B::draw_imp

Class B
   size=8 align=8
   base size=8 base align=8
B (0x0x7f2235043270) 0 nearly-empty
    vptr=((& B::_ZTV1B) + 16)
A (0x0x7f223519ef00) 0 nearly-empty
      primary-for B (0x0x7f2235043270)

可以看到,两个存在继承关系的类,其虚函数表中的函数(析构函数和draw)分别为各自类的实现;

注:虚函数表中一些特殊的结构,比如16字节偏移、对齐、两个析构函数,这些都是ABI的要求,不必关注;

虚函数的原理

汇编分析

g++ test.cpp -S
cat test.s

摘录其中一部分分析虚函数的调用过程(移除了部分特殊指示代码):


_ZTV1A:							# A::vtable
	.quad	0
	.quad	_ZTI1A
	.quad	_ZN1AD1Ev
	.quad	_ZN1AD0Ev
	.quad	_ZN1A8draw_impEv

_ZTV1B:							# B::vtable
	.quad	0
	.quad	_ZTI1B
	.quad	_ZN1BD1Ev
	.quad	_ZN1BD0Ev
	.quad	_ZN1B8draw_impEv

_ZN1A4drawEv:                   # A::draw()
    pushq	%rbp                # 栈基址
    movq	%rsp, %rbp          # 新的栈基址
    subq	$16, %rsp   
	subq	$16, %rsp           # 栈共增长32Byte
	movq	%rdi, -8(%rbp)      # this --> 栈第一个8字节
	movq	-8(%rbp), %rax      # this --> rax
	movq	(%rax), %rax        # *rax --> rax :this的首部就是虚函数表的指针地址,8字节)
	addq	$16, %rax           # rax偏移16字节,考虑到虚表本身偏移16,即总共偏移为32,指向draw_imp
	movq	(%rax), %rdx        # *rax --> rax :获取函数地址(draw_imp)
	movq	-8(%rbp), %rax      # 
	movq	%rax, %rdi
	call	*%rdx               # 调用(draw_imp)
	nop
	leave
	ret

_ZN1AC2Ev:                          # A::A()
	endbr64
	pushq	%rbp
	movq	%rsp, %rbp
	movq	%rdi, -8(%rbp)          # this指针放到栈的第一个8字节
	leaq	16+_ZTV1A(%rip), %rdx   # rdx = 虚表首部地址 + 16
	movq	-8(%rbp), %rax          # rax = this
	movq	%rdx, (%rax)            # *this = rdx ,即类A的虚表指针放到对象的第一个8字节
	nop
	popq	%rbp
	ret
.LFE10:
	.set	_ZN1AC1Ev,_ZN1AC2Ev

_ZN1BC2Ev:                      # B::B()
	endbr64
	pushq	%rbp
	movq	%rsp, %rbp
	subq	$16, %rsp
	movq	%rdi, -8(%rbp)      # this指针放到栈的第一个8字节
	movq	-8(%rbp), %rax      # rax = this
	movq	%rax, %rdi          # rdi = this
	call	_ZN1AC2Ev           # A::A()
	leaq	16+_ZTV1B(%rip), %rdx   # 类B的虚表指针(+16)存放到rdx
	movq	-8(%rbp), %rax      # rax = this
	movq	%rdx, (%rax)        # 类B的虚表指针存放到this对象的第一个8字节(覆盖了A产生的虚表指针)
	nop
	leave
	ret
.LFE12:
	.set	_ZN1BC1Ev,_ZN1BC2Ev

_ZN1AD2Ev:							# A::~A()@1
	endbr64
	pushq	%rbp
	movq	%rsp, %rbp
	movq	%rdi, -8(%rbp)			# this指针存储到栈的第一个8字节
	leaq	16+_ZTV1A(%rip), %rdx	# 类A的虚表指针(+16)存放到rdx
	movq	-8(%rbp), %rax			# rax = this
	movq	%rdx, (%rax)			# *this = 类A的虚表指针(+16)
	nop
	popq	%rbp
	ret
.LFE1:
	.set	_ZN1AD1Ev,_ZN1AD2Ev

_ZN1BD2Ev:							# B::~B()@1
	endbr64
	pushq	%rbp
	movq	%rsp, %rbp
	subq	$16, %rsp
	movq	%rdi, -8(%rbp)			# this指针存储到栈的第一个8字节
	leaq	16+_ZTV1B(%rip), %rdx	# 类B的虚表指针(+16)存放到rdx
	movq	-8(%rbp), %rax			# rax = this
	movq	%rdx, (%rax)			# *this = 类B的虚表指针(+16)
	movq	-8(%rbp), %rax			
	movq	%rax, %rdi				# rdi = this
	call	_ZN1AD2Ev				# call A::~A()@1
	nop
	leave
	ret
.LFE15:
	.set	_ZN1BD1Ev,_ZN1BD2Ev

_ZN1BD0Ev: 						# B::~B()@2
	endbr64
	pushq	%rbp
	movq	%rsp, %rbp
	subq	$16, %rsp
	movq	%rdi, -8(%rbp)		# this指针存储到栈的第一个8字节
	movq	-8(%rbp), %rax		# rax = this
	movq	%rax, %rdi			# rdi = this
	call	_ZN1BD1Ev			# call B::~B()@1
	movq	-8(%rbp), %rax		# rax = this
	movl	$8, %esi			# esi = 8
	movq	%rax, %rdi			# rdi = this
	call	_ZdlPvm@PLT			# delete this
	leave
	ret

main:
	endbr64
	pushq	%rbp                # 栈基址
	movq	%rsp, %rbp          # 新的栈基址
	pushq	%rbx                # 
	subq	$24, %rsp           # 栈增长24字节
	movl	$8, %edi            # edi = 8
	call	_Znwm@PLT           # call new (size = 8)
	movq	%rax, %rbx          # rax --> rbx(new得到的指针)
	movq	$0, (%rbx)          # *rbx = 0
	movq	%rbx, %rdi          # rbx --> rdi
	call	_ZN1BC1Ev           # call B::B()
	movq	%rbx, -24(%rbp)     # rbx 放到栈的第一个8字节(new得到的指针)
	movq	-24(%rbp), %rax     # rax = (new得到的指针)
	movq	%rax, %rdi          # rdx = (new得到的指针)
	call	_ZN1A4drawEv        # call A::draw()
	movq	-24(%rbp), %rax     # rax = (new得到的指针)
	testq	%rax, %rax
	movq	(%rax), %rdx        # rdx = 虚表指针(B的虚表),vtable + 16
	addq	$8, %rdx            # rdx = vtable + 24
	movq	(%rdx), %rdx        # rdx = *(vtable + 24), i.e. B::~B()@2
	movq	%rax, %rdi          
	call	*%rdx               # call B::~B()
	movl	$0, %eax
	movq	-8(%rbp), %rbx
	leave
	ret

一些结论:

  • 虚表指针是在对象的构造函数中赋值的;
  • 继承关系中的构造函数和析构函数执行过程中,虚表指针会被多次赋值;
  • 虚函数的调用增加了利用虚表指针取得虚函数地址的过程;

一些和虚函数有关的问题

构造函数或析构函数中调用虚函数

先给出结论:

在构造函数或析构函数中调用虚函数,从编译角度而言,没有问题。但是一般而言,虚函数的通常情形是用基类的接口去调用子类的实现;在这个问题中,即在基类的构造函数或者析构函数中去调用子类的实现。但是这个目的是不能达到的。

从虚表的汇编分析中可以看到,在程序的构造和析构过程中,当执行到基类的构造或析构函数时,虚表指针会首先替换成基类的虚表指针,因此虚函数此时是失效的。

从语义角度而言,构造时先构造基类,再构造子类;析构时先析构子类,再析构基类。因此基类的构造函数和析构函数执行时,子类对象不存在。此时用虚函数调用子类的实现是不合理的。

默认参数和虚函数

问题:如果一个虚函数有默认参数,并且基类和子类的默认参数不一致,则实际调用时默认参数是什么?

先理解默认参数编译期处理:由于默认参数是放在头文件中的,因此有理由猜测,默认参数和类的实现无关,而和调用位置有关。

示例代码:

void fun(int a = 5);

void fun(int a)
{
    (void)a;
}

int main()
{
    fun();
    return 0;
}

对应的汇编代码为:

_Z3funi:
	endbr64
	pushq	%rbp
	movq	%rsp, %rbp
	movl	%edi, -4(%rbp)
	nop
	popq	%rbp
	ret
main:
	endbr64
	pushq	%rbp
	movq	%rsp, %rbp
	movl	$5, %edi
	call	_Z3funi
	movl	$0, %eax
	popq	%rbp
	ret

由此可见,调用位置根据调用的函数的默认参数进行传参,只与声明有关。

回到问题上,在调用含有默认参数的虚函数时,由于编译期间只能确定调用者的类型,因此传参只会根据该类型对应的默认参数进行传参,而不会根据对象的实际类型区分传参。

posted on 2023-07-11 16:26  amazzzzzing  阅读(70)  评论(0)    收藏  举报