在C++对象模型之简述C++对象的内存布局一文中,详细分析了各种成员变量和成员函数对一个类(没有任何继承的)对象的内存分布的影响,及详细讲解了如何遍历对象的内存,包括虚函数表。如果你在阅读本文之前,还没有看过C++对象模型之简述C++对象的内存布局一文,建议先阅读一下。而本文主要讨论继承对于对象的内存分布的影响,包括:继承后类的对象的成员的布局、继承对于虚函数表的影响、virtual函数机制如何实现、运行时类型识别等。由于在C++中继承的关系比较复杂,所以本文会讨论如下的继承情况:
1)单一继承
2)多重继承
3)重复继承
4)单一虚拟继承
5)钻石型虚拟继承
此外,当一个类作为一个基类时,它的析构函数应该是virtual函数,这样下面的代码才能正确地运行
Base *p = new Derived;
...
delete p;
在本文的例子,为了验证虚函数表的内容,会遍历并调用虚函数表中的所有函数。但是当析构函数为virtual时,在遍历的过程中就会调用到对象的析构函数,从而对对象进行析构的操作,导致接下来的调用出错。但是本文的目的是分析和验证C++对象的内存布局,而不是设计一个软件,析构函数为非virtual函数,并不会影响我们的分析和理解,因为virtual析构函数与其他的virtual函数是一样的,只是做的事不一样。所以在本文中的例子中,析构函数均不为virtual,特此说明一下。
同时为了调用的方便,所有的virtual的函数原型均为:返回值为void,参数也为void。
注:以下的例子中的测试环境为:32位Ubuntu 14.04 g++ 4.8.2,若在不同的环境中进行测试,结果可能有不同。
1、根据指向虚函数表的指针(vptr)遍历虚函数表
由于在访问对象的内存时,都要遍历虚函数表来确定虚函数表中的内容,所以对这部分的功能抽象出来,写成一个函数,如下:
void visitVtbl(int **vtbl, int count)
{
cout << vtbl << endl;
cout << "\t[-1]: " << (long)vtbl[-1] << endl;
typedef void (*FuncPtr)();
for (int i = 0; vtbl[i] && i < count; ++i)
{
cout << "\t[" << i << "]: " << vtbl[i] << " -> ";
FuncPtr func = (FuncPtr)vtbl[i];
func();
}
}
代码解释:
参数vtbl为虚函数表的第一个元素的地址,也就是对象中的vptr的值。参数count指的是该虚函数表中虚函数的数量。由于虚函数表中保存的信息并不全是虚函数的地址,也不是所有的虚函数表中都以NULL表示虚函数表中的函数地址已经到了尽头。所以为了让测试程序更好地运行,所以加上这一参数。
虚函数表保存的是函数的指针,若把虚函数表当作一个数组,则要指向该数组需要一个双指针,即参数中的int **vtbl,获取函数指针的值,即获取数组中元素的值,可以通过vtbl[i]来获得。
虚函数表中还保存着对象的类型信息,通常为了便于查找对象的类型信息,使用虚函数表中的索引(下标)为-1的位置保存该类对应的类型信息对象(即类std::type_info的对象)的地址,即保存在第一个虚函数的地址之前。
2、单一继承
类的具体代码如下:
class Base
{
public:
Base()
{
mBase1 = 101;
mBase2 = 102;
}
virtual void func1()
{
cout << "Base::func1()" << endl;
}
virtual void func2()
{
cout << "Base::func2()" << endl;
}
private:
int mBase1;
int mBase2;
};
class Derived : public Base
{
public:
Derived():
Base()
{
mDerived1 = 1001;
mDerived2 = 1002;
}
virtual void func2()
{
cout << "Derived::func2()" << endl;
}
virtual void func3()
{
cout << "Derived::func3()" << endl;
}
private:
int mDerived1;
int mDerived2;
};
使用如下的代码进行测试:
int main()
{
Derived d;
char *p = (char*)&d;
visitVtbl((int**)*(int**)p, 3);
p += sizeof(int**);
cout << *(int*)p << endl;
p += sizeof(int);
cout << *(int*)p << endl;
p += sizeof(int);
cout << *(int*)p << endl;
p += sizeof(int);
cout << *(int*)p << endl;
return 0;
}
代码解释:
在测试代码中,最难明白的就是以下语句中的参数:
visitVtbl((int**)*(int**)p, 3);
char指针p指向了对象中的vptr,由于vptr也是一个指针,所以p应该是一个双指针,对其解引用(*p)可以获得vptr的值。然而在同一个系统中,无论是什么类型的指针,其占用的内存大小都是相同的(一般在32位系统中为4字节,64位系统中为8字节),所以可以通过以下语句获取vptr的值: