有两个概念可以解释C++对象模型 1.语言中直接支持面向对象程序设计的部分 包括了构造函数、析构函数、多态、虚函数等等. 2.对于各种支持的底层实现机制 对象模型研究的是对象在存储上的空间与时间上的优化,并对C++面向对象技术加以支持,如以虚指针、虚表机制支持多态机制.
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()这是一种运行期多态,即父类指针唯有在程序运行时才能知道所指的真正类型是什么,这种运行期决议,是通过虚函数表来实现的.
输出一详解:我们强行把类对象的地址转换为int* 类型,取得了虚函数指针的地址.虚函数指针指向虚函数表,虚函数表中存储的是一系列虚函数的地址,虚函数地址出现的顺序与类中虚函数声明的顺序一致,对虚函数指针地址值解引用,可以得到虚函数表的地址,也即是虚函数表的第一个虚函数的地址: 输出二详解:
我们把虚表指针的值取出来:(int )(&b);它是一个地址,虚函数表的地址.把虚函数表的地址强制转换成int * :(int )(int *)(&b)再把它转换成我们Func指针类型:(Fun )(int ) * (int *)(&b);在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;};概述:在此模型下,nonstatic数据成员被置于每一个类对象中,而static数据成员被置于类对象之外。static与nonstatic函数也都放在类对象之外.而对于虚函数,则通过虚函数表+虚函数指针来支持,具体如下:
每一个类生成一个表格,称为虚表,虚表中存放着一堆指针,这些指针指向该类每一个虚函数,虚表中的函数地址按声明时的顺序排列,不过当子类中有多个重载函数时例外,后面会讨论. 每个类对象都拥有一个虚表指针(vptr),由编译器为其生成,虚表指针的设定与重置皆由类的复制控制(也即是构造函数、析构函数、赋值操作符)来完成,vptr的位置由编译器来决定,许多编译器把vptr放在一个类对象的最前端。关于数据成员布局的内容,在后面会详细分析。另外,虚函数表的前面设置了一个指向type_info的指针,用以支持RTTI(Run Time Type Identification,运行时类型识别)。RTTI是为多态而生成的信息,包括对象继承关系,对象本身的描述等,只有具有虚函数的对象在会生成。在此模型下,Base对象的对象模型如下:
在C++对象模型中,对于一般继承(这个是相对于虚拟继承而言),若子类重写了父类的虚函数,则子类虚函数将覆盖虚表中对应父类虚函数(注意子类和父类拥有各自的一个虚函数表);若子类并无overwrite父类虚函数,而是声明了自己的新的虚函数,则该虚函数地址将扩充到虚函数表最后,而对于虚继承,若子类overwrite父类虚函数m同样地将覆盖父类子物体中虚函数表对应位置,若子类声明了自己的新的虚函数,则编译器为其子类增加一个新的虚表指针vptr.
单继承中(一般继承),子类会扩展父类的虚函数表,在多继承中,子类含有多个父类的子对象,该往哪个父类的虚函数表扩展呢?当子类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的对象模型是这样的:
菱形继承也称为重复继承,它指的是基类被某个派生类简单重复继承了多次,这样,派生类对象中拥有多份基类实例:看代码:
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实体。
虚继承解决了菱形继承中派生类拥有多个间接父类实例的情况,虚继承中派生类的内存布局与普通继承有很多不同,主要体现在:
虚继承的子类,如果本身定义新的虚函数,则编译器会为其生成一个虚函数指针(vptr)以及一张虚函数表,该vptr位于对象内存最前面. 而非虚继承,直接扩展父类的虚函数表.虚继承的子类单独保留了父类的vptr与虚函数表,这部分内容与子类内容以一个四字节的0来分界.虚继承的子类对象中,含有四字节的虚表指针偏移值.在C++模型中,虚继承而来的子类会生成一个隐藏的虚基类指针, 虚基类表指针总是在虚函数表指针之后 因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。 一个类的虚基类指针指向的虚基类表,与虚函数一样,虚基类表也由多个条目组成,条目中存放的是偏移值,第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr),我们通过一张图来更好的理解. 虚基类表的第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值,这点我们在下面会验证。
根据我们前面对虚继承的派生类的内存布局的分析,B1类的对象模型应该是这样的
菱形虚拟继承下,派生类D类的对象模型又有不同的构成的,在D类对象的内存构成上,有以下几点:
在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同.超类B的内容放到了D类对象内存布局的最后。 菱形虚拟继承下的C++对象模型为:解析: * 编译器为空类安插1字节的char,以使该类对象在内存配置一个地址。 * b1虚继承b,编译器为其安插8字节的虚基类表指针,此时b1已不为空,编译器不再为其安插1字节的char. * b2 同理. * d含有来自b1和b2两个父类的虚基类表指针,大小为16字节.
新闻热点
疑难解答