虚函数内存布局解析

一、 基础概念

1.1 基础问题

为什么需要虚函数?

主要在于功能解耦合以及规范化的需要。我们很多时候需要做到:一个模块对达成某个功能(机制)有一组可替换的实现(策略)。也就是说,需要策略和机制分离。那么我们的模块就应该只依赖这个机制的接口,而非这个机制的具体实现。则:

相对而言java中更加完善,它会告诉妳这是一个接口

  1. 可以使用纯虚函数要求某个子类必须实现某个方法
  2. 也可以使用普通函数,允许子类复用这个方法,或者子类自己实现这个方法

更加具体的来说,虚函数还解决了动态分发得问题:

#include <cstdio>

class Base{
    public:
    void non_virtual(){printf("base\n");}
};

class Child: public Base {
    public:
    void non_virtual(){printf("child\n");}
};

int main (int argc, char *argv[])
{
    Base *b = new Child();
    b->non_virtual();		// base
    return 0;
}

运行后输出base,这里如果我们想在业务中使用Child的功能,那么我们就必须把Child耦合到业务代码,这里还存在一个问题就是:Basechild都生成了非虚的析构函数,直接delete b可能导致释放不完全。

而虚函数可以很好的解决这个问题:

#include <cstdio>

class Base{
    public:
    virtual void non_virtual(){printf("base\n");}
};

class Child: public Base {
    public:
    virtual void non_virtual(){printf("child\n");}
};

int main (int argc, char *argv[])
{
    Base *b = new Child();
    b->non_virtual();
    return 0;
}

输出为child,这是怎么实现的呢?正是本文的内容。

虚函数注意事项

  1. 基类必须实现虚析构函数,从而保证执行正确的析构版本。
  2. 建议所有覆盖都显示地注明override。这样编译器可以帮助你进行继承检查。
  3. 一个派生类的函数如果覆盖了继承而来的虚函数,那么形参类型、返回值类型必须与被覆盖函数完全一致(不然编译器会认为不是同一个函数)
  4. 子类覆盖的函数,哪怕没有标注virtual,其实质上也是virtual的(可以认为子类不需要对于覆盖的函数,显式添加virtual)。
  5. 如果你希望函数不要再被子类覆盖,那么可以使用final关键字。
class Child: public Base {
    public:
    virtual void non_virtual() override final{printf("child\n");}
};
  1. 如果虚函数有默认实参,那么继承时也应当使用相同的默认值。
  2. 如果你偏要使用基类的方法,那么可以通过作用域运算符::做到。
b->Base::non_virtual();

访问控制

privatepublic就不说了。protect能够让成员变成:用户访问不到,子类友元访问得到;且子类和友元只能通过派生类对象访问基类成员,不能直接用基类对象访问基类成员。

1.2 最派生类和最派生对象

// 下面代码中mostderived时最派生类
class base {};
class derived : base {};
class base2 {};
class mostderived : derived, base2 {};

mostderived md;

同样,我们把没有父类得base称为最超类

1.3 虚调用偏移

**虚调用偏移(vcall offset)是一个偏移量。**有一些虚函数,声明于虚基类,重写于派生类。当我们需要调用在派生类中覆写得函数的时候,需要将base_this转化为derive_thisvcall_offset只存在于虚继承的场景。

#include <stdio.h>
#include <stdint.h>


struct ABParent{
    int64_t ab;
    virtual void parent_virtual1() { printf("front ABParent\n"); }
    virtual ~ABParent(){}
};

struct A: virtual ABParent{
    int64_t a;
    virtual void parent_virtual1() { printf("front\n"); }
};

int main (int argc, char *argv[])
{
    auto a = new A();
    ABParent *p = a;
    printf("p-a=%ld\n", (uint64_t)p - (uint64_t)a);
    p->parent_virtual1();
    delete p;
    
    return 0;
}

上述程序输出:

p-a=16
front

这对于习惯C语言的人来说会感到违背直觉。为什么pa都能调用到同一个函数,但是pa却在不同的地址?
实际上,当执行p->parent_virtual1(); 时,this指针会进行修正,从而指向真正的对象A
在下面的例子中,我们会看到 vcall_offset (-16) 这表示需要将p指针加上-16作为真正对象Athis指针。

1.4 虚基偏移

虚基偏移(vbase offset)的作用则相反,是为了从真正对象A访问到虚基类ABParent。只存在于虚继承的场景。
如果vbase_offset存在,则会位于Avtable的第一个slot. 例如上面的代码生成了这样的vtable:

Vtable for 'A' (13 entries).
   0 | vbase_offset (16)
   1 | offset_to_top (0)
   2 | A RTTI
       -- (A, 0) vtable address --
   3 | void A::parent_virtual1()
   4 | A::~A() [complete]
   5 | A::~A() [deleting]
   6 | vcall_offset (-16)
   7 | vcall_offset (-16)
   8 | offset_to_top (-16)
   9 | A RTTI
       -- (ABParent, 16) vtable address --
  10 | void A::parent_virtual1()
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  11 | A::~A() [complete]
       [this adjustment: 0 non-virtual, -32 vcall offset offset]
  12 | A::~A() [deleting]
       [this adjustment: 0 non-virtual, -32 vcall offset offset]
  • offset_to_top(0):表示当前这个虚函数表地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为0。

  • RTTI指针:指向存储运行时类型信息(type_info)的地址,用于运行时类型识别,用于typeiddynamic_cast

对应于这样得layout:

*** Dumping AST Record Layout
         0 | struct ABParent
         0 |   (ABParent vtable pointer)
         8 |   int64_t ab
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping AST Record Layout
         0 | struct A
         0 |   (A vtable pointer)
         8 |   int64_t a
        16 |   struct ABParent (virtual base)
        16 |     (ABParent vtable pointer)
        24 |     int64_t ab
           | [sizeof=32, dsize=32, align=8,
           |  nvsize=16, nvalign=8]

Layoutoffset=16的位置(第 12 行)恰好对应 vtablevbase_offset=16。再次证明vbase_offset就是从子类对象寻找虚基类对象的距离。

1.5 编译选项

本文采用的编译选项是:

// 查看对象布局
clang -Xclang -fdump-record-layouts -stdlib=libc++ -c main.cpp
// 查看虚函数表布局
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c main.cpp

gdb打印相关命令:

set print object on
set print vtbl on
set print pretty on
//以上可以定义一个gdb命令的alias,简化命令
p 实例
info vtbl 实例

二、 普通继承内存布局

2.1 单一继承

2.1.1 成员变量布局
class A
{
public:
    char aval;
    static int sival;
    void funcA1();
};
class B : public A
{
public:
    double bval;
    void funcB1();
};
class C : public B
{
public:
    int cval;
    void funcC1() {}
};

内存布局:

*** Dumping AST Record Layout
         0 | class A
         0 |   char aval
           | [sizeof=1, dsize=1, align=1,
           |  nvsize=1, nvalign=1]

*** Dumping AST Record Layout
         0 | class B
         0 |   class A (base)
         0 |     char aval
         8 |   double bval
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping AST Record Layout
         0 | class C
         0 |   class B (base)
         0 |     class A (base)
         0 |       char aval
         8 |     double bval
        16 |   int cval
           | [sizeof=24, dsize=20, align=8,
           |  nvsize=20, nvalign=8]

可以看出,普通的单一继承,成员变量是从上到下依次排列的,并且遵循前面提到的字节对齐规则

2.1.2 虚函数表
  • A中包含两个虚函数vfuncA1vfuncA2
  • B重写 (Override)了vfuncA1,自定义虚函数vfuncB.
  • C重写了vfunc1,自定义虚函数vfuncC.
class A {
public:
  char aval;
  static int sival;
  virtual void vfuncA1() {}
  virtual void vfuncA2() {}
};
class B : public A {
public:
  double bval;
  virtual void vfuncA1() {}
  virtual void vfuncB() {}
};
class C : public B {
public:
  int cval;
  virtual void vfuncA1() {}
  virtual void vfuncC() {}
};

内存布局如下:

*** Dumping AST Record Layout
         0 | class A
         0 |   (A vtable pointer)
         8 |   char aval
           | [sizeof=16, dsize=9, align=8,
           |  nvsize=9, nvalign=8]

*** Dumping AST Record Layout
         0 | class B
         0 |   class A (primary base)
         0 |     (A vtable pointer)
         8 |     char aval
        16 |   double bval
           | [sizeof=24, dsize=24, align=8,
           |  nvsize=24, nvalign=8]

*** Dumping AST Record Layout
         0 | class C
         0 |   class B (primary base)
         0 |     class A (primary base)
         0 |       (A vtable pointer)
         8 |       char aval
        16 |     double bval
        24 |   int cval
           | [sizeof=32, dsize=28, align=8,
           |  nvsize=28, nvalign=8]

三个类的虚函数表得内存布局如下:

Vtable for 'C' (6 entries).
   0 | offset_to_top (0)
   1 | C RTTI
       -- (A, 0) vtable address --
       -- (B, 0) vtable address --
       -- (C, 0) vtable address --
   2 | void C::vfuncA1()
   3 | void A::vfuncA2()
   4 | void B::vfuncB()
   5 | void C::vfuncC()

Vtable for 'B' (5 entries).
   0 | offset_to_top (0)
   1 | B RTTI
       -- (A, 0) vtable address --
       -- (B, 0) vtable address --
   2 | void B::vfuncA1()
   3 | void A::vfuncA2()
   4 | void B::vfuncB()

Vtable for 'A' (4 entries).
   0 | offset_to_top (0)
   1 | A RTTI
       -- (A, 0) vtable address --
   2 | void A::vfuncA1()
   3 | void A::vfuncA2()

可以看出,在单一继承中,子类的虚函数表通过以下步骤构造出来:

  • 先拷贝上一层次父类的虚函数表。
  • 如果子类有自定义虚函数(例如B::vfuncB, C::vfuncC),那么直接在虚函数表后追加这些虚函数的地址。
  • 如果子类覆盖了父类的虚函数,使用新地址(例如B::vfuncA1, C::vfuncA1)覆盖原有地址(即A::vfunc1)。
    在这里插入图片描述

2.2 多继承

class A {
  char aval;
  virtual void vfuncA1() {}
  virtual void vfuncA2() {}
};
class B {
  double bval;
  virtual void vfuncB1() {}
  virtual void vfuncB2() {}
};
class C : public A, public B {
  char cval;
  virtual void vfuncC() {}
  virtual void vfuncA1() {}
  virtual void vfuncB1() {}
};
2.2.1 内存布局
clang -Xclang -fdump-record-layouts -stdlib=libc++ -c main.cpp

*** Dumping AST Record Layout
         0 | class A
         0 |   (A vtable pointer)
         8 |   char aval
           | [sizeof=16, dsize=9, align=8,
           |  nvsize=9, nvalign=8]

*** Dumping AST Record Layout
         0 | class B
         0 |   (B vtable pointer)
         8 |   double bval
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping AST Record Layout
         0 | class C
         0 |   class A (primary base)
         0 |     (A vtable pointer)
         8 |     char aval
        16 |   class B (base)
        16 |     (B vtable pointer)
        24 |     double bval
        32 |   char cval
           | [sizeof=40, dsize=33, align=8,
           |  nvsize=33, nvalign=8]
  1. 类对象得大小为40,包含了两个vtbl
  2. 继承有primary base父类和普通base父类之分。

在这里插入图片描述
总的来说,在最底层子类的内存布局中,多继承的成员变量,以及 vtable 指针的排列规则是:

  • 第一个声明得是primary base 父类
  • 按照继承的声明顺序依次排列,并需要遵循编译器的字节对齐规则。
  • 最后排列最底层子类的成员变量。
2.2.2 虚函数表
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c main.cpp
Vtable for 'C' (10 entries).
   0 | offset_to_top (0)
   1 | C RTTI
       -- (A, 0) vtable address --
       -- (C, 0) vtable address --
   2 | void C::vfuncA1()
   3 | void A::vfuncA2()
   4 | void C::vfuncC()
   5 | void C::vfuncB1()
   6 | offset_to_top (-16)
   7 | C RTTI
       -- (B, 16) vtable address --
   8 | void C::vfuncB1()
       [this adjustment: -16 non-virtual]
   9 | void B::vfuncB2()

在这里插入图片描述
从上面可以看出,C 的虚函数表是由 2 部分组成的:

  • 首先是 「C 继承 A」,按照上述单一继承的虚函数表生成原则,生成了第一个虚函数表。此时C::vfuncB1()对于 A 来说是一个自定义的虚函数,因此虚函数表的第一部分有 4 个函数地址。
  • 其次是「C 继承 B」,同样按照单一继承的规则生成,但不用追加C::vfuncC(),因为C::vfuncC()已经在第一部分填入。
  • C 和 A共享vptr, B 拥有独立的vptr

