【C++】C++对象模型


一、对象模型概述

在C++中,有两种数据成员:static 和nonstatic,以及三种类成员函数:static、nonstatic和virtual

一个包含了上面5种类型的成员函数或成员的类:

class Base
{
public:
    Base(int i):baseI(i){}
    int getI(){ return baseI; }
    static void countI(){}
    virtual ~Base(){}
    virtual void print(void)
    { 
    	cout << "Base::print()"; 
    }
private:
    int baseI;
    static int baseS;
};

虚函数相关的问题

  1. 虚函数的作用:虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。 虚函数是C++中用于实现多态的机制。

  2. 虚函数底层怎么实现:利用虚函数表(vtbl)和指向虚函数表的指针(vptr)

  3. VPTR什么时候被初始化:对象在创建时(运行构造函数时),由编译器对vptr指针进行初始化;只有当对象的构造完全结束后vptr的指向才最终决定下来;父类对象的vptr指向父类的虚函数表,子类对象的vptr指向子类的虚函数表。定义子类对象时,vptr先指向父类的虚函数表,在父类构造完成之后,子类的vptr才指向自己的虚函数表。(父类或者子类的构造函数中调用虚成员函数不会实现多态的原因)

  4. 虚函数表放在哪里:全局数据区;虚函数表特征类似于类中静态成员变量

  5. 构造函数不能是虚函数,为什么?

    1. 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象的实际类型。
    2. 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。
  6. 析构函数必须是虚函数,为什么?

    1. 如果析构函数不是虚函数,则用父类指针指向子类对象时, 父类指针无法访问到子类的析构函数,那么删除父类指针时,只会调用父类自己的析构函数,而不能调用派生类。
    2. 父类使用虚析构函数后,父类指针才可以(通过虚函数表)访问子类的虚析构函数,调用子类的析构函数析构子类本身。
  7. 析构函数可以抛出异常吗?不可,为什么:如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。

二、C++继承对象模型

2.1 非继承下C++对象模型


C++对象模型中:

  • non static数据成员被放置到对象内部
  • static数据成员, static and nonstatic 函数成员均被放到对象之外
  • 对于虚函数的支持则分两步完成:
  1. 每一个class产生一堆指向虚函数的指针(vptr),放在表格之中。这个表格称之为虚函数表(vtbl) 。
  2. 每一个对象被添加了一个虚指针(vptr),指向相关的虚函数表vtbl。vptr的设定(setting)和重置(resetting)都由每一个class的构造函数,析构函数和拷贝赋值运算符自动完成。
  3. 虚函数表地址的前面设置了一个指向type_info的指针,RTTI(Run Time Type Identification)运行时类型识别是有编译器在编译器生成的特殊类型信息,包括对象继承关系,对象本身的描述,RTTI是为多态而生成的信息,所以只有具有虚函数的对象在会生成。

这个模型的优点在于它的空间和存取时间的效率;缺点在于如果应用程序本身未改变,但当所使用的类的non static数据成员添加删除或修改时,需要重新编译。

2.2 单继承下C++对象模型

定义一个派生类:

class Derive : public Base
{
public:
    Derive(int d) :Base(1000),DeriveI(d){}
    //overwrite父类虚函数
    virtual void print(void){ cout << "Drive::Drive_print()" ; }
    // Derive声明的新的虚函数
    virtual void Drive_print(){ cout << "Drive::Drive_print()" ; }
    virtual ~Derive(){}
private:
    int DeriveI;
};

在C++对象模型中:
对于一般继承(这个一般是相对于虚拟继承而言):
若子类并无overwrite父类虚函数,而是声明了自己新的虚函数,则该虚函数地址将扩充到虚函数表最后


若子类重写(overwrite)了父类的虚函数,则子类虚函数将覆盖虚表中对应的父类虚函数(注意子类与父类拥有各自的一个虚函数表);

