C++ 内存布局 - Part5: 继承关系中 构造析构与vptr的调整

这里以单继承为例,汇编采用AT&T格式,先看示例代码:

cpp 复制代码
#include <iostream>

class Base {
public:
    Base() {
            std::cout << "Base Constructor, this ptr: " << this << std::endl;
            printVptr();
    }
    virtual ~Base() {
            std::cout << "Base Destructor, this ptr: " << this << std::endl;
            printVptr();
    }
    virtual void printVptr() { std::cout << "Base::printVptr, vptr = " << *(void **)this << std::endl; }

    int a;
    int b;
};

class Derived : public Base {
public:
    Derived() {
            std::cout << "Derived Constructor, this ptr: " << this << std::endl;
            printVptr();
    }
    ~Derived() {
        std::cout << "Derived Destructor, this ptr: " << this << std::endl;
        printVptr();

    }
    void printVptr() override { std::cout << "Derived::printVptr, vptr = " << *(void **)this << std::endl; }

    int c;
};

int main() {
    std::cout << "Base size: " << sizeof(Base) << std::endl;
    std::cout << "Derived size: " << sizeof(Derived) << std::endl;

    Base *obj = new Derived();
    std::cout << "before delete, this ptr is: " << obj << std::endl;
    delete obj;
    return 0;
}

编译运行结果:

bash 复制代码
Base size: 16
Derived size: 24
Base Constructor, this ptr: 0xa1a2c0
Base::printVptr, vptr = 0x401090
Derived Constructor, this ptr: 0xa1a2c0
Derived::printVptr, vptr = 0x401068
before delete, this ptr is: 0xa1a2c0
Derived Destructor, this ptr: 0xa1a2c0
Derived::printVptr, vptr = 0x401068
Base Destructor, this ptr: 0xa1a2c0
Base::printVptr, vptr = 0x401090

从结果中可以看到, 在执行Base部分的构造和析构时,vptr指向的是基类的虚表,执行Derived部分的构造和析构时,vptr指向的是派生类的虚表。

查看汇编代码,以Base的析构为例,其他的构造析构都是类似:

bash 复制代码
Base::~Base() [base object destructor]:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $16, %rsp
        movq    %rdi, -8(%rbp)             将this指针入栈
        movl    $vtable for Base+16, %edx  将虚表指针存入edx
        movq    -8(%rbp), %rax             将this指针存入rax
        movq    %rdx, (%rax)  将虚表指针存入rax指向的内存,也就是对象内存的开始部分,vptr
        movl    $.LC1, %esi
        movl    $_ZSt4cout, %edi
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        movq    %rax, %rdx
        movq    -8(%rbp), %rax
        movq    %rax, %rsi
        movq    %rdx, %rdi
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(void const*)
        movl    $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esi
        movq    %rax, %rdi
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
        movq    -8(%rbp), %rax
        movq    %rax, %rdi
        call    Base::printVptr()
        nop
        leave
        ret

因此,整个构造析构的顺序为:

当new一个派生类对象时,首先会执行基类的构造函数,这时这个构造中的对象,其vptr指向基类的虚表,当基类部分构造完毕,继续执行派生类的构造函数时,此时对象的vptr指向派生类的虚表。

当delete这个派生类对象时,首先执行派生类的析构函数,此时对象的vptr仍然指向派生类的虚表,派生类的析构函数会继续执行基类的析构函数,此时对象的vptr会指向基类的虚表。最后派生类的析构函数会负责释放整个对象内存: call operator delete(void*, unsigned long)

相关推荐
小爬虫程序猿1 分钟前
Java爬虫:在1688上“照片快递”上传图片
java·开发语言·爬虫
Json_181790144802 分钟前
小红书笔记详情API接口系列(概述到示例案例)
开发语言·python
热爱生活热爱你23 分钟前
Qt5 读写共享内存,已验证,支持汉字的正确写入和读取
开发语言·qt
大白的编程日记.1 小时前
【C++笔记】list结构剖析及其模拟实现
c++·笔记·list
TheoBrien1 小时前
JUC(java.util.concurrent) 的常⻅类
java·开发语言
淡淡蛋痛1 小时前
D3.js 入门指南
开发语言·javascript·信息可视化
wrx繁星点点1 小时前
创建型模式-建造者模式:构建复杂对象的优雅解决方案
java·开发语言·数据结构·数据库·spring·maven·建造者模式
王大锤43911 小时前
golang的多表联合orm
开发语言·后端·golang
好看资源平台2 小时前
快速上手 Rust——实用示例
开发语言·后端·rust
许野平2 小时前
Rust:文档注释 //! 和 ///
开发语言·后端·rust·注释·文档注释