可以发现:

  • C的虚函数表存在一个重复项C::vfuncB1()
  • 虽然C有两个vptr,但是只有一个虚函数表,两个vptr指向了虚函数表得不同位置

如何验证呢?

#include <iostream>
#include <stdint.h>

using namespace std;

class A {
public:
  char aval;
  virtual void vfuncA1() { cout << "A::vfuncA1()" << endl; }
  virtual void vfuncA2() { cout << "A::vfuncA2()" << endl; }
};
class B {
public:
  double bval;
  virtual void vfuncB1() { cout << "B::vfuncB1()" << endl; }
  virtual void vfuncB2() { cout << "B::vfuncB2()" << endl; }
};
class C : public A, public B {
public:
  char cval;
  virtual void vfuncC() { cout << "C::vfuncC()" << endl; }
  virtual void vfuncA1() { cout << "C::vfuncA1()" << endl; }
  virtual void vfuncB1() { cout << "C::vfuncB1()" << endl; }
};

int main() {
  C c;
  uint64_t *cvtable = (uint64_t *)*(uint64_t *)(&c);
  uint64_t *cvtable2 = (uint64_t *)*(uint64_t *)((uint8_t *)(&c) + 16);
  typedef void (*func_t)(void);

  cout << "---- vtable1 ----" << endl;
  ((func_t)(*(cvtable + 0)))(); // C::vfuncA1()
  ((func_t)(*(cvtable + 1)))(); // A::vfuncA2()
  ((func_t)(*(cvtable + 2)))(); // C::vfuncC()
  ((func_t)(*(cvtable + 3)))(); // C::vfuncB1()

  printf("offset_to_top = %ld\n", *(cvtable2 - 2)); // -16

  cout << "---- vtable2 ----" << endl;
  ((func_t)(*(cvtable2 + 0)))(); // C::vfuncB1(), same as cvtable + 6
  ((func_t)(*(cvtable2 + 1)))(); // B::vfuncB2(), same as cvtable + 7
}

三、 虚继承内存布局

3.1 单一继承

class Base {
  char baseval;
  virtual void vfuncBase1() {}
  virtual void vfuncBase2() {}
};

class A : virtual public Base {
  double aval;
  virtual void vfuncBase1() {}
  virtual void vfuncA() {}
};

class B : virtual public Base {
  double bval;
  virtual void vfuncBase2() {}
  virtual void vfuncB() {}
};

int main(int argc, char *argv[]) {
  A a;
  B b;
  return 0;
}
3.1.1 内存布局

A的内存布局如下:

clang -Xclang -fdump-record-layouts -stdlib=libc++ -c main.cpp

*** Dumping AST Record Layout
         0 | class Base
         0 |   (Base vtable pointer)
         8 |   char baseval
           | [sizeof=16, dsize=9, align=8,
           |  nvsize=9, nvalign=8]

*** Dumping AST Record Layout
         0 | class A
         0 |   (A vtable pointer)
         8 |   double aval
        16 |   class Base (virtual base)
        16 |     (Base vtable pointer)
        24 |     char baseval
           | [sizeof=32, dsize=25, align=8,
           |  nvsize=16, nvalign=8]

与普通单一继承不同,虚拟单一继承时存在两个vptr的,而且被继承的对象排在后面
在这里插入图片描述

3.1.2 虚函数表
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c main.cpp
Vtable for 'A' (11 entries).
   0 | vbase_offset (16)
   1 | offset_to_top (0)
   2 | A RTTI
       -- (A, 0) vtable address --
   3 | void A::vfuncBase1()
   4 | void A::vfuncA()
   5 | vcall_offset (0)
   6 | vcall_offset (-16)
   7 | offset_to_top (-16)
   8 | A RTTI
       -- (Base, 16) vtable address --
   9 | void A::vfuncBase1()
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  10 | void Base::vfuncBase2()
  • 表的第一部分{3,4},按照A是一个单一类的规则直接构造
  • 表的第二部分{9,10},按照A单一继承Base的规则构造
  • A如果要调用Base::vfuncBase2,编译器会通过this+vbase_offsetA转化为base再进行调用
  • Base如果要调用A::vfuncBase1(), 编译器会通过this+vcall_offsetBase转化为A再进行调用
    在这里插入图片描述