而对于虚继承,若子类overwrite父类虚函数,同样地将覆盖父类子物体中的虚函数表对应位置,而若子类声明了自己新的虚函数,则编译器将为子类增加一个新的虚表指针vptr,这与一般继承不同,在后面再讨论。
单继承下的对象内存模型图片总结

2.3 多继承下C++对象模型

2.3.1 一般多继承(非菱形继承)

在一般多继承中:

  1. 没有overwrite时:子类的虚函数被放在声明的第一个基类的虚函数表中。

  2. overwrite时,所有基类f函数都被子类的f函数覆盖。

  3. 内存布局中,父类按照其声明顺序排列。
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;
};

一般多继承下的对象内存模型图片总结

2.3.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类子对象的内存布局如下图:

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;           //正确

2.4 虚继承

虚继承解决了菱形继承中派生类拥有多个间接父类实例的情况。虚继承的派生类的内存布局与普通继承很多不同,主要体现在:

  1. 虚继承的子类,如果本身定义了新的虚函数,则编译器为其生成一个虚函数指针(vptr)以及一张虚函数表。该vptr位于对象内存最前面。(非虚继承:直接扩展父类虚函数表。)
  2. 虚继承的子类也单独保留了父类的vprt与虚函数表。这部分内容接与子类内容以一个四字节的0来分界。
  3. 虚继承的子类对象中,含有四字节的虚表指针偏移值。

2.4.1 虚基类表

C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr)。因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的是偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。我们通过一张图来更好地理解。

2.4.2 简单虚继承

//类的内容与前面相同
class B{...}
class B1 : virtual public B



虚继承的子类,有单独的虚函数表,另外也单独保存一份父类的虚函数表,两部分之间用一个四个字节的0x00000000来作为分界(不同编译器这里存在差异,在GCC下没有0x00000000分隔的,在VC++下有)。派生类的内存中,首先是自己的虚函数表,虚基类表,然后是派生类的数据成员,之后就是基类的虚函数表,之后是基类的数据成员。
如果派生类没有自己的虚函数,那么派生类就不会有虚函数表,但是派生类数据和基类数据之间,还是需要0x0来间隔

2.4.3 虚拟菱形继承

class B{...}
class B1: virtual public  B{...}
class B2: virtual public  B{...}
class D : public B1,public B2{...}


菱形虚拟继承下,最派生类D类的对象模型又有不同的构成了。在D类对象的内存构成上,有以下几点:

  1. 在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)
  2. D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔。
  3. 编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同。
  4. 超类B的内容放到了D类对象内存布局的最后。

三、C++多态

多态性的定义,可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。
多态可分为两种:

  • 静态多态:通过重载和模板技术实现,在编译的时候确定。
  • 动态多态:通过虚函数和继承关系来实现,执行动态绑定,在运行的时候确定。

3.1 动态多态

动态绑定,就是在运行时,虚函数会根据绑定对象的实际类型,选择调用函数的版本。
动态多态最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而调用不同的方法。
如果没有使用虚函数,即没有利用C++多态性,则利用基类指针调用相应函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是一定的,而固定的地址将始终调用同一个函数,这就无法“实现一个接口,多种实现”的目的了。

class Base
{
public:
    virtual void func()
    {
        cout << "Base::fun()" << endl;
    }
};
class Derived : public Base
{
public:
    virtual void func()
    {
      cout << "Derived::fun()" << endl;
    }

};
int main()
{
    Base* b=new Derived;          //使用基类指针指向派生类对象
    b->func();                    //动态绑定派生类成员函数func
    Base& rb=*(new Derived);      //也可以使用引用指向派生类对象
    rb.func();                
}

输出:
Derived::fun()
Derived::fun()

五、参考

图说C++对象模型:对象内存布局详解
C++ 虚函数表解析
c++ 虚继承与继承的差异


文章作者: YukinoKyoU
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 YukinoKyoU !
评论
  目录