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)

相关推荐
z落落1 分钟前
C# ArrayList 动态集合(接口/区别/API/深浅拷贝)+ List<T> 泛型集合
开发语言·c#
Cx330❀1 分钟前
【Linux网络】从零构建高性能UDP服务器:从Echo到英译汉业务级实现
大数据·linux·服务器·开发语言·网络·c++·udp
basketball6163 分钟前
Golang:基础语法总结
开发语言·后端·golang
兰令水4 分钟前
leecodecode【双指针题2】【2026.5.26打卡-java版本】
java·开发语言·算法
不吃土豆的马铃薯4 分钟前
TCP 三次握手 / 四次挥手详解
服务器·开发语言·网络·c++·网络协议·tcp/ip
羑悻的小杀马特5 分钟前
【动态规划篇】正则表达式与通配符:开启代码匹配的赛博奇幻之旅
c++·算法·leetcode·正则表达式
ch.ju6 分钟前
Java程序设计(第3版)第四章——引用
java·开发语言
Huangjin007_9 分钟前
【C++ STL篇(十三)】无序关联容器 unordered_set / unordered_map解析
开发语言·c++
白日与明月10 分钟前
pip下载库指定操作系统及python版本
开发语言·python·pip
折哥的程序人生 · 物流技术专研10 分钟前
Qoder 1.0 完全指南:从安装到Agents驱动开发实战
开发语言·人工智能·python·ai编程