C++ 虚函数详解(虚函数表、vfptr)——带虚函数表的内存分布图
前言
总所周知,虚函数是实现多态的基础。
- 引用或指针的静态类型与对象本身的动态类型的不同,才是C++支持多态的根本所在。
- 当使用基类的引用或指针调用一个虚函数成员时,会执行动态绑定。
- 所有的虚函数都必须有定义,因为编译器直到运行前也不知道到底要调用哪个版本的虚函数。
- 只有通过指针或引用调用虚函数才会发生动态绑定,因为只有这种情况,引用或指针的静态类型与对象本身的动态类型才会不同。
关于另一篇博客
大家在网上搜索关于虚函数的博客应该都会搜到陈皓写的那篇C++ 虚函数表解析吧,这篇文章确实不错,画的图也比较好理解,对于指针理解比较深刻的人应该不会理解错误,但对于新人来说可能还是有点不友好。以下几点我觉得需要强调:
- 虚函数表的指针,实质是指针的指针。
- 虚函数表的内容,实质是一个指针的数组。(同时辅证了上一点)
- 在图例中,所以就会两次指针指向的过程。
还有一点就是在该大神的例子程序的输出中,给出的中文解释我认为是错误的,看起来是很容易误导人的。 最开始的例子程序中的:
cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
这两句明显错误,本人在困惑之余便开始了自己的验证。
而且图例中也应该有两次指针指向的过程。
虚函数表(vfptr)
虚函数表的指针存储在对象实例中最前面的位置。
这意味着我们可以通过对象实例的地址得到这个虚函数表的指针,然后就遍历虚函数表中的各个函数指针,然后调用相应的函数。
下面开始各个例子程序的实验!(win10+vs2017)
只有基类
#include "pch.h"
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
int main()
{
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
Base * p = &b;
cout << "该对象的地址:" << p << endl;
cout << "虚函数表的指针也是从这个地址"<< (int*)(&b) <<"开始存的" << endl << endl;
cout << "虚函数表的指针指向的地址10进制:" << *(int*)(&b) << "即虚函数表的指针存的内容"<<endl;
cout << "即虚函数表的地址:" << (int*)*(int*)(&b) << endl << endl;
pFun = (Fun)*(int*)*(int*)(&b);//第一个虚函数的指针
cout << "第一个虚函数的地址:" << pFun << endl;
pFun();
Fun gFun = NULL;
gFun = (Fun)*((int*)*(int*)(&b) + 1);//第二个虚函数的指针
Fun hFun = NULL;
hFun = (Fun)*((int*)*(int*)(&b) + 2);//第三个虚函数的指针
}
- 理解内存里每个字节是有编号,这个编号便是我们说的地址。
- 指针存的是一个地址,我们只关心指针指向的地址(指针存的地址)和指向对象的类型,而不关心指针这个对象本身的地址。
- 对指针解引用,实际是从指针指向的地址的那个字节开始,按照指向对象的类型的字节大小n,读取n个字节出来,来组成这个类型的对象。
- 打印指针时,会打印出来指针指向的地址,以16进制。
b
返回Base
类型的对象。&b
返回Base *
类型的指针。(int *)(&b)
将Base *
类型的指针转换为int *
类型的指针,转换后指针指向地址没变,但指向对象的类型变了。*(int *)(&b)
对int *
类型的指针解引用,从地址开始的那个字节开始,取出sizeof(int)个字节,赋值给一个int对象(因为指针认为自己指向一个int对象)。(int *)*(int *)(&b)
相当于(int *)
后接一个int值,返回一个int指针,将这个int值作为该指针指向的地址值。*(int *)*(int *)(&b)
对int *
类型的指针解引用,返回int值。(Fun)*(int *)*(int *)(&b)
,Fun是函数指针,后接一个int值,将这个int值作为该函数指针指向的地址值。- 如果以上过程你都正确理解,那么你就能理解这句
gFun = (Fun)*((int*)*(int*)(&b) + 1);
了。首先(int*)*(int*)(&b)
将虚函数表的指针转换为指向指针数组首元素的指针(即转换过程中,指针指向地址没变的),然后((int*)*(int*)(&b) + 1)
这里就是数组的指针的正常操作,现在这个指针指向了数组的第二个元素(即第二个虚函数指针),最后就是解引用,然后转换为Fun
函数指针。
如果你还没有理解某个步骤,建议直接查看以下图例的大图,配合debug显示的局部变量表使用,再回头看整个过程。
上图解释了虚函数的实现机制:
- 在有虚函数的基类对象中,肯定至少有三块不同的内存存储区域。
- 首先是对象内存空间,其开始区域,存了虚函数表的指针。
- 虚函数表实际是一个指针的数组,这些指针就是虚函数的函数指针。
- 最后是各个虚函数的存储区域。
虚函数表的结束标志
在上面例子中还需要讲一个细节,在虚函数表最后位置有一个字节用来标志虚函数表的结束。
char* end = NULL;
end = (char*)((int*)*(int*)(&b) + 3);
加入如上代码便可以得到结束标志,((int*)*(int*)(&b) + 3)
这里指向了虚函数表即指针数组的第四个元素,但实际上数组里只有三个指针,所以这里便刚好指向了结束标志。再通过(char*)
转换指针类型,代表指向的是一个字节。
由于我是第二次运行程序,所以地址有点不一样。这里end指针存的地址,按照之前的例子应该是0x00305b38再加12。
这里你最好再明确下char型存储的含义:(即ASCII表中:是int型<---->char型的相互转换关系)
char end1 = '\0';//字符串的结束符
char end2 = 0;//字符串的结束符
char zero1 = '0';//这才是真正的字符0
char zero2 = 48;
单继承(无虚函数覆盖)
在此例中,基类有三个虚函数,派生类也有三个虚函数,但派生类一个虚函数也没有去重写。
#include "pch.h"
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
class Derive : public Base {
public:
virtual void f1() { cout << "Derive::f" << endl; }
virtual void g1() { cout << "Derive::g" << endl; }
virtual void h1() { cout << "Derive::h" << endl; }
};
int main()
{
typedef void(*Fun)(void);
Derive d;
Base *p = &d;
Fun fFun = NULL;
fFun = (Fun)*((int*)*(int*)(&d) + 0);//第一个虚函数的指针
Fun gFun = NULL;
gFun = (Fun)*((int*)*(int*)(&d) + 1);//第二个虚函数的指针
Fun hFun = NULL;
hFun = (Fun)*((int*)*(int*)(&d) + 2);//第三个虚函数的指针
Fun f1 = NULL;
f1 = (Fun)*((int*)*(int*)(&d) + 3);
Fun g1 = NULL;
g1 = (Fun)*((int*)*(int*)(&d) + 4);
Fun h1 = NULL;
h1 = (Fun)*((int*)*(int*)(&d) + 5);
char* end = NULL;
end = (char*)((int*)*(int*)(&d) + 6);
}
虽然虚函数表里只能显示父类的虚函数,但通过增加数组指针的方法,一样可以获得派生类的虚函数指针。就算这里是Derive *p1 = &d;
也一样,只显示基类的三个虚函数。
- 虚函数指针按照声明顺序放在虚函数表里面。
- 基类的虚函数在派生类的虚函数前面。
虚函数表的内存模型如下:
但这里我已经厌倦了给每个虚函数生成一个函数指针,所以可以用以下循环:
int main()
{
typedef void(*Fun)(void);
Derive d;
int *vTable = (int *)*(int *)(&d);//虚函数表的指针
for (int i = 0; i<6; ++i)//判断条件写成vTable[i] != 0,有可能会报异常
{
printf("function : %d :0X%x->", i, vTable[i]);
Fun f = (Fun)(vTable[i]);
f(); //访问虚函数
}
}
vTable[i]
相当于给vTable指针加i
,再解引用。其实就是数组的用法啦,所以就少了解引用的一步。
打印出来的是各个虚函数的地址。
单继承(有虚函数覆盖)
在此例中,派生类只覆盖了基类的一个函数:f()。
#include "pch.h"
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
class Derive : public Base {
public:
virtual void f() { cout << "Derive::f" << endl; }
virtual void g1() { cout << "Derive::g" << endl; }
virtual void h1() { cout << "Derive::h" << endl; }
};
int main()
{
typedef void(*Fun)(void);
Derive d;
int *vTable = (int *)*(int *)(&d);
for (int i = 0; i<5; ++i)
{
printf("function : %d :0X%x->", i, vTable[i]);
Fun f = (Fun)(vTable[i]);
f(); //访问虚函数
}
}
可以看出:
- 由于f虚函数被重写,原本虚函数表(即指针数组)第一个元素是Base::f()的指针,现在被替换为了Derive::f()的指针
- 其他虚函数按照之前的顺序排列
虚函数表的内存模型如下:
多重继承(无虚函数覆盖)
在此例中,有三个基类,一个派生类,且派生类一个虚函数也没有去重写。
#include "pch.h"
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void f() { cout << "Base1::f" << endl; }
virtual void g() { cout << "Base1::g" << endl; }
virtual void h() { cout << "Base1::h" << endl; }
};
class Base2 {
public:
virtual void f() { cout << "Base2::f" << endl; }
virtual void g() { cout << "Base2::g" << endl; }
virtual void h() { cout << "Base2::h" << endl; }
};
class Base3 {
public:
virtual void f() { cout << "Base3::f" << endl; }
virtual void g() { cout << "Base3::g" << endl; }
virtual void h() { cout << "Base3::h" << endl; }
};
class Derive : public Base1,public Base2, public Base3 {
public:
virtual void f1() { cout << "Derive::f" << endl; }
virtual void g1() { cout << "Derive::g" << endl; }
virtual void h1() { cout << "Derive::h" << endl; }
};
typedef void(*Fun)(void);
void printVfun(int n,int * vTable) {
for (int i = 0; i < n; ++i)
{
printf("function : %d :0X%x->", i, vTable[i]);
Fun f = (Fun)(vTable[i]);
f(); //访问虚函数
}
cout << "" << endl;
}
int main()
{
Derive d;
int *vTable1 = (int *)*(int *)(&d);//第一个虚函数表的指针
printVfun(6, vTable1);
int *vTable2 = (int *)*((int *)(&d)+1);//第二个虚函数表的指针
printVfun(3, vTable2);
int *vTable3 = (int *)*((int *)(&d) + 2);//第三个虚函数表的指针
printVfun(3, vTable3);
}
可以看到:
- 对于继承到的每个基类,都有一个对应的虚函数表。
- 派生类的虚函数的指针,被放进了第一个基类对应的虚函数表里。(按照声明顺序来判断的)
内存模型如下:
多重继承(有虚函数覆盖)
在此例中,有三个基类,一个派生类,且派生类重写了三个基类的同一个虚函数。
#include "pch.h"
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void f() { cout << "Base1::f" << endl; }
virtual void g() { cout << "Base1::g" << endl; }
virtual void h() { cout << "Base1::h" << endl; }
};
class Base2 {
public:
virtual void f() { cout << "Base2::f" << endl; }
virtual void g() { cout << "Base2::g" << endl; }
virtual void h() { cout << "Base2::h" << endl; }
};
class Base3 {
public:
virtual void f() { cout << "Base3::f" << endl; }
virtual void g() { cout << "Base3::g" << endl; }
virtual void h() { cout << "Base3::h" << endl; }
};
class Derive : public Base1, public Base2, public Base3 {
public:
virtual void f() { cout << "Derive::f" << endl; }
virtual void g1() { cout << "Derive::g" << endl; }
virtual void h1() { cout << "Derive::h" << endl; }
};
typedef void(*Fun)(void);
void printVfun(int n, int * vTable) {
for (int i = 0; i < n; ++i)
{
printf("function : %d :0X%x->", i, vTable[i]);
Fun f = (Fun)(vTable[i]);
f(); //访问虚函数
}
cout << "" << endl;
}
int main()
{
Derive d;
int *vTable1 = (int *)*(int *)(&d);//第一个虚函数表的指针
printVfun(5, vTable1);
int *vTable2 = (int *)*((int *)(&d) + 1);//第二个虚函数表的指针
printVfun(3, vTable2);
int *vTable3 = (int *)*((int *)(&d) + 2);//第三个虚函数表的指针
printVfun(3, vTable3);
}
可以看到:
- 三个基类的虚函数表的第一项,都被替换为Derive::f的指针
- 这样任意基类指针指向派生类对象,都可以调用到Derive::f
对象模型如下:
类与虚函数表与虚函数的对应关系
注意本章中的示意图都只会关注基类的虚函数指针。或者因为重写,而导致在虚函数表中基类的虚函数指针被替换的情况。(就像局部变量图中的一样)
单继承(无虚函数覆盖)
在该例中运行:
Base b1;
Base b2;
Derive d1;
Derive d2;
- 每一个类对应到一个虚函数表。
- 两个虚函数表里各个指针指向的地址都是相同的。
单继承(有虚函数覆盖)
Base b;
Derive d;
- 基类的虚函数表的三项还是没有变化
- 派生类的虚函数表的第一项被替换了
多重继承(无虚函数覆盖)
Base1 b1;
Base2 b2;
Base3 b3;
Derive d;
- 派生类因为继承了三个基类,所以会有三张虚函数表。
多重继承(有虚函数覆盖)
- 派生类的每个虚函数表的第一项都被替换为Derive::f()的指针了,因为它把三个基类的f虚函数都重写了。