C++对象模型

Posted 杨龙飞的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++对象模型相关的知识,希望对你有一定的参考价值。

 1.C++对象模型概述

有两个概念可以解释C++对象模型
1.语言中直接支持面向对象程序设计的部分
包括了构造函数、析构函数、多态、虚函数等等.
2.对于各种支持的底层实现机制
对象模型研究的是对象在存储上的空间与时间上的优化,并对C++面向对象技术加以支持,如以虚指针、虚表机制支持多态机制.

2.理解虚函数表

2.1多态与虚表

C++中虚函数的作用主要是为了实现多态机制,多台,简单的来说,是指在继承层次中,父类的指针可以具有多种形态——当它指向某个子类对象时,通过它能够调用子类的函数,而非父类的函数

class Base  virtual void print(void);
class Drive1:public Basevirtual void print(void);
class Drive2:public Basevittual 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字节.

以上是关于C++对象模型的主要内容,如果未能解决你的问题,请参考以下文章

解构 C++ 符号的歧义

关于 C++ 标准的歧义

LOKI C++:“ScatterHierarchyTag”如何解决继承歧义?

重载强制转换运算符时的 C++ 歧义

算法复杂性分界函数—多项式

面向对象建模