1.C++对象模型概述
有两个概念可以解释C++对象模型
1.语言中直接支持面向对象程序设计的部分
包括了构造函数、析构函数、多态、虚函数等等.
2.对于各种支持的底层实现机制
对象模型研究的是对象在存储上的空间与时间上的优化,并对C++面向对象技术加以支持,如以虚指针、虚表机制支持多态机制.
2.理解虚函数表
2.1多态与虚表
C++中虚函数的作用主要是为了实现多态机制,多台,简单的来说,是指在继承层次中,父类的指针可以具有多种形态——当它指向某个子类对象时,通过它能够调用子类的函数,而非父类的函数
class Base { virtual void print(void);}
class Drive1:public Base{virtual void print(void);}
class Drive2:public Base{vittual void print(void);}
Base * ptr1 = new Base;
Base *ptr2 = new Driver1;
Base *ptr3 = new Driver2;
ptr1->print();//调用Base::print()
ptr2->print();//调用Drive1::print()
ptr3->print();//调用Driver2::print()
这是一种运行期多态,即父类指针唯有在程序运行时才能知道所指的真正类型是什么,这种运行期决议,是通过虚函数表来实现的.
3.2 使用指针访问虚表
#include<iostream>
using namespace std;
class Base
{
public:
Base(int i):baseI(i){};
virtual void print(void){
cout <<"调用了虚函数Base::print()"<<endl;
}
virtual void setI(){
cout << "调用了虚函数Base::setI();"<<endl;
}
virtual ~Base(){}
private:
int baseI;
};
int main(int argc,char *argv[])
{
Base b(1000);
int *vptrAdree = (int *)(&b);
cout << "虚函数表(vptr)的地址是: "<<vptrAdree <<endl;
typedef void(*Fun)(void);
Fun vfunc = (Fun)*((int *)*(int *)(&b));
cout << "第一个虚函数的地址是:" <<(int *)*(int *)(&b) <<endl;
cout << "通过地址,调用虚函数Base::print():";
vfunc();
return 0;
}
运行结果:
yang@yang:~/C++/对象模型$ ./a.out
输出一:虚函数表(vptr)的地址是: 0x7ffc7fa95d20
输出二:第一个虚函数的地址是:0x400dd0
输出三:通过地址,调用虚函数Base::print():调用了虚函数Base::print()
输出一详解:我们强行把类对象的地址转换为int* 类型,取得了虚函数指针的地址.虚函数指针指向虚函数表,虚函数表中存储的是一系列虚函数的地址,虚函数地址出现的顺序与类中虚函数声明的顺序一致,对虚函数指针地址值解引用,可以得到虚函数表的地址,也即是虚函数表的第一个虚函数的地址:
输出二详解:
- 我们把虚表指针的值取出来:(int )(&b);它是一个地址,虚函数表的地址.
- 把虚函数表的地址强制转换成int * :(int )(int *)(&b)
- 再把它转换成我们Func指针类型:(Fun )(int ) * (int *)(&b);
3.对象模型概述
3.1对象模型概述
在C++中,有两种数据成员,static和nonstatic,以及三种类成员函数:static、nonstatic 和virtual;
现在我们有一个类Base,它包含了上面这5种类型的数据或函数
#include<iostream>
using namespace std;
class Base
{
public:
Base(int i):BaseI(i){};
int getI(){
return BaseI;
}
static void countI(){};
virtual void print(void){
cout << "Base::print()";
}
virtual ~Base(){}
private:
int baseI;
static int baseS;
};
3.2 非继承下的C++ 对象模型
概述:在此模型下,nonstatic数据成员被置于每一个类对象中,而static数据成员被置于类对象之外。static与nonstatic函数也都放在类对象之外.而对于虚函数,则通过虚函数表+虚函数指针来支持,具体如下:
- 每一个类生成一个表格,称为虚表,虚表中存放着一堆指针,这些指针指向该类每一个虚函数,虚表中的函数地址按声明时的顺序排列,不过当子类中有多个重载函数时例外,后面会讨论.
- 每个类对象都拥有一个虚表指针(vptr),由编译器为其生成,虚表指针的设定与重置皆由类的复制控制(也即是构造函数、析构函数、赋值操作符)来完成,vptr的位置由编译器来决定,许多编译器把vptr放在一个类对象的最前端。关于数据成员布局的内容,在后面会详细分析。另外,虚函数表的前面设置了一个指向type_info的指针,用以支持RTTI(Run Time Type Identification,运行时类型识别)。RTTI是为多态而生成的信息,包括对象继承关系,对象本身的描述等,只有具有虚函数的对象在会生成。
在此模型下,Base对象的对象模型如下:
4.继承下的C++对象模型
4.1单继承
#include<iostream>
using namespace std;
class Derive:public Base
{
public:
Derive(int d):DeriveI(d){};
//重载父类的虚函数
virtual void print(void) {
cout << "Drive::Dirve_print()";
}
//Derive声明的新的虚函数
virtual void Drive_print(){
cout << "Drive::Drive_print()";
}
private:
int DeriveI;
};
在C++对象模型中,对于一般继承(这个是相对于虚拟继承而言),若子类重写了父类的虚函数,则子类虚函数将覆盖虚表中对应父类虚函数(注意子类和父类拥有各自的一个虚函数表);若子类并无overwrite父类虚函数,而是声明了自己的新的虚函数,则该虚函数地址将扩充到虚函数表最后,而对于虚继承,若子类overwrite父类虚函数m同样地将覆盖父类子物体中虚函数表对应位置,若子类声明了自己的新的虚函数,则编译器为其子类增加一个新的虚表指针vptr.
4.2多继承
4.2.1一般的多重继承
单继承中(一般继承),子类会扩展父类的虚函数表,在多继承中,子类含有多个父类的子对象,该往哪个父类的虚函数表扩展呢?当子类overwrite了父类的函数,需要覆盖多个父类的虚函数表吗?
- 子类的虚函数被放在声明的第一个基类的虚函数表中.
- overwrite时,所有基类的print()函数都被子类的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;
};
此时Drive_multyBase的对象模型是这样的:
4.2.2菱形继承(子类间接继承多次同一个基类)
菱形继承也称为重复继承,它指的是基类被某个派生类简单重复继承了多次,这样,派生类对象中拥有多份基类实例:看代码:
class B
{
public:
int ib;
public:
B(int i=1) :ib(i){}
virtual void f() { cout << "B::f()" << endl; }
virtual void Bf() { cout << "B::Bf()" << endl; }
};
class B1 : public B
{
public:
int ib1;
public:
B1(int i = 100 ) :ib1(i) {}
virtual void f() { cout << "B1::f()" << endl; }
virtual void f1() { cout << "B1::f1()" << endl; }
virtual void Bf1() { cout << "B1::Bf1()" << endl; }
};
class B2 : public B
{
public:
int ib2;
public:
B2(int i = 1000) :ib2(i) {}
virtual void f() { cout << "B2::f()" << endl; }
virtual void f2() { cout << "B2::f2()" << endl; }
virtual void Bf2() { cout << "B2::Bf2()" << endl; }
};
class D : public B1, public B2
{
public:
int id;
public:
D(int i= 10000) :id(i){}
virtual void f() { cout << "D::f()" << endl; }
virtual void f1() { cout << "D::f1()" << endl; }
virtual void f2() { cout << "D::f2()" << endl; }
virtual void Df() { cout << "D::Df()" << endl; }
};
我们根据单继承,我们可以分析出B1,B2类继承B类时的内存布局,又根据一般多继承,我们可以分析D类的内存布局:
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; //正确
尽管我们可以通过明确指明调用路径以消除二义性,但二义性的潜在性还没有消除,我们可以通过虚继承来使D类只拥有一个ib实体。
5.虚继承
虚继承解决了菱形继承中派生类拥有多个间接父类实例的情况,虚继承中派生类的内存布局与普通继承有很多不同,主要体现在:
- 虚继承的子类,如果本身定义新的虚函数,则编译器会为其生成一个虚函数指针(vptr)以及一张虚函数表,该vptr位于对象内存最前面.
- 而非虚继承,直接扩展父类的虚函数表.
- 虚继承的子类单独保留了父类的vptr与虚函数表,这部分内容与子类内容以一个四字节的0来分界.
- 虚继承的子类对象中,含有四字节的虚表指针偏移值.
5.1 虚基类指针
在C++模型中,虚继承而来的子类会生成一个隐藏的虚基类指针, 虚基类表指针总是在虚函数表指针之后
因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表,与虚函数一样,虚基类表也由多个条目组成,条目中存放的是偏移值,第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr),我们通过一张图来更好的理解.
虚基类表的第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值,这点我们在下面会验证。
5.2简单的虚继承
//类的内容与前面相同
class B{....}
class B1:virtual public public B
根据我们前面对虚继承的派生类的内存布局的分析,B1类的对象模型应该是这样的
5.3虚拟菱形继承
class B{...}
class B1: virtual public B{...}
class B2: virtual public B{...}
class D : public B1,public B2{...}
菱形虚拟继承下,派生类D类的对象模型又有不同的构成的,在D类对象的内存构成上,有以下几点:
- 在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)
- D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔
- 编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同.
- 超类B的内容放到了D类对象内存布局的最后。
菱形虚拟继承下的C++对象模型为:
6.下面这个空类构成的继承层次中,每个类的大小是多少?
class B{};
class B1 :public virtual B{};
class B2 :public virtual B{};
class D : public B1, public B2{};
int main()
{
B b;
B1 b1;
B2 b2;
D d;
cout << "sizeof(b)=" << sizeof(b)<<endl;
cout << "sizeof(b1)=" << sizeof(b1) << endl;
cout << "sizeof(b2)=" << sizeof(b2) << endl;
cout << "sizeof(d)=" << sizeof(d) << endl;
getchar();
}
结果:
yang@yang:~/C++/对象模型$ ./a.out
sizeof(b)=1
sizeof(b1)=8
sizeof(b2)=8
sizeof(d)=16
解析:
* 编译器为空类安插1字节的char,以使该类对象在内存配置一个地址。
* b1虚继承b,编译器为其安插8字节的虚基类表指针,此时b1已不为空,编译器不再为其安插1字节的char.
* b2 同理.
* d含有来自b1和b2两个父类的虚基类表指针,大小为16字节.