• C++虚表和多态底层实现和GDB观察对象虚表内存布局


    什么是虚表?

    1. 虚表全称为虚拟函数表
    2. 在C++语言中,每个有虚函数的类或者虚继承的子类,编译器都会为它生成一个虚表

    虚表存储在哪里?

    1. 对象头8B(如果是32位操作系统是对象头4B,如果是64位操作系统是对象头8B)

    什么是虚函数?

    虚函数就是函数前面用virtual来修饰它,用法格式为:

    virtual 函数返回类型 函数名(参数表){函数体};

    例如:

    class Heap {
    public:
        virtual void* allocate(size_t size) {
            return nullptr;
        };
    };
    

    什么是纯虚函数?

    纯虚函数的用法格式为:

    virtual 函数返回类型 函数名(参数表)= 0;

    class Heap {
    public:
        virtual void* allocate(size_t size) = 0;
    };
    

    但是如果使用纯虚函数,我们不能创建 Heap 对象。
    new Heap 编译报错:不允许使用抽象类类型“Heap”的对象,因为函数“Heap::allocate”是纯虚函数。

    C++类对象内存布局

    以下结论摘自 《C++中一个class类对象占用多少内字节》,本文不做深入讨论

    1. 一个空的类对象在内存中占1个字节;

    这是为何呢?我想对于这个问题,不仅是刚入行不久的开发新手,就算有过几年以上C++开发经验的开发人员也未必能说清楚这个。
    编译器在执行 Heap* p = new Heap;这行代码后需要,作出一个 class Heap 的对象。并且这个对象的地址还是独一无二的,于是编译器就会给空类创建一个隐含的一个字节的空间。

    1. static 静态成员变量 占用类对象内存空间;
    2. 成员函数 占用类对象内存空间的;
    3. 每个有虚函数的类或者虚继承的子类(即父类中包含虚函数),类对象内存空间的头部将增加一个“虚表指针”(32位操作系统占4B,64位操作系统占8B);

    带有“抽象”的“纯虚函数”的类,不能实例化。(因为,“不能实例化抽象类”)
    所以,它也不存在类对象内存空间。

    GDB观察栈&对象&虚表的方法

    先讲一下我的 GDB 观察方法,为了缩减篇幅,之后只会给图和结论。

    运行环境 版本
    操作系统 Ubuntu 64位
    IDE Clion

    首先 Base 的测试代码如下图所示:

    #include <cstdio>
    
    class Base {
    public:
      virtual void f() {};
      virtual void g() {};
      virtual void h() {};
    };
    
    
    int main() {
      Base* base = new Base;
      long* vtable = (long*)*(long*)base;
      printf("%p\n", vtable);
      printf("%lx\n",*vtable);
      printf("%lx\n",*(vtable+1));
      printf("%lx\n",*(vtable+2));
      return 0;
    }
    

    把断点打在 main 函数 return 0 的位置,并且运行C++程序。

    然后执行以下命令查看当前“栈帧”的局部变量及其内存地址

    (gdb) info locals
    base = 0x55555556aeb0
    vtable = 0x555555557d88 <vtable for Base+16>
    
    (gdb) info locals vtable
    vtable = 0x555555557d88 <vtable for Base+16>
    

    查看局部变量对应的Base对象的头8B,g表示显示8个字节,x表示以十六进制数显示:

    (gdb) x/1gx 0x55555556aeb0
    0x55555556aeb0:	0x0000555555557d88
    

    Base对象头8B刚好存储的是虚表地址

    查看虚表中存储的字节:

    (gdb) x/3gx 0x0000555555557d88
    0x555555557d88 <vtable for Base+16>:	0x0000555555555218	0x0000555555555228
    0x555555557d98 <vtable for Base+32>:	0x0000555555555238
    

    查看以指令格式 i(instruction) 查看虚表中的字节对应的含义:

    (gdb) x/i 0x0000555555555218
       0x555555555218 <Base::f()>:	endbr64 
    (gdb) x/i 0x0000555555555228
       0x555555555228 <Base::g()>:	endbr64 
    (gdb) x/i 0x0000555555555238
       0x555555555238 <Base::h()>:	endbr64 
    

    下图是我用 Excel 画的一个简单的示意图:

    ★ 如果创建多个 Base 对象实例,他们的头部“虚表指针”相同,共用同一个“虚表”。

    不同继承情况:

    然后,我们多种不同的继承情况来研究子类的内存对象结构。

    子类不覆写父类虚函数时

    当子类不覆写父类虚函数时,在子类的虚函数表中,先存放基类的虚函数,在存放子类自己的虚函数。

    //子类1,无虚函数重载 
    class Child1 : public Base {
    public:
        virtual void f1() { }
        virtual void g1() { }
        virtual void h1() { }
    };
    

    子类覆写父类其中一个虚函数时

    当子类重写了父类的虚函数,编译器会将子类虚函数表中对应的父类的虚函数替换成子类的函数。

    //子类2,覆写父类的虚函数f 
    class Child2 : public Base {
    public:
        virtual void f() override { };
        virtual void g1() { };
        virtual void h1() { };
    };
    

    子类覆写父类全部虚函数时

    // 覆写全部父类虚函数
    class Child3 : public Base {
    public:
        virtual void f() override { };
        virtual void g() override { };
        virtual void h() override { };
    };
    

    多重继承

    对于多重继承,子类对象先存放第一个父类的数据拷贝,在存放第二个父类的数据拷贝,依次类推,最后存放自己的数据成员。

    其中,每一个父类拷贝都包含一个虚函数表指针。如果子类重载了某个父类的某个虚函数,那么该将该父类虚函数表的函数覆盖。

    另外,子类自己的虚函数,存储于第一个父类的虚函数表后边部分。

    来看一个例子:

    #include <cstdio>
    
    class Base {
    public:
        virtual void f() {};
        virtual void g() {};
        virtual void h() {};
    };
    
    class Parent {
    public:
        virtual void x() {};
        virtual void y() {};
        virtual void z() {};
    };
    
    class Child4 : public Base, public Parent {
    public:
        void f() override { };
        void y() override { };
        virtual void f4() { };
        virtual void g4() { };
        virtual void h4() { };
    };
    
    int main() {
        Base* base = new Base;
        Parent* parent = new Parent;
        Base* child4 = new Child4;
        long* vtable = (long*)*(long*)base;
        printf("%p\n", vtable);
        printf("%lx\n",*vtable);
        printf("%lx\n",*(vtable+1));
        printf("%lx\n",*(vtable+2));
        return 0;
    }
    

    Child4 继承了 Base 和 Parent 两个父类,每一个父类拷贝都包含一个虚函数表指针,所有 Child4 对象有两个虚表指针。
    如果父类 Base 有数据成员的话,这两个“虚表指针”中间将间隔 Base类的数据成员拷贝。
    如果 Parent 和 Child4 也有自己的数据成员,那要放在 Parent 的数据成员拷贝之后。
    Child4 自己的虚函数保存在第一个父类 Base 的虚函数之后。
    Child4 在第一个父类 Base 的虚函数之后可以找到 Child4::y() 方法,同时,在第二个父类 Parent 对应虚表中 Parent::y() 被 Child4::y() 覆盖和取代了。

    虚继承

    虚继承的用法格式为:

    class 子类 : virtual 父类;

    class Child5 : virtual public Base {
    public:
        virtual void f5() {};
        virtual void g5() {};
        virtual void h5() {};
    };
    

    我在 Linux 上试验时,并未发现 C++类对象的内存结构 上的(7)单一虚继承 提到的现象,
    考虑到 Windows 使用是 Visual C++ 编译器,而 Linux 使用的是 g++ 编译器,可能存在差异。

    g++ 在处理虚继承和普通继承时,是一样的规则。同上 子类不覆写父类虚函数时 的结果。

    参考文档:

    C++类对象的内存结构
    用vs查看c++类内存布局

  • 相关阅读:
    std::erase总结
    C++控制台应用程序运行控制台闪退
    判断当前进程是否已经打开C++
    获取当前系统语言C++
    VS中设置Qt多语言界面
    QString的功能
    安装mysql5.6
    centos6.9 PHP的编译安装并连接nginx
    centos6删除nginx
    centos6删除mysql安装
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/16557717.html
Copyright © 2020-2023  润新知