深入C++对象模型(一)

关于对象

C++在加入封装后(只含有数据成员和普通成员函数)的布局成本增加了多少?

  • 答案是并没有增加布局成本。就像C struct一样,《成员函数》虽然含在class的声明之内,却不出现在对象中。每一个非类联函数只会诞生一个函数实体。至于每一个“拥有零个或一个定义的” 类联函数则会在其每一个使用者(模块)身上产生一个函数实体

C++在布局以及存取时间上主要的额外负担是由virtual引起的,包括:

  • 虚函数机制,用以支持一个有效率的“执行期绑定”(runtime binding)
  • 虚基类,用以实现“多次出现在继承体系中的基类,有一个单一而被共享的实体”

C++ 对象模式(The C++ Object Model)

在C++中,有两种class data members:static 和 nonstatic,以及三种class member functions:static、nonstatic和virtual。已知下面这个class Point声明:

class Point{
public:
    Point(float xval);
    virtual ~Point();
    float x() const;
    static int PointCount();

protected:
    virtual ostream& print(ostream &os) const;
    float _x;
    static int _point_count;
};

C++对象模型中,nonstatic data members被配置于每一个class object之内,static data members则被存放在所有的class object之外。static和nonstatic function members也被放在所有的class object之外。virtual function则以两个步骤支持之:
1. 每个class产生出一堆指向virtual functions的指针,放在表格之中。这个表格被称为virtual table(vtbl)
2. 每一个class object被安插一个指针,指向相关的virtual table。通常这个指针被称为vptr。vptr的设定和重置都由每一个class的constructor、destructor和copy assignment运算符自动完成。每一个class所关联的type_info object(用以支持runtime type identification, RTTI)也经由virtual table被指出来,通常放在表格的第一个slot处

故上面的声明所对应的对象模型如下:

上图说明了C++对象模型如何应用于Point Class身上,这个模型的主要优点在于它的空间和存取时间的效率。主要缺点是:如果应用程序代码未曾改变,但所用到的class objects的nonstatic data members有所修改(有可能是增加、移除或更改),那么应用程序代码同样得重新编译。

继承关系可以指定为虚拟(virtual,也就是共享的意思):在虚拟继承的情况下,base class不管在继承链中被派生(derived)多少次,永远只会存在一个实例(称为subobject)。虚基类子对象是由最派生类(最后派生出来的类)的构造函数通过调用虚基类的构造函数进行初始化

class istream : virtual public ios{ ... };
class ostream : virtual public ios{ ... };



此外再来补充一下虚基类的有关几个概念:

一、虚基类的作用:

    当一个类的部分或者全部基类来自另一个共同的基类时,这些直接基类中从上一级共同基类继承来的  就拥有相同的名称。在派生类的对象中,这些同名数据成员在内存中同时拥有多个拷贝,同一个函数名会有多个映射。我们可以使用作用域分蝙蝠来唯一标识并分别访问他们,也可以将共同基类设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只用一个拷贝,同一个函数名也只有一个映射。

二、虚基类的声明  语法形式:

    class 派生类名:virtual  继承方式  基类名

三、使用虚基类时应该注意:

  1>一个类可以在一个类族中用作虚基类,也可以用作非虚基类。

  2>在派生类的对象中,同名的虚基类只产生一个虚基类子对象,而某个非虚基类产生各自的对象。

  3>虚基类子对象是由最派生类(最后派生出来的类)的构造函数通过调用虚基类的构造函数进行初始化。

  4>最派生类是指在继承类结构中建立对象时所指定的类。

  5>在派生类的构造函数的成员初始化列表中,必须列出对虚基类构造函数的调用,如果没有列出,则表示使用该虚基类的缺省构造函数。

  6>在虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中,都要列出对虚基类构造函数的调用。但只有用于建立对象的最派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次。

  7>在一个成员初始化列表中,同时出现对虚基类和非虚基类构造函数的调用时,基类的构造函数先于非虚基类的构造函数执行。

  8>虚基类并不是在声明基类时声明的,而是在声明派生类是,指定继承方式时声明的。因为一个基类可以在生成一个派生类作为虚基类,而在生成另一个派生类时不作为虚基类。

  温馨提示:使用多重继承时要十分小心,经常会出现二义性。许多专业人员认为:不要提倡在程序中使用多重继承,只有在比较简单和不易出现二义性的情况或是在必要时才使用多重继承,能用单一继承解决的问题就不要使用多重继承,也是由于这个原因,有些面向对象的程序设计语言,并不支持多重继承。

   现在对虚基类构造函数了解了没??如果还不了解那么咱们就继续深入研究.....

首先,要知道虚拟继承与普通继承的区别:

假设derived继承自base类,那么derived与base是一种“is a”的关系,即derived类是base类,而反之错误;

假设derived虚继承自base类,那么derived与base是一种“has a”的关系,即derived类有一个指向base类的vptr。

