派生类重载的delete操作符调用时可以动态绑定吗

我们来看一个和派生类重载delete操作符相关的C++程序:

cpp 复制代码
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看看,编译时会发生失败:

cpp 复制代码
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如下:

cpp 复制代码
destory dog
delete dog storage in 0x10ad2b0

可见,当程序执行delete ap时,还是调用了dog类提供的operator delete(),这有点出乎意料,既然不是virtual函数,基类指针又是怎么知道派生类中的这个static成员函数的呢?

我们看一下汇编代码,下面是gcc在O1优化选项下生成的汇编代码:

cpp 复制代码
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对应的汇编代码是:

CPP 复制代码
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],它的汇编代码如下:

cpp 复制代码
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]相同,它的汇编代码如下:

cpp 复制代码
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操作的,仅供编译器使用。

如果我们在测试程序中,编写下面的测试代码:

cpp 复制代码
void foo() {
    dog d; // 创建栈上对象
}

dog global; // 创建全局对象

编译器生成的汇编代码如下:

cpp 复制代码
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寄存器中,具体细节可自己分析一下:

cpp 复制代码
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  
相关推荐
Theodore_102239 分钟前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
‘’林花谢了春红‘’2 小时前
C++ list (链表)容器
c++·链表·list
----云烟----3 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024063 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic3 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it3 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康3 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神4 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
机器视觉知识推荐、就业指导4 小时前
C++设计模式:建造者模式(Builder) 房屋建造案例
c++
宅小海4 小时前
scala String
大数据·开发语言·scala