C++虚函数表

虚函数表

C++中虚函数是通过一张虚函数表(Virtual Table)来实现的,在这个表中,主要是一个类的虚函数表的地址表;这张表解决了继承、覆盖的问题。在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以当我们用父类的指针来操作一个子类的时候,这张虚函数表就像一张地图一样指明了实际所应该调用的函数。

C++编译器是保证虚函数表的指针存在于对象实例中最前面的位置(是为了保证取到虚函数表的最高的性能),这样我们就能通过已经实例化的对象的地址得到这张虚函数表,再遍历其中的函数指针,并调用相应的函数。

下面先看一段代码:

class Base {
public:
	virtual void f() { cout << "Base::f()" << endl; }
	virtual void g() { cout << "Base::g()" << endl; }
	virtual void h() { cout << "Base::h()" << endl; }
};

typedef void(*Fun)(void);

int main()
{
	Base b;
	Fun pFun = NULL;
	cout << "虚函数表的地址为:" << (int*)(&b) << endl;
	cout << "虚函数表的第一个函数地址为:" << (int*)*(int*)(&b) << endl;

	pFun = (Fun)*((int*)*(int*)(&b));
	pFun();
	system("pause");
	return 0;
}

运行结果如下:
TIM截图20181030142610.png
我们再追踪一下虚函数表的地址:

image.png

image.png

image.png

结合结果分析一下代码:首先是创建了一个Base的类,Base类里面有三个成员函数,都为虚函数;然后typedef void(*Fun)(void)是利用类型别名声明一个函数指针,指向的地址为NULL,可以等价成这样:typedef decltype(void) *Fun。然后再到main函数里,利用Base实例化了对象了b;然后Fun pFun=NULL则是声明了一个返回指向函数的指针,该指针pFun此时也是NULL,根据图1可以知道,他的类型是void(*)(),表示的就是函数指针,而当执行完这句后就会从原来没有分配的0xcccccccc变成0x00000000。接下来就是(int*)(&b)强行把&b转成int *,取得虚函数表的地址;再次取址就可以得到第一个虚函数的地址了,也就是Base::f() 。接下来就是 pFun = (Fun)*((int*)*(int*)(&b)); 把函数指针指向虚函数表的第一个函数,最后再pFunc()运行。

如果想让pFun调用其它的函数,可以是这样:

(Fun)*((int*)*(int*)(&b)+0);  // Base::f()

(Fun)*((int*)*(int*)(&b)+1);  // Base::g()

(Fun)*((int*)*(int*)(&b)+2);  // Base::h()

通过下图可以很好的进行理解:

image.png

(int*)*(int*)(&b)可以这样理解,(int*)(&b)就是对象b的地址,只不过被强制转换成了int*了,如果直接调用*(int*)(&b)则是指向对象b地址所指向的数据,但是此处是个虚函数表呀,所以指不过去,必须通过(int*)将其转换成函数指针来进行指向就不一样了,它的指向就变成了对象b中第一个函数的地址,所以(int*)*(int*)(&b)就是独享b中第一个函数的地址;又因为pFun是由Fun这个函数声明的函数指针,所以相当于是Fun的实体,必须再将这个地址转换成pFun认识的,即加上(Fun)*进行强制转换:简要概括就是从b地址开始
读取四个字节的内容,然后将这个内容解释成一个内存地址,然后访问这个地址,然后将这个地址中存放的值再解释成一个函数的地址.

image.png

下面将对比说明有无虚函数覆盖情况下的虚函数表的样子:

一般继承(无虚函数覆盖)

先写出一个继承关系:
image.png

写成代码如下:

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 << "Base::f1()" << endl; }
	virtual void g1() { cout << "Base::g1()" << endl; }
	virtual void h1() { cout << "Base::h1()" << endl; }
};

typedef void(*Fun)(void);

int main()
{
	//Base b;
	Derive d;
	Fun pFun = NULL;
	cout << "虚函数表的地址为:" << (int*)(&d) << endl;
	cout << "虚函数表的第一个函数地址为:" << (int*)*(int*)(&d)<< endl;
	pFun =(Fun)*((int*)*(int*)(&d)+1);
	pFun();
	pFun = (Fun)*((int*)*(int*)(&d) + 3);
	pFun();

	system("pause");
	return 0;
}

执行结果如下:

image.png

通过调试看一下相应的虚函数表:

image.png

image.png

这个继承关系中,子类没有重载任何父类的函数,我们实例化了一个对象d:Derive d,则它的虚函数表是如下的:

image.png

所以虚函数按照其声明顺序放于表中,并且父类的虚函数在子类的虚函数前面

