对象模型
在C++中,有两种数据成员(class data members):static成员 和nonstatic成员,以及三种类成员函数(class member functions):static、nonstatic和virtual:
接下来我们看三种对象模型是如何存放成员变量的
简单对象模型
在该模型下,对象由一系列的指针组成,每一个指针都指向一个数据成员或成员函数,顺序按声明顺序排列,也就是说,每个数据成员和成员函数在类中所占的大小是相同的,都为一个指针的大小。
表格驱动对象模型
把成员函数放在一个成员函数表中,每个slot指向一个成员函数,slot中存放的是成员函数指针;把数据成员放在一个数据成员表中,存放数据本身。成员对象包含两个指针分别指向前面这两个表格。
C++对象模型
在此模型下,nonstatic 数据成员被置于每一个类对象中,而static数据成员被置于类对象之外。static与nonstatic函数也都放在类对象之外,而对于virtual 函数,则通过虚函数表+虚指针来支持,具体如下:
- 每个类生成一个表格,称为虚表(virtual table,简称vtbl)。虚表中存放着一堆指针,这些指针指向该类每一个虚函数。虚表中的函数地址将按声明时的顺序排列,不过当子类有多个重载函数时例外。
- 每个类对象都拥有一个虚表指针(vptr),指向相关的虚函数表vtbl,由编译器为其生成。虚表指针的设定与重置皆由类的复制控制(也即是构造函数、析构函数、赋值操作符)来完成。vptr的位置为编译器决定,传统上它被放在所有显示声明的成员之后,不过现在许多编译器把vptr放在一个类对象的最前端。
- 虚函数表的前面设置了一个指向type_info的指针,用以支持RTTI(Run Time Type Identification,运行时类型识别)。RTTI是为多态而生成的信息,包括对象继承关系,对象本身的描述等,只有具有虚函数的对象在会生成。
这个模型的优点在于它的空间和存取时间的效率;缺点如下:如果应用程序本身未改变,但当所使用的类的non static数据成员添加删除或修改时,需要重新编译。
对象b含有一个__vfptr,即vprt。并且只有nonstatic数据成员被放置于对象内。vfptr中有两个指针类型的数据(地址),第一个指向了Base类的析构函数,第二个指向了Base的虚函数print,顺序与声明顺序相同。
代码演示
void testBase(Base& p)
{
cout << "对象的内存起始地址:" << &p << endl;
cout << "虚函数表地址:" << (int*)(&p) << endl;
//验证虚表
cout << "虚函数表第一个函数的地址:" << (int*)*((int*)(&p)) << endl;
cout << "析构函数的地址:" << (int*)*(int*)*((int*)(&p)) << endl;
cout << "虚函数表中,第二个虚函数即print()的地址:" << ((int*)*(int*)(&p) + 1) << endl;
//通过地址调用虚函数print()
typedef void(*Fun)(void);
Fun IsPrint = (Fun) * ((int*)*(int*)(&p) + 1);
cout << endl;
cout << "调用了虚函数";
IsPrint(); //若地址正确,则调用了Base类的虚函数print()
cout << endl;
//输入static函数的地址
p.countI();//先调用函数以产生一个实例
cout << "static函数countI()的地址:" << p.countI << endl;
//验证nonstatic数据成员
cout << "推测nonstatic数据成员baseI的地址:" << (int*)(&p) + 1 << endl;
cout << "根据推测出的地址,输出该地址的值:" << *((int*)(&p) + 1) << endl;
cout << "Base::getI():" << p.getI() << endl;
}
int main() {
Base b(1000);
testBase(b);
cout << endl;
}
结果分析
虚函数表的第一个位置是析构函数,第二个位置是虚函数print函数,可以通过地址而不是对象调用它。
虚表指针的下一个位置是非静态成员变量,静态成员函数的地址段与虚表指针和非静态成员的地址段不同。