3.2 菱形继承

class Child : public A, public B {
  char childval;
  virtual void vfuncC() {}
  virtual void vfuncB() {}
  virtual void vfuncA() {}
};
3.2.1 内存布局
*** Dumping AST Record Layout
         0 | class Child
         0 |   class A (primary base)
         0 |     (A vtable pointer)
         8 |     double aval
        16 |   class B (base)
        16 |     (B vtable pointer)
        24 |     double bval
        32 |   char childval
        40 |   class Base (virtual base)
        40 |     (Base vtable pointer)
        48 |     char baseval
           | [sizeof=56, dsize=49, align=8,
           |  nvsize=33, nvalign=8]
  • 成员变量和虚函数指针与「多继承」的情况相同。
  • ChildBase(被虚拟继承的父类)的内容排在最后(比Child 的自定义成员还要后),并且只保留了一份Base的数据,这就是虚拟继承的作用。
    在这里插入图片描述
3.2.2 虚函数表
Vtable for 'Child' (18 entries).
   0 | vbase_offset (40)
   1 | offset_to_top (0)
   2 | Child RTTI
       -- (A, 0) vtable address --
       -- (Child, 0) vtable address --
   3 | void A::vfuncBase1()
   4 | void Child::vfuncA()
   5 | void Child::vfuncC()
   6 | void Child::vfuncB()
   7 | vbase_offset (24)
   8 | offset_to_top (-16)
   9 | Child RTTI
       -- (B, 16) vtable address --
  10 | void B::vfuncBase2()
  11 | void Child::vfuncB()
       [this adjustment: -16 non-virtual]
  12 | vcall_offset (-24)
  13 | vcall_offset (-40)
  14 | offset_to_top (-40)
  15 | Child RTTI
       -- (Base, 40) vtable address --
  16 | void A::vfuncBase1()
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  17 | void B::vfuncBase2()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]

在这里插入图片描述

验证代码:

#include <stdint.h>
#include <stdio.h>

class Base {
  char baseval;
  virtual void vfuncBase1() { printf("void Base::vfuncBase1()\n"); }
  virtual void vfuncBase2() { printf("void Base::vfuncBase2()\n"); }
};

class A : virtual public Base {
  double aval;
  virtual void vfuncBase1() { printf("void A::vfuncBase1()\n"); }
  virtual void vfuncA() { printf("void A::vfuncA()\n"); }
};

class B : virtual public Base {
  double bval;
  virtual void vfuncBase2() { printf("void B::vfuncBase2()\n"); }
  virtual void vfuncB() { printf("void B::vfuncB()\n"); }
};

class Child : public A, public B {
  char childval;
  virtual void vfuncC() { printf("void Child::vfuncC()\n"); }
  virtual void vfuncB() { printf("void Child::vfuncB()\n"); }
  virtual void vfuncA() { printf("void Child::vfuncA()\n"); }
};

int main(int argc, char *argv[]) {
  A a;
  B b;
  Child c;

  uint64_t *cvtable = (uint64_t *)*(uint64_t *)(&c);
  uint64_t *cvtable1 = (uint64_t *)*((uint64_t *)(&c) + 2);
  uint64_t *cvtable2 = (uint64_t *)*((uint64_t *)(&c) + 5);

  typedef void (*func_t)(void);
  printf("-----vtable0-----\n");
  ((func_t)(*(cvtable + 0)))(); // void A::vfuncBase1()
  ((func_t)(*(cvtable + 1)))(); // void Child::vfuncA()
  ((func_t)(*(cvtable + 2)))(); // void Child::vfuncC()
  ((func_t)(*(cvtable + 3)))(); // void Child::vfuncB()
  printf("-----vtable1-----\n");
  ((func_t)(*(cvtable1 + 0)))(); // void B::vfuncBase2()
  ((func_t)(*(cvtable1 + 1)))(); // void Child::vfuncB()
  printf("-----vtable2-----\n");
  ((func_t)(*(cvtable2 + 0)))(); // void A::vfuncBase1()
  ((func_t)(*(cvtable2 + 1)))(); // void B::vfuncBase2()

  return 0;
}

参考

  1. 虚函数内存布局解析-clang
  2. cpp继承内存布局
  3. java 中 public,default,protected,private区别
  4. java的继承规则
  5. 面试系列之C++的对象布局
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值