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)

相关推荐
编程阿峰8 分钟前
Python 环境配置(二)安装jupyter、matplotlib、numpy库
开发语言·python·jupyter·numpy·matplotlib
磁爆步兵29 分钟前
内存分区:程序运行的核心秘密
java·开发语言·jvm
2301_7779983435 分钟前
Linux线程同步与互斥(三):信号量与环形队列实现生产者消费者模型
linux·c语言·c++
啦啦啦啦啦zzzz35 分钟前
设计模式:单例模式和工厂模式
c++·单例模式·设计模式·工厂模式
Henry Zhu12339 分钟前
C++ 动态多态详解
c++
此生决int1 小时前
深入理解C++系列(06)——模版初阶与STL
开发语言·c++
c238561 小时前
《枚举算法 “翻译官”:用 enum class 给 “游游的礼盒” 算分》
c语言·c++·算法
swany1 小时前
同步数据中,只需要几秒钟 & milvus向量数据库不可用 dify1.16.1 升级后踩坑记录
开发语言·python·numpy
小杍随笔1 小时前
2025年Rust GUI框架实战万字避坑指南
开发语言·后端·rust
noipp1 小时前
推荐题目:洛谷 P5843 [SCOI2012] Blinker 的噩梦
c语言·数据结构·c++·算法·游戏·洛谷·luogu