派生类重载的delete操作符调用时可以动态绑定吗
我们来看一个和派生类重载delete操作符相关的C++程序:
class animal {
public:
virtual ~animal() {}
};
class dog : public animal {
public:
virtual ~dog() {
puts("destory dog");
}
void operator delete(void* p) {
printf("delete dog storage in %p\n", p);
::operator delete(p);
}
};
int main(int argc, char** argv) {
animal* ap = new dog;
delete ap;
return 0;
}
派生类dog继承基类animal,并且重载了operator delete()。ap 是一个基类animal的指针,但是它指向了一个由派生类dog在堆上创建的对象。那么,当程序执行delete ap时,会调用dog类重载的operator delete()吗?也就是说当delete一个基类指针时,会调用派生类重载的operator delete()函数吗?
不过需要注意的是,这里void dog::operator delete(void* p)并不是一个virtual函数,我们试着把它声明成virtual看看,编译时会发生失败:
error: 'operator delete' cannot be declared 'virtual', since it is always static
编译失败的原因是operator delete()总是类的static函数,也就是它不可能当作virtual函数的,也不是非static成员函数。况且也并没有在基类animal中定义一个operator delete()虚函数,然后在派生类dog中重写override这个函数,并不是我们日常编程实践中常见的OOP编程套路。因此,既然无法使用virtual函数来动态绑定,感觉应该是调用了全局的operator delete()函数。我们运行一下程序,它的输出log如下:
destory dog
delete dog storage in 0x10ad2b0
可见,当程序执行delete ap时,还是调用了dog类提供的operator delete(),这有点出乎意料,既然不是virtual函数,基类指针又是怎么知道派生类中的这个static成员函数的呢?
我们看一下汇编代码,下面是gcc在O1优化选项下生成的汇编代码:
dog::~dog() [base object destructor]:
sub rsp, 8
mov QWORD PTR [rdi], OFFSET FLAT:vtable for dog+16
mov edi, OFFSET FLAT:.LC0
call puts
add rsp, 8
ret
.LC1:
.string "delete dog storage in %p\n"
dog::operator delete(void*):
push rbx
mov rbx, rdi
mov rsi, rdi
mov edi, OFFSET FLAT:.LC1
mov eax, 0
call printf
mov rdi, rbx
call operator delete(void*)
pop rbx
ret
dog::~dog() [deleting destructor]:
push rbx
mov rbx, rdi
call dog::~dog() [complete object destructor]
mov rdi, rbx
call dog::operator delete(void*)
pop rbx
ret
dog::dog() [base object constructor]:
mov QWORD PTR [rdi], OFFSET FLAT:vtable for dog+16
ret
main:
push rbx
mov edi, 8
call operator new(unsigned long)
mov rbx, rax
mov rdi, rax
call dog::dog() [complete object constructor]
mov rax, QWORD PTR [rbx]
mov rdi, rbx
call [QWORD PTR [rax+8]]
mov eax, 0
pop rbx
ret
在main函数中语句delete ap对应的汇编代码是:
mov rax, QWORD PTR [rbx] //rbx是this指针,rax是虚函数表指针vptr
mov rdi, rbx
call [QWORD PTR [rax+8]] // 调用虚函数中的第2个虚函数
核心指令call [QWORD PTR [rax+8]],它调用了虚函数表中的第2个虚函数,在这里rax是指向dog类虚函数表的vptr指针,[rax+0]指向第1个虚函数,[rax+8]指向第2个虚函数。下面是dog类虚函数表的信息:
vtable for dog:
.quad 0
.quad typeinfo for dog
.quad dog::~dog() [complete object destructor]
.quad dog::~dog() [deleting destructor]
第2个虚函数是:dog::~dog() [deleting destructor],它的汇编代码如下:
dog::~dog() [deleting destructor]:
push rbx
mov rbx, rdi
call dog::~dog() [complete object destructor]
mov rdi, rbx
call dog::operator delete(void*)
pop rbx
ret
第6行指令:call dog::operator delete(void*),此处调用了dog类重载的operator delete()函数,因此程序最终还是调用了dog类重载的operator delete()函数,并没有调用全局的operator delete()。
我们知道在C++中,delete的语义是先调用对象的析构函数,然后再调用delete操作符函数,看一下dog::~dog() [deleting destructor]的实现流程:
第4行代码:call dog::~dog() [complete object destructor],在这里它和dog::~dog() [base object destructor]相同,它的汇编代码如下:
dog::~dog() [base object destructor]:
sub rsp, 8
mov QWORD PTR [rdi], OFFSET FLAT:vtable for dog+16
mov edi, OFFSET FLAT:.LC0
call puts
add rsp, 8
ret
它就是dog类的析构函数,可见函数dog::~dog() [deleting destructor]先调用了dog::~dog() [base object destructor],然后调用了call dog::operator delete(void*)。该函数先调用了dog类的析构函数,然后再调用dog类的重载的operator delete(),正好符合delete操作符的语义,也就是说在这里,编译器使用了一个独立的函数来封装了这个delete操作符的功能。可见,编译器生成了一个特殊的virtual析构函数,在这个析构函数中调用了operator delete()。
因此,派生类中重载的delete操作符在使用基类指针析构堆上对象时,也是动态绑定来调用的,只不过它并不是使用传统的方式,定义成虚函数来动态绑定的,而是被封装在一个编译器自动生成的虚析构函数中,通过动态绑定虚析构函数来间接的动态绑定。
我们再看一下虚函数表中的第1个虚函数:dog::~dog() [complete object destructor],它是dog类正常的析构函数,也就是程序中所定义的虚析构函数,它主要用于栈上对象和static对象的析构和在子类的析构函数中调用父类的析构函数,而第2个虚函数:dog::~dog() [deleting destructor],它是编译器为dog类新增的析构函数,主要用于delete操作符来析构堆上对象,这个函数用户并不可见,毕竟按照C++的语义,一个类只能有一个析构函数,故这个析构函数对用户是不可见的,只是编译器用来辅助进行对象的delete操作的,仅供编译器使用。
如果我们在测试程序中,编写下面的测试代码:
void foo() {
dog d; // 创建栈上对象
}
dog global; // 创建全局对象
编译器生成的汇编代码如下:
foo():
sub rsp, 24
mov QWORD PTR [rsp+8], OFFSET FLAT:vtable for dog+16
lea rdi, [rsp+8]
call dog::~dog() [complete object destructor]
add rsp, 24
ret
__static_initialization_and_destruction_0():
sub rsp, 8
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:global // 程序退出时,回调global的析构函数
mov edi, OFFSET FLAT:dog::~dog() [complete object destructor]
call __cxa_atexit
add rsp, 8
ret
可见,这两种创建类型的对象在析构时,都调用了正常实现的析构函数:dog::~dog() [complete object destructor]。
需要注意的是,这种动态绑定delete操作符的机制,是GCC和CLANG编译器所使用的方案,MSVC编译器并没有使用,它没有定义了一个新的析构函数,而是通过为析构函数传递不同标志参数的方式来实现的。
下面是MSVC编译器生成的汇编代码,传递的标志参数存放在edx寄存器中,具体细节可自己分析一下:
virtual void * dog::`scalar deleting destructor'(unsigned int) PROC ; dog::`scalar deleting destructor', COMDAT
$LN18:
mov QWORD PTR [rsp+8], rbx
push rdi
sub rsp, 32 ; 00000020H
lea rax, OFFSET FLAT:const dog::`vftable'
mov rbx, rcx
mov QWORD PTR [rcx], rax
mov edi, edx // 把标志参数存入edi寄存器
lea rcx, OFFSET FLAT:`string'
call puts
lea rax, OFFSET FLAT:const animal::`vftable'
mov QWORD PTR [rbx], rax
test dil, 1
je SHORT $LN4@scalar // 参数edi的第0位为0时,不需要delete堆上内存
test dil, 4
jne SHORT $LN3@scalar // 参数edi的第2位为0时,在22行调用dog重载的operator delete,否则在在27行调用全局的operator delete
mov rdx, rbx
lea rcx, OFFSET FLAT:`string'
call printf
mov rcx, rbx
call void operator delete(void *) ; operator delete
jmp SHORT $LN4@scalar
$LN3@scalar:
mov edx, 8
mov rcx, rbx
call void __global_delete(void *,unsigned __int64) ; __global_delete
$LN4@scalar:
mov rax, rbx
mov rbx, QWORD PTR [rsp+48]
add rsp, 32 ; 00000020H
pop rdi
ret 0
virtual void * dog::`scalar deleting destructor'(unsigned int) ENDP