C++对象模型
1、何为C++对象模型?
C++对象模型可以概括为以下2部分:
- ① 语言中直接支持面向对象程序设计的部分
- 面向对象程序设计部分:如构造函数、析构函数、虚函数、继承(单继承、多继承、虚继承)、多态等。
- ② 对于各种支持的底层实现机制
在C++类中有两种数据成员、三种成员函数:
- 两种数据成员:static、nonstatic。
- 三种成员函数:static、nonstatic、virtual。
代码举例说明:
//base.h
#pragma once
#include<iostream>
using namespace std;
class Base
{
public:
Base(int);
virtual ~Base(void);
int getIBase() const;
static int instanceCount();
virtual void print() const;
protected:
int iBase;
static int count;
};
问:Base类在机器中我们如何构建出各种成员数据和成员函数的呢?
2、基本C++对象模型
在介绍C++使用的对象模型之前,介绍2种对象模型:简单对象模型(a simple object model)、表格驱动对象模型(a table-driven object model)。
a)简单对象模型
所有的成员占用相同的空间(跟成员类型无关),对象只是维护了一个包含成员指针的一个表。表中放的是成员的地址,无论上成员变量还是函数,都是这样处理。对象并没有直接保存成员而是保存了成员的指针。
b)表格驱动对象模型
这个模型在 简单对象模型 的基础上又添加了 一个间接层。
将成员分成函数和数据,并且用两个表格保存,然后对象只保存了 两个指向表格的指针。
表格驱动对象模型 可以保证所有的 对象 具有相同的大小(只保存俩指针); 简单对象模型 的类对象还与成员的个数相关。
- 其中数据成员表中包含实际数据;
- 函数成员表中包含的实际函数的地址(与数据成员相比,多一次寻址)。
c)C++对象模型
在C++对象模型中:
- ① nonstatic 数据成员被放置到对象内部;
- ② static数据成员、static and nonstatic 函数成员均被放到对象之外。
- ③ 对虚函数的支持分为两步:
- a)每个class会为每个虚函数生成一个指针,这些指针统一放在虚函数表中(vtbl)
- b)每个class的对象都会添加一个指针(vptr),指向相关的虚函数表(vtbl)。
- vptr的设定(setting)和重置(resetting)都由每一个class的构造函数,析构函数和拷贝赋值运算符自动完成。
- ④ 另外,虚函数表地址的前面设置了一个指向type_info类的指针。
- C++提供了一个type_info类来获取对象类型信息。
C++对象模型优点与缺点:
- 优点:在于它的空间和存取时间的效率
- 缺点:当所使用的类的non static数据成员添加删除或修改时,需要重新编译。
d)模型验证测试
class Base
{
public:
Base(int i) :baseI(i){};
virtual void print(void){ cout << "调用了虚函数Base::print()"; }
virtual void setI(){cout<<"调用了虚函数Base::setI()";}
virtual ~Base(){}
private:
int baseI;
};
-
当一个类本身定义了虚函数,或其父类有虚函数时,为了支持多态机制,编译器将为该类添加一个虚函数指针(vptr)。
-
虚函数指针一般都放在 对象内存布局的第一个位置 上,这是为了保证在多层继承或多重继承的情况下能以最高效率取到虚函数表。
-
当vptr位于对象内存最前面时,对象的地址即为虚函数指针地址。我们可以取得虚函数指针的地址:
Base b(1000); int *vptrAddr = (int *)(&b); //强行把类对象的地址转换为 int* 类型,取得了虚函数指针的地址。
-
虚函数指针指向虚函数表,虚函数表中存储的是一系列虚函数的地址,虚函数地址出现的顺序与类中虚函数声明的顺序一致。
-
对虚函数指针地址值,可以得到虚函数表的地址,也即是虚函数表第一个虚函数的地址:
typedef void(*Fun)(void); Fun vfunc = (Fun)*( (int *)*(int*)(&b)); /* 取出虚函数表指针的值: *(int*)(&b); 它是一个地址,虚函数表的地址 把虚函数表的地址强制转换成 int* :(int *)*(int*)(&b); 再把它转化成我们Fun指针类型 : (Fun)*(int*)*(int*)(&b) */ cout << "第一个虚函数的地址是:" << (int *)*(int*)(&b) << endl; cout << "通过地址,调用虚函数Base::print():"; vfunc();
3、C++模型中加入单继承
class Base
{
public:
Base(int i) :baseI(i){};
virtual void print(void){ cout << "调用了虚函数Base::print()"; }
//virtual void setI(){cout<<"调用了虚函数Base::setI()";}
virtual ~Base(){}
private:
int baseI;
};
class Derive : public Base
{
public:
Derive(int d) :Base(1000), DeriveI(d){};
//override父类虚函数
virtual void print(void){ cout << "Drive::Drive_print()" ; }
// Derive声明的新的虚函数
virtual void Drive_print(){ cout << "Drive::Drive_print()" ; }
virtual ~Derive(){}
private:
int DeriveI;
};
简单继承下有重写的C++对象模型: 无重写的话,在子类虚函数表中是不会覆盖父类虚函数的。
继承类图为:
4、C++模型中加入多继承
4.1 一般的多重继承(非菱形继承)
从单继承可以知道,派生类中只是扩充了基类的虚函数表。如果是多继承的话,又是如何扩充的?
- ① 每个基类都有自己的虚表
- ② 子类的虚函数被放到了第一个基类的虚函数表中。
- ③ 内存布局中,其父类布局依次按声明顺序排列。
- ④ 每个基类的虚表中的print()函数都被overwrite成了子类的print ()。这样做就是为了解决不同的基类类型的指针指向同一个子类实例,而能够调用到实际的函数。
class Base
{
public:
Base(int i) :baseI(i){};
virtual ~Base(){}
int getI(){ return baseI; }
static void countI(){};
virtual void print(void){ cout << "Base::print()"; }
private:
int baseI;
static int baseS;
};
class Base_2
{
public:
Base_2(int i) :base2I(i){};
virtual ~Base_2(){}
int getI(){ return base2I; }
static void countI(){};
virtual void print(void){ cout << "Base_2::print()"; }
private:
int base2I;
static int base2S;
};
class Drive_multyBase :public Base, public Base_2
{
public:
Drive_multyBase(int d) :Base(1000), Base_2(2000) ,Drive_multyBaseI(d){};
virtual void print(void){ cout << "Drive_multyBase::print" ; }
virtual void Drive_print(){ cout << "Drive_multyBase::Drive_print" ; }
private:
int Drive_multyBaseI;
};
4.2 菱形继承
D类对象内存布局中:
-
图中青色表示b1类子对象实例,黄色表示b2类子对象实例,灰色表示D类子对象实例。
-
由于D类间接继承了B类两次,导致D类对象中含有两个B类的数据成员
ib
,一个属于来源B1类,一个来源B2类。这样不仅增大了空间,更重要的是引起了程序歧义:D d; d.ib = 1; //二义错误,调用的是B1::ib还是B2::ib? //正确调用方式 d.B1::ib = 1; d.B2::ib = 1;
5、C++模型中加入虚继承
什么是虚继承?
//类的内容与前面相同
class B{...}
class B1 : virtual public B
虚继承是为了解决 重复继承中多个间接父类 的问题的,所以不能使用上面简单的扩充并为每个虚基类提供一个虚函数指针(这样会导致重复继承的基类会有多个虚函数表)形式。
虚继承的派生类的内存结构,和普通继承完全不同:
- 虚继承的子类,有单独的虚函数表,另外也单独保存一份父类的虚函数表,两部分之间用一个四个字节的0x00000000来作为分界。
- 派生类的内存中,首先是自己的虚函数表,然后是派生类的数据成员,然后是0x0,之后就是基类的虚函数表,之后是基类的数据成员。
- 如果派生类没有自己的虚函数,那么派生类就不会有虚函数表,但是派生类数据和基类数据之间,还是需要0x0来间隔。
总结:
- 因此,在虚继承中,派生类和基类的数据,是完全间隔的,先存放派生类自己的虚函数表和数据,中间以 0x0 分界,最后保存基类的虚函数和数据。
- 如果派生类重载了父类的虚函数,那么则将派生类内存中基类虚函数表的相应函数替换。
- 在C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr)
- 因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
5.1 简单虚继承
类图如下:
子类对象模型如下:
5.2 菱形虚继承
类图如下:
菱形虚拟继承下,最派生类D类的对象模型又有不同的构成了。在D类对象的内存构成上,有以下几点:
- 在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)
- D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔。
- 编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同。
- 超类B的内容放到了D类对象内存布局的最后。
菱形虚拟继承下的C++对象模型为: