当前位置: 首页 > article >正文

派生类重载的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  

http://www.kler.cn/a/373757.html

相关文章:

  • 深度学习(八) TensorFlow、PyTorch、Keras框架大比拼(8/10)
  • ABAP开发学习——内存管理二
  • 【英特尔IA-32架构软件开发者开发手册第3卷:系统编程指南】2001年版翻译,2-9
  • 10进阶篇:运用第一性原理解答“是什么”类型题目
  • 【GO学习笔记 go基础】编译器下载安装+Go设置代理加速+项目调试+基础语法+go.mod项目配置+接口(interface)
  • uniapp编译多端项目App、小程序,input框键盘输入后
  • 创建一个基于SSM框架的药品商超管理系统
  • springboot响应文件流文件给浏览器+前端下载
  • redis详细教程(3.hash和set类型)
  • [TypeError]: type ‘AbstractProvider‘ is not subscriptable
  • 三项智能网联汽车强制性国家标准正式发布(附图解)
  • 应用在汽车控制系统安全气囊的爱普生可编程晶振SG-8018CG
  • SpringBoot技术:闲一品交易的新机遇
  • Java 多线程(九)—— JUC 常见组件 与 线程安全的集合类
  • ComfyUI正式版来袭!一键安装无需手动部署!支持所有电脑系统
  • 线程本地变量-ThreadLocal
  • CMake知识点
  • [LeetCode] 36. 有效的数独
  • JAVA的动态代理
  • 创新实践:基于边缘智能+扣子的智能取物机器人解决方案
  • DDRPHY数字IC后端设计实现系列专题之后端设计导入,IO Ring设计
  • Java中String的length与Oracle数据库中VARCHAR2实际存储长度不一致的问题
  • 【优选算法篇】前缀之美,后缀之韵:于数列深处追寻算法的动与静
  • 面试题:JVM(一)
  • 类和对象(中)—— 类的六个默认成员函数
  • 【面试题】Node.JS篇