我们来看一个和派生类重载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, 8mov QWORD PTR [rdi], OFFSET FLAT:vtable for dog+16mov edi, OFFSET FLAT:.LC0call putsadd rsp, 8ret
.LC1:.string "delete dog storage in %p\n"
dog::operator delete(void*):push rbxmov rbx, rdimov rsi, rdimov edi, OFFSET FLAT:.LC1mov eax, 0call printfmov rdi, rbxcall operator delete(void*)pop rbxret
dog::~dog() [deleting destructor]:push rbxmov rbx, rdicall dog::~dog() [complete object destructor]mov rdi, rbxcall dog::operator delete(void*)pop rbxret
dog::dog() [base object constructor]:mov QWORD PTR [rdi], OFFSET FLAT:vtable for dog+16ret
main:push rbxmov edi, 8call operator new(unsigned long)mov rbx, raxmov rdi, raxcall dog::dog() [complete object constructor]mov rax, QWORD PTR [rbx]mov rdi, rbxcall [QWORD PTR [rax+8]]mov eax, 0pop rbxret
在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 rbxmov rbx, rdicall dog::~dog() [complete object destructor]mov rdi, rbxcall dog::operator delete(void*)pop rbxret
第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, 8mov QWORD PTR [rdi], OFFSET FLAT:vtable for dog+16mov edi, OFFSET FLAT:.LC0call putsadd rsp, 8ret
它就是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, 24mov QWORD PTR [rsp+8], OFFSET FLAT:vtable for dog+16lea rdi, [rsp+8]call dog::~dog() [complete object destructor]add rsp, 24ret
__static_initialization_and_destruction_0():sub rsp, 8mov edx, OFFSET FLAT:__dso_handlemov esi, OFFSET FLAT:global // 程序退出时,回调global的析构函数mov edi, OFFSET FLAT:dog::~dog() [complete object destructor]call __cxa_atexitadd rsp, 8ret
可见,这两种创建类型的对象在析构时,都调用了正常实现的析构函数: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], rbxpush rdisub rsp, 32 ; 00000020Hlea rax, OFFSET FLAT:const dog::`vftable'mov rbx, rcxmov QWORD PTR [rcx], raxmov edi, edx // 把标志参数存入edi寄存器lea rcx, OFFSET FLAT:`string'call putslea rax, OFFSET FLAT:const animal::`vftable'mov QWORD PTR [rbx], raxtest dil, 1je SHORT $LN4@scalar // 参数edi的第0位为0时,不需要delete堆上内存test dil, 4jne SHORT $LN3@scalar // 参数edi的第2位为0时,在22行调用dog重载的operator delete,否则在在27行调用全局的operator deletemov rdx, rbxlea rcx, OFFSET FLAT:`string'call printfmov rcx, rbxcall void operator delete(void *) ; operator deletejmp SHORT $LN4@scalar
$LN3@scalar:mov edx, 8mov rcx, rbxcall void __global_delete(void *,unsigned __int64) ; __global_delete
$LN4@scalar:mov rax, rbxmov rbx, QWORD PTR [rsp+48]add rsp, 32 ; 00000020Hpop rdiret 0
virtual void * dog::`scalar deleting destructor'(unsigned int) ENDP