一般继承(有虚函数覆盖)

这样的一个继承关系:

image.png

这个继承关系中,Derive的f()重载了Base类中的f(),下面我们来逐步调试:

1540901666638

上图是刚刚通过Derive d声明的虚函数表的样子,我们再直接打印出对象d的第一个函数、第三个函数和第四个函数来看看:

image.png

相应的虚函数表变成了:

image.png

我们可以知道覆盖的f()函数被放到了虚函数表中原来父类虚函数的位置,而没有被覆盖的函数依次往后排列

多重继承(无虚函数覆盖)

现在用这样一个继承关系:

image.png

调试程序发现:

image.png

如果我们访问第一个函数地址之后的第6个函数位置会发生什么呢?

image.png

是找不到的,说明虚函数表已经不是按原来的方式通过一个地址找到所有的函数,或者说所有的子函数实现是按照顺序排列来存放的了。

虚函数表是这样的:

image.png

但是我们通过我们目前实现访问虚函数表的方式是访问不到下面两张虚函数表的,却可以通过这样来实现:

Derive d;
Base2 *b2=&d;
b2->f();
b2->g();

image.png

image.png

在声明了b2并绑定d的时候,已经指向了Base2的虚函数表的地址,再通过b2->f()就可以访问Base2的虚函数表中第一个函数的位置。

多重继承(有虚函数覆盖)

继承关系:

image.png

在这里就不放出代码和调试内容了,直接给出虚函数表的样子:

image.png

几点注意

1.不能通过父类型的指针访问子类自己的虚函数,是非法的

Base *b=new Derive();
b->f1();     //编译会出错

P.S:我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为

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::g1" << endl; }
};

typedef void(*Fun)(void);

int main()
{
	Fun pFun = NULL;
	Derive d;
	int** pVtab = (int**)&d;
	//Base1's vtable
	//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+0);
	pFun = (Fun)pVtab[0][0];
	pFun();
	//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+1);
	pFun = (Fun)pVtab[0][1];
	pFun();
	//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+2);
	pFun = (Fun)pVtab[0][2];
	pFun();
	//Derive's vtable
	//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+3);
	pFun = (Fun)pVtab[0][3];
	pFun();
	//The tail of the vtable
	pFun = (Fun)pVtab[0][4];
	cout << pFun << endl;
	//Base2's vtable
	//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
	pFun = (Fun)pVtab[1][0];
	pFun();
	//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
	pFun = (Fun)pVtab[1][1];
	pFun();
	pFun = (Fun)pVtab[1][2];
	pFun();
	//The tail of the vtable
	pFun = (Fun)pVtab[1][3];
	cout << pFun << endl;

	//Base3's vtable
	//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
	pFun = (Fun)pVtab[2][0];
	pFun();
	//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
	pFun = (Fun)pVtab[2][1];
	pFun();
	pFun = (Fun)pVtab[2][2];
	pFun();
	//The tail of the vtable
	pFun = (Fun)pVtab[2][3];
	cout << pFun << endl;
	return 0;

}

在这里延伸一下关于int **int *,这里引用了知乎上的一片答案:

C语言里面的定义的指针,它除了表示一个地址,它还带有类型信息。这个类型信息,用来告诉你,在这个地址空间上,存放着什么类型的变量。打个比如,有如下的代码片段:int a;
int *p = &a;
假设p的指针值为0x08004000,并且int类型长度为4字节。那么p将告诉你,[0x08004000, 0x08004004) 内存空间上存放着一个int类型。如果你只从0x08004000地址中,只读取两个字节,那是错误的。同样道理,以下代码片段:int a;
int *b = &a;
int **c = &b;
是告诉你,c的指针值是另一个指针(b),而该指针则指向一个int变量。如果你刻意要较真,认为指针就是一个地址,并且举出下面的例子:int a;
int *b = &a;
int *c = &b;  // 请留意这里
最后一句赋值,实际上是将类型信息丢弃了。因为在编译器看来,C 就表示一个指针,它指向的是一个整数,只是你自己将它解释成指针而已。printf("%d\n",*(int *)(*c)); // 开发人员,将*c解释成指针,编译器是不认帐的喔
为什么将一个整数解释成一个指针没有问题呢?那是因为凑巧,你把它放到64位架构上试试看看。

2.如果父类的虚函数是private或者是protected的,但这些非public的虚函数同样会存在于虚函数表中!
3.虚函数表不一定是存在最开头,但是目前各个编译器大多是这样设置的。
image.png

posted @ 2018-10-30 14:15  MrYun  阅读(12753)  评论(2编辑  收藏  举报