关于虚函数表的一些讨论
1、准备工作,VS2013认为数组名和指针并不是完全等价的。举个栗子:
下面的代码,揭示了它们之间的部分区别:
#include <iostream>
using namespace std;
int main()
{
int array[][5] = { 5, 2, 3, 4, 5, 5, 2, 3, 4, 5, 5, 2, 3, 4, 5 };
cout << "针对数组array:" << endl;
cout << array << endl; //输出首地址
cout << *array << endl; //输出首地址
cout << **array << endl; //输出数组首元素
cout << endl;
int** ppi = reinterpret_cast<int**>(array); //强转
cout << "针对指针ppi:" << endl;
cout << ppi << endl; //输出数组array的首地址,没问题
//输出ppi指针指向的地址的具体元素 -- 5(编译器仍然认为是地址,因为它是int*类型)
cout << *ppi << endl;
//cout << **ppi << endl; //编译器报错,无法读取该数据
cout << endl;
cout << "sizeof(array): " << sizeof(array) << endl; //输出二维数组的大小
cout << "sizeof(*array): " << sizeof(*array) << endl; //输出二维数组一个维度的大小
cout << "sizeof(ppi): " << sizeof(ppi) << endl; //输出该指针的大小
cout << "sizeof(*ppi): " << sizeof(*ppi) << endl; //输出该指针的大小
return 0;
}
运行效果: 注:如果不了解 reinterpret_cast ,建议阅读《More Effective C++》条款2:最好使用C++转型操作符。
由运行效果,我们至少可以得到以下几点假设:
a、VS2013认为数组名和指针并不是完全等价的。
b、n维数组的数组名如果如果对其取值,将会得到(n-1)维数组的地址,但是,对指向同样内容的n重指针,
得到的则会是该地址下的具体元素值(虽然,编译器依旧会认为它是地址)。
2、预备知识,虚函数的工作原理
通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组(虚函数表)的指针。
虚函数表保存了为类对象声明的虚函数的指针。无论表的大小如何,在类中只需要保存1个地址成员即可。
—— 摘录自《C++ Primer Plus》:虚成员函数和动态绑定
请注意黑体字部分说明了在类内部并没有真正的含有虚函数表,而是只是拥有虚函数表的指针而已。
(这在《More Effective C++》条款24中也有详细地讨论,推荐去看看。)
3、VS2013中的虚函数表,先来个例子压压惊。
#include <iostream>
#include <string>
using namespace std;
typedef void(*FUN)();
typedef void(*FUN1)(string);
typedef string (*FUN2)();
class Base
{
private:
virtual void A()
{
cout << "I'm Base::A()." << endl;
}
void B()
{
cout << "I'm Base::B()." << endl;
}
virtual void C()
{
cout << "I'm Base::C()." << endl;
}
virtual void D(string x)
{
cout << "I'm Base::D(" << x << ")." << endl;
}
virtual string E()
{
return "I'm string Base::E().";
}
};
int main()
{
Base* base = new Base;
int* pi = reinterpret_cast<int*>(base); //1、将base*转换为int*,这就是指向vtbl的指针
int i = *pi; //2、获取base的首地址指向的内容,也就是vtbl的首地址
FUN* pf = reinterpret_cast<FUN*>(i); //将虚函数的首地址转化为该虚函数指针
pf[0](); //第一个虚函数
pf[1](); //第二个虚函数
//第三个虚函数是FUN1类型的
//reinterpret_cast<FUN1>(pf[2])("string");//输出:I'm Base::D(string).有错误提示
//第四个虚函数是FUN2类型的
//cout << reinterpret_cast<FUN2>(pf[3])() << endl; //无输出,有错误提示
return 0;
}
测试效果: 由测试效果,我们可以推测以下结论:
a、在VS2013中,类的指针,指向虚函数表的指针都是该类的首地址。
b、由虚函数D和E在VS2013中的测试效果,我有以下推测:
i、从编译器对D函数的暧昧表现来看,VS2013的虚函数表存储虚函数的指针是可能是将它看着是一个字节的bit串来存储的。
这或许就能解释为什么各种不同类型的函数指针能在一个虚函数表内保存了。
ii、至于为什么编译器VS2013会拒绝对后面函数的访问,可能是由于VS2013看不惯用户对不同类型的函数指针进行强转吧。
因为,D和E函数报的错误都是下图的情况:
c、由于文章开头的结论,所以上述推论,不一定正确。
4、其他 (TODO)
5、声明:
1、本文的讨论大部分出自作者根据实验结果的一些主观臆测,不代表任何权威观点。
2、本文的测试代码和运行结果均是基于VS2013,对于其他编译器,运行结果听天由命。