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)

相关推荐
Narutolxy5 分钟前
深入探讨 Go 中的高级表单验证与翻译:Gin 与 Validator 的实践之道20241223
开发语言·golang·gin
Hello.Reader13 分钟前
全面解析 Golang Gin 框架
开发语言·golang·gin
禁默23 分钟前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程
yuyanjingtao26 分钟前
CCF-GESP 等级考试 2023年9月认证C++四级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试
Code哈哈笑33 分钟前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
程序猿进阶36 分钟前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
qq_4336184438 分钟前
shell 编程(二)
开发语言·bash·shell
闻缺陷则喜何志丹41 分钟前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
charlie1145141911 小时前
C++ STL CookBook
开发语言·c++·stl·c++20
袁袁袁袁满1 小时前
100天精通Python(爬虫篇)——第113天:‌爬虫基础模块之urllib详细教程大全
开发语言·爬虫·python·网络爬虫·爬虫实战·urllib·urllib模块教程