因此虚继承可以认为不是一种继承关系,而可以认为是一种组合的关系。因为虚继承有着“继承”两个关键字,那么大部分人都认为虚继承与普通继承的用法没有什么太大的不同,由此用在继承体系中,这种将虚继承认为是普通继承的危害更加大!先用一个例子来说明问题:

复制代码
#include <iostream>
using namespace std;

class base
{
public:
    base()
    {
        cout <<"base::base()!"<<endl;
    }
    void printBase()
    {
        cout<<"base::printBase()!"<<endl;
    }
};

class derived:public base
{
public:
    derived()
    {
        cout<<"derived::derived()!"<<endl;
    }
    void printDerived()
    {
        cout<<"derived::printDerived()!"<<endl;
    }
};

int main(int argc, char* argv[])
{

    derived oo;
    base oo1(static_cast<base>(oo));

    oo1.printBase();

    cout <<"---------------------"<<endl;
    derived oo2= static_cast<derived&>(oo1);
    oo2.printDerived();
}
复制代码

运行结果:

对前面的例子稍加修改......................

复制代码
#include <iostream>
using namespace std;

class base1
{
public:
    base1()
    {
        cout<<"base::base()!"<<endl;
    }
    void printBase()
    {
        cout<<"base::printBase()!"<<endl;
    }
};
class derived1:virtual public base1
{
public:
    derived1()
    {
        cout<<"derived::derived()!"<<endl;
    }
    void printDerived()
    {
        cout <<"derived::printDerived()!"<<endl ;
    }

};

int main(int argc, char* argv[])
{
    derived1 oo;
    base1 oo1(static_cast<base1>(oo));
    oo1.printBase();
    
    derived1 oo2 = static_cast<derived1&>(oo1);
    oo2.printDerived();
    return 0;
}
复制代码

 

会发现编译错误:error C2635: cannot convert a 'base1*' to a 'derived1*'; conversion from a virtual base class is implied(代码中红色部分出错)

可以看到不能将基类通过static_cast转换为继承类。我们知道c++提供的强制转换函数static_cast对于继承体系中的类对象的转换一般是可行的。那么这里为什么不可以呢??

virtual base class的原始模型是在class object中为每一个有关联的virtual base class加上一个指针vptr,该指针指向virtual基类表。有的编译器是在继承类已存在的virtual table直接扩充导入一个virtual base class table。不管怎么样由于虚继承已完全破坏了继承体系,不能按照平常的继承体系来进行类型转换。

 

  • 我们清楚了虚基类构造函数是怎么回事,那么接下来讲解一下虚基类构造函数调用顺序!!

我们下来了解虚拟继承中遇到最广泛的菱形结构:

复制代码
#include <iostream>
using namespace std;

class stream
{
public:
    stream()
    {
        cout <<"stream::stream()!"<<endl;
    }
};


class iistream:virtual stream
{
public:
    iistream()
    {
        cout <<"istream::istream()!"<<endl;
    }
};

class oostream:virtual stream
{
public:
    oostream()
    {
        cout <<"ostream::ostream()!"<<endl;
    }
};

class iiostream:public iistream,oostream
{
public:
    iiostream()
    {
        cout<<"iiostream::iiostream()!"<<endl;
    }
};

int main(int argc, char* argv[])
{
    iiostream oo;
    return 0;
}
复制代码

 

运行结果:

本来虚拟继承的目的就是当多重继承出现重复的基类时,其只保存一份基类,减少内存开销。

 

这样子的菱形结构,使公共基类只产生一个拷贝。

从基类stream派生新类时,使用virtual将类stream说明为虚基类,这时派生类istream、ostream包含一个指向虚基类的vptr,而不会产生实际的stream空间。所以最终iiostream也含有一个指向虚基类的vptr,调用stream中的成员方法时,通过vptr去调用,不会产生二义性!

现在我们换种方式使用虚继承:

复制代码
#include <iostream>
using namespace std;

class stream
{
public:
    stream()
    {
        cout <<"stream::stream()!"<<endl;
    }
};


class iistream:public stream
{
public:
    iistream()
    {
        cout <<"istream::istream()!"<<endl;
    }
};

class oostream:public stream
{
public:
    oostream()
    {
        cout <<"ostream::ostream()!"<<endl;
    }
};

class iiostream:virtual iistream,oostream
{
public:
    iiostream()
    {
        cout<<"iiostream::iiostream()!"<<endl;
    }
};

int main(int argc, char* argv[])
{
    iiostream oo;
    return 0;
}
复制代码

运行结果:

从结果上可以看到,其构造过程中重复出现基类的stream的构造过程,这样就完全没有达到虚拟继承的目的。其继承结构为:

从继承结构可以看出,如果iiostream对象调用基类stream重的成员方法,会导致方法的二义性。因为iiostream含有指向其虚继承基类istream,ostream的vptr。而istream,ostream包含了stream的空间,所以导致iiostream不知道导致时调用那个stream的方法。要解决该问题,即在调用成员方法时需要加上作用域!

 

后续


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值