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++类内存布局

posted @ 2022-08-09 10:28  极客子羽  阅读(815)  评论(0编辑  收藏  举报