虚函数实现原理之虚函数表

Posted zuofaqi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了虚函数实现原理之虚函数表相关的知识,希望对你有一定的参考价值。

引言

C++使用虚函数来实现多态机制,大多数编译器是通过虚函数表来实现动态绑定。

类的内存布局

1.普通类

class B {
public:
  int m;
  int n;
};

int main() {
  printf("%ld
", sizeof(B));

  B b;
  printf("%p, %p, %p
", &b, &b.m, &b.n);

  return 0;
}

类中只有普通成员变量,对象在内存中顺序存储成员变量。输出:

8
0x7ffee48dae00, 0x7ffee48dae00, 0x7ffee48dae04

2.有虚函数的类

class B {
public:
  virtual void f() {
    printf("B::f
");
  }
  virtual void g() {
    printf("B::g
");
  }
  virtual void h() {
    printf("B::h
");
  }

  int m;
  int n;
};

int main() {
  printf("%ld
", sizeof(B));

  B b;
  printf("%p
%p
%p
", &b, &b.m, &b.n);

  return 0;
}

先看输出结果:

16
0x7ffea995e540
0x7ffea995e548
0x7ffea995e54c

我们看到,这个对象的内存占用比上一个多了8个字节,其中成员变量m的地址也和对象b的地址不一样了,正好是 &b+8。
之所以这样,是因为对象中多出了虚函数指针,这个虚函数指针指向这个类的虚函数表,也就是说,这个指针保存的内容是一个虚函数表的地址,虚函数表,在内存中就是一个存储函数指针的数组

#include <stdio.h>

class B {
public:
  virtual void f() {
    printf("B::f
");
  }
  virtual void g() {
    printf("B::g
");
  }
  virtual void h() {
    printf("B::h
");
  }

  int m;
  int n;
};

using Func = void(*)(void);

int main() {
  printf("%ld
", sizeof(B));

  B b;
  printf("%p
%p
%p
", &b, &b.m, &b.n);

  long** vt = (long**)*(long***)&b;
  printf("vtable addr: %lx
", (long)vt);

  Func p = nullptr;
  for (int i = 0; i < 3; ++i)
  {
    p = (Func)*(vt + i);
    p();
  }

  return 0;
}

输出如下:

16
0x7ffe4222ebd0
0x7ffe4222ebd8
0x7ffe4222ebdc
vtable addr: 55a2e85c3d80
B::f
B::g
B::h

图形表示为:
技术图片

3.单继承无重写的类

写一个子类,继承自B类,没有重写B中的方法

#include <stdio.h>

class B {
public:
  virtual void f() {
    printf("B::f
");
  }
  virtual void g() {
    printf("B::g
");
  }
  virtual void h() {
    printf("B::h
");
  }

  int m;
  int n;
};

class D : public B {
public:
  virtual void df() {
    printf("D::df
");
  }
  virtual void dg() {
    printf("D::dg
");
  }
  virtual void dh() {
    printf("D::dh
");
  }

  int x,y;
};

using Func = void(*)(void);

int main() {
  B b;
  printf("sizeof b: %ld
", sizeof(b));
  printf("addr B:
%p
%p
%p
", &b, &b.m, &b.n);
  printf("vtable B: %p
", *(long***)&b);
  printf("------------
");

  D d;
  printf("sizeof d:%ld
", sizeof(d));
  printf("addr D:
%p
%p
%p
%p
%p
", &d, &d.m, &d.n, &d.x, &d.y);
  printf("vtable D: %p
", *(long***)&d);
  printf("------------
");

  long** vt = (long**)*(long***)&d;
  Func p = nullptr;
  for (int i = 0; i < 6; ++i)
  {
    p = (Func)*(vt+i);
    p();
  }

  return 0;
}

结果:

sizeof b: 16
addr B:
0x7fffce329990
0x7fffce329998
0x7fffce32999c
vtable B: 0x559e5a8c9d68
------------
sizeof d:24
addr D:
0x7fffce3299a0
0x7fffce3299a8
0x7fffce3299ac
0x7fffce3299b0
0x7fffce3299b4
vtable D: 0x559e5a8c9d28
------------
B::f
B::g
B::h
D::df
D::dg
D::dh

这个时候内存布局变成了这样:
技术图片
可以看到,子类只有一个虚函数指针,一个虚函数表,子类的函数附加在基类后面

4.单继承有重写

#include <stdio.h>

class B {
public:
  virtual void f() {
    printf("B::f
");
  }
  virtual void g() {
    printf("B::g
");
  }
  virtual void h() {
    printf("B::h
");
  }

  int m;
  int n;
};

class D : public B {
public:
  virtual void f() {
    printf("D::f
");
  }
  virtual void dg() {
    printf("D::dg
");
  }
  virtual void dh() {
    printf("D::dh
");
  }

  int x,y;
};

using Func = void(*)(void);

int main() {
  B b;
  printf("sizeof b: %ld
", sizeof(b));
  printf("addr B:
%p
%p
%p
", &b, &b.m, &b.n);
  printf("vtable B: %p
", *(long***)&b);
  printf("------------
");

  D d;
  printf("sizeof d:%ld
", sizeof(d));
  printf("addr D:
%p
%p
%p
%p
%p
", &d, &d.m, &d.n, &d.x, &d.y);
  printf("vtable D: %p
", *(long***)&d);
  printf("------------
");

  long** vt = (long**)*(long***)&d;
  Func p = nullptr;
  for (int i = 0; i < 5; ++i)
  {
    p = (Func)*(vt+i);
    p();
  }

  return 0;
}

结果:

sizeof b: 16
addr B:
0x7fffbfd06b60
0x7fffbfd06b68
0x7fffbfd06b6c
vtable B: 0x56277e20cd68
------------
sizeof d:24
addr D:
0x7fffbfd06b70
0x7fffbfd06b78
0x7fffbfd06b7c
0x7fffbfd06b80
0x7fffbfd06b84
vtable D: 0x56277e20cd30
------------
D::f
B::g
B::h
D::dg
D::dh

子类重写了基类的 B::f 方法,内存布局变为:
技术图片
类D的虚函数表中,第一个函数被替换成了 D::f,其他不变。
这个替换,是在编译期间完成的。
这里就是实现多态的关键地方了,当用基类的指针调用f函数,如果该指针指向的是基类,那么虚函数指针指向的就是基类的虚函数表,调用B::f。如果该指针指向子类对象,则虚函数指针指向子类的虚函数表,调用D::f

5.多重继承无重写

#include <stdio.h>

class B {
public:
  virtual void f() {
    printf("B::f
");
  }
  virtual void g() {
    printf("B::g
");
  }
  virtual void h() {
    printf("B::h
");
  }

  int m;
  int n;
};

class B2 {
public:
  virtual void f() {
    printf("B2::f
");
  }
  virtual void g() {
    printf("B2::g
");
  }
  virtual void h() {
    printf("B2::h
");
  }

  int i,j;
};

class D : public B, public B2 {
public:
  virtual void df() {
    printf("D::df
");
  }
  virtual void dg() {
    printf("D::dg
");
  }
  virtual void dh() {
    printf("D::dh
");
  }

  int x,y;
};

using Func = void(*)(void);

int main() {
  B b;
  printf("sizeof b: %ld
", sizeof(b));
  printf("addr B:
%p
%p
%p
", &b, &b.m, &b.n);
  printf("vtable B: %p
", *(long***)&b);
  printf("------------
");

  B2 b2;
  printf("sizeof b2: %ld
", sizeof(b2));
  printf("addr B2:
%p
%p
%p
", &b2, &b2.i, &b2.j);
  printf("vtable B2: %p
", *(long***)&b2);
  printf("------------
");

  D d;
  printf("sizeof d:%ld
", sizeof(d));
  printf("addr D:
%p
%p
%p
%p
%p
%p
%p
", &d, &d.m, &d.n, &d.i, &d.j, &d.x, &d.y);
  printf("vtable D: %p
", *(long***)&d);
  printf("------------
");

  // first vtable pointer
  long** vt1 = (long**)*(long***)&d;
  Func p = nullptr;
  for (int i = 0; i < 6; ++i)
  {
    p = (Func)*(vt1+i);
    p();
  }

  // second vtable pointer. member 'm' and 'n' type is int, pointer +2
  long** vt2 = (long**)*((long***)&d + 2);
  for (int i = 0; i < 3; ++i)
  {
    p = (Func)*(vt2+i);
    p();
  }

  return 0;
}

结果:

sizeof b: 16
addr B:
0x7ffc2868cf40
0x7ffc2868cf48
0x7ffc2868cf4c
vtable B: 0x5574802e5d38
------------
sizeof b2: 16
addr B2:
0x7ffc2868cf50
0x7ffc2868cf58
0x7ffc2868cf5c
vtable B2: 0x5574802e5d10
------------
sizeof d:40
addr D:
0x7ffc2868cf60
0x7ffc2868cf68
0x7ffc2868cf6c
0x7ffc2868cf78
0x7ffc2868cf7c
0x7ffc2868cf80
0x7ffc2868cf84
vtable D: 0x5574802e5ca8
------------
B::f
B::g
B::h
D::df
D::dg
D::dh
B2::f
B2::g
B2::h

这次的内存布局为:
技术图片
可以看到:
每个基类都有虚函数表;子类的函数被放到第一个第一个基类的虚函数表中。

6.多重继承有重写

#include <stdio.h>

class B {
public:
  virtual void f() {
    printf("B::f
");
  }
  virtual void g() {
    printf("B::g
");
  }
  virtual void h() {
    printf("B::h
");
  }

  int m;
  int n;
};

class B2 {
public:
  virtual void f() {
    printf("B2::f
");
  }
  virtual void g() {
    printf("B2::g
");
  }
  virtual void h() {
    printf("B2::h
");
  }

  int i,j;
};

class D : public B, public B2 {
public:
  virtual void f() {
    printf("D::f
");
  }
  virtual void dg() {
    printf("D::dg
");
  }
  virtual void dh() {
    printf("D::dh
");
  }

  int x,y;
};

using Func = void(*)(void);

int main() {
  B b;
  printf("sizeof b: %ld
", sizeof(b));
  printf("addr B:
%p
%p
%p
", &b, &b.m, &b.n);
  printf("vtable B: %p
", *(long***)&b);
  printf("------------
");

  B2 b2;
  printf("sizeof b2: %ld
", sizeof(b2));
  printf("addr B2:
%p
%p
%p
", &b2, &b2.i, &b2.j);
  printf("vtable B2: %p
", *(long***)&b2);
  printf("------------
");

  D d;
  printf("sizeof d:%ld
", sizeof(d));
  printf("addr D:
%p
%p
%p
%p
%p
%p
%p
", &d, &d.m, &d.n, &d.i, &d.j, &d.x, &d.y);
  printf("vtable D: %p
", *(long***)&d);
  printf("------------
");

  // first vtable pointer
  long** vt1 = (long**)*(long***)&d;
  Func p = nullptr;
  for (int i = 0; i < 5; ++i)
  {
    p = (Func)*(vt1+i);
    p();
  }

  // second vtable pointer. member 'm' and 'n' type is int, pointer +2
  long** vt2 = (long**)*((long***)&d + 2);
  for (int i = 0; i < 3; ++i)
  {
    p = (Func)*(vt2+i);
    p();
  }

  return 0;
}

结果:

sizeof b: 16
addr B:
0x7ffe091424e0
0x7ffe091424e8
0x7ffe091424ec
vtable B: 0x5598cb9edd38
------------
sizeof b2: 16
addr B2:
0x7ffe091424f0
0x7ffe091424f8
0x7ffe091424fc
vtable B2: 0x5598cb9edd10
------------
sizeof d:40
addr D:
0x7ffe09142500
0x7ffe09142508
0x7ffe0914250c
0x7ffe09142518
0x7ffe0914251c
0x7ffe09142520
0x7ffe09142524
vtable D: 0x5598cb9edcb0
------------
D::f
B::g
B::h
D::dg
D::dh
D::f
B2::g
B2::h

此时的内存布局为:
技术图片
基类虚函数表中的f函数都被替换成了子类的函数指针

以上是关于虚函数实现原理之虚函数表的主要内容,如果未能解决你的问题,请参考以下文章

C++性能榨汁机之虚函数的开销

多态实现之虚函数

C++面试题之虚函数(表)实现机制

C++之虚函数和虚函数表

C++之虚函数和多态

C++之虚函数