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

引言

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

类的内存布局

1.普通类

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

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

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

  return 0;
}

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

8
0x7ffee48dae00, 0x7ffee48dae00, 0x7ffee48dae04

2.有虚函数的类

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

  int m;
  int n;
};

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

  B b;
  printf("%p\n%p\n%p\n", &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\n");
  }
  virtual void g() {
    printf("B::g\n");
  }
  virtual void h() {
    printf("B::h\n");
  }

  int m;
  int n;
};

using Func = void(*)(void);

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

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

  long** vt = (long**)*(long***)&b;
  printf("vtable addr: %lx\n", (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\n");
  }
  virtual void g() {
    printf("B::g\n");
  }
  virtual void h() {
    printf("B::h\n");
  }

  int m;
  int n;
};

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

  int x,y;
};

using Func = void(*)(void);

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

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

  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\n");
  }
  virtual void g() {
    printf("B::g\n");
  }
  virtual void h() {
    printf("B::h\n");
  }

  int m;
  int n;
};

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

  int x,y;
};

using Func = void(*)(void);

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

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

  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\n");
  }
  virtual void g() {
    printf("B::g\n");
  }
  virtual void h() {
    printf("B::h\n");
  }

  int m;
  int n;
};

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

  int i,j;
};

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

  int x,y;
};

using Func = void(*)(void);

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

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

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

  // 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\n");
  }
  virtual void g() {
    printf("B::g\n");
  }
  virtual void h() {
    printf("B::h\n");
  }

  int m;
  int n;
};

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

  int i,j;
};

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

  int x,y;
};

using Func = void(*)(void);

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

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

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

  // 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函数都被替换成了子类的函数指针

原文地址:https://www.cnblogs.com/zuofaqi/p/12152468.html

时间: 2024-10-12 19:06:34

虚函数实现原理之虚函数表的相关文章

虚函数列表: 取出方法 // 虚函数工作原理和(虚)继承类的内存占用大小计算 32位机器上 sizeof(void *) // 4byte

#include <iostream> using namespace std; class A { public: A(){} virtual void geta(){ cout << "A:A" <<endl; } virtual void getb(){ cout << "A:B" <<endl; } }; class B :public A{ public: B(){} virtual void g

C++中虚函数工作原理和(虚)继承类…

转载请标明出处,原文地址:http://blog.csdn.net/hackbuteer1/article/details/7883531 一.虚函数的工作原理 虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数.典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式.vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到 vtbl.当

C++中虚函数工作原理和(虚)继承类的内存占用大小计算

一.虚继承情况下类的内存大小计算 当每个基类中有多个虚函数时,并且在虚继承的情况下,内存是如何分配的,如何计算类的大小,下面举例说明: #include<iostream> using namespace std; class A { public: int a; virtual void aa(){}; }; class D { public: virtual void dd(){}; }; class C { public: virtual void cc(){}; }; class B

C++中为什么构造函数不能是虚函数,析构函数是虚函数

一, 什么是虚函数? 简单地说,那些被virtual关键字修饰的成员函数,就是虚函数.虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离:用形象的语言来解释就是实现以共同的方法,但因个体差异而采用不同的策略. 所谓虚函数就是多态情况下只执行一个,而从继承的概念来讲,总是要先构造父类对象,然后才能是子类对象,如果构造函数设为虚函数,那么当你在构造父类的构造函数时就不得不显示的调用构造,还有一个原因就是为了防错,试想如果你在子类中一不小心重写了个跟

C++之虚函数的原理

1. 看看一个类中如果有虚函数,它的内存布局: class A{ double i; int j; virtual void foo(){} virtual void fun(){} }; 内存布局: 1> class A size(24): 1> +--- 1> 0 | {vfptr} 1> 8 | i 1> 16 | j 1> | <alignment member> (size=4) 1> +--- 可以看出,A中有一个vfptr,这个是指向一个

C++中虚函数实现原理揭秘

编译器到底做了什么实现的虚函数的晚绑定呢?我们来探个究竟.      编译器对每个包含虚函数的类创建一个表(称为V TA B L E).在V TA B L E中,编译器放置特定类的虚函数地址.在每个带有虚函数的类 中,编译器秘密地置一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的V TA B L E.通过基类指针做虚函数调 用时(也就是做多态调用时),编译器静态地插入取得这个V P T R,并在V TA B L E表中查找函数地址的代码,这样就能调用正确的函数使

C++虚函数实现原理详解

前言 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态",这是一种泛型技术.所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法.比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议. 关于虚函数的使用方法,我在这里不做过多的阐述.大家可以看看相关的C++的书籍.在这篇文章中,我只想从虚函数的实现机制上面为大家

[转]虚函数实现原理

前言 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有“多种形态”,这是一种泛型技术.所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法.比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议. 关于虚函数的使用方法,我在这里不做过多的阐述.大家可以看看相关的C++的书籍.在这篇文章中,我只想从虚函数的实现机制上面为大家一个清晰的

【整理】C++虚函数及其继承、虚继承类大小

参考文章: http://blog.chinaunix.net/uid-25132162-id-1564955.html http://blog.csdn.net/haoel/article/details/1948051/ 一.虚函数与继承 1.空类,空类单继承,空类多继承的sizeof #include <iostream> using namespace std; class Base1 { }; class Base2 { }; class Derived1:public Base1