当前位置:首页 > 芯闻号 > 充电吧
[导读]对象内存有以下类,这些类实例化的对象在内存中占的大小分别是多少?class X{};class Y : public virtual X{};class Z : public virtual X{};

对象内存
有以下类,这些类实例化的对象在内存中占的大小分别是多少?
class X
{
};
class Y : public virtual X
{
};
class Z : public virtual X
{
};
class A : public Y, public Z
{
};
乍一看这三个类都没有成员变量,对象所占的内存大小应该是0,而实际不是。
        sizeof X的结果是1
        sizeof Y的结果是8
        sizeof Z的结果是8
        sizeof Z的结果是12
class X不是空的,它有一个隐晦的1 byte,那是编译器安插进去的一个char,使得该class对应的object得以在内存中配置唯一的地址
class Y和class Z的大小都是8,这个和机器以及编译器有关,受到以下三个因素影响:
1. 语言本身造成的额外负担
    当语言支持virtual base class时,在derived class中,这个额外负担反应在某种形式的指针身上,它或者指向virtual base class subobject或者指向一个相关表格,这个表格里或者virtual base class subobject的地址或者其偏移量。
2. 编译器对于特殊情况所提供的优化处理
    virtual base class X subobject的1 byte大小也出现在class Y和Z身上。传统上它被放在derived class的固定(不变动)部分的尾端。某些编译器会对empty virtual base class提供特殊支持(如下面第3点)
    对于后续编译器优化,这1byte被舍去了,因为derived class object中已经不需要这1byte来申请唯一地址了,object已经至少有一个指针了。
3. Alignment的限制
     class Y和Z的大小截止当前为5 bytes,在大部分机器上,群聚的结构体大小会受到alignment的限制,使他们能够更加有效率地在内存中被存取
    alignment就是将数值调整到某数的整数倍,在32位计算机上,通常alignment为4bytes(32位),以使bus的“运输量”达到最高效率。

class的data member一般是可以表现这个class在程序执行时的某种状态。nonstatic data members放置的是“个别的class object”感兴趣的数据,static data member则放置的是“整个class”感兴趣的数据。不管该class被产生出多少个objects,static data members永远只存在一份实体(即使还没产生任何该class的object实体,其static data member也已经存在)。但一个template class的static data members的行为有所不同(后续会讨论)

Data Member的绑定
关于全局变量和内部变量的resolved问题
      现在编译器已优化,对于class内部和外部同名变量,内部function优先使用内部同名变量。但是对于typedef定义优先使用先出现的,即如果使用点出现时,只有外部typedef可见,那么使用外部typedef。因此为避免歧义,需要把内部的typedef放在所有数据使用之前,如下:
typedef int length;
class Point3D
{
private:
    typedef float length;
    length _val;
};

Data Member的布局
nonstatic data member在class object中的排列顺序和其被声明的顺序一致,中间插入static data members不会被放进对象布局中。
C++ Standard要求,在同一个access section(也就是private、public、protected等区段)中,members的排列只需符合“较晚出现的members在class object中有较高的地址”这一条件即可,也就是说各个members并不一定得连续排列,中间可能会被一些字节介入,如member的边界调整(alignment)可能就填补一些bytes。access sections的多寡并不会招来额外负担,例如在一个section中声明8个members或者在8个sections中总共声明8个members得到的object大小是一样的。

各类Data Member的存取
static data member
每一个static data member只有一个实体,存放在程序的data segment中,每次程序取用static member就会被内部转化成对该唯一的extern实体的直接参考操作。
//origin.chunkSize = 250;
Point3d::chunkSize = 250;
//pt->chunkSize = 250;
Point3D::chunkSize = 250;
不管这个static data member是通过继承还是什么途径得来的,存取都是一样的。取一个static data member的地址会得到一个指向其数据类型的指针,而不是一个指向其class member的指针,因为static member并不包含在一个class object中,在data segment中。两个class命名了相同的static变量,那么他们在data segment就会导致名称冲突,编译器解决的办法是对他们重新编码。

Nonstatic Data Member
nonstatic data member的存取是通过class object来操作的,通过明确的或者暗喻的(通过调用内部方法间接使用nonstatic data member)。对于nonstatic data member的存取是编译器把class object的起始地址加上data member的偏移量(offset),如:
    origin.y = 0.0

那么地址&origin.y等于&origin+(&Point3d::_y-1),指向data member的指针,其offset值总被加上1,这是用于区分“一个指向data member的指针,用以指出class的第一个member”和“一个指向data member的指针,没有指向任何member”两种情况,所以这里需要-1

Add Polymorphism
class Point2d
{
    ...
    virtual function...
};
class Point3d : public Point2d
{
   ...
};
加入多态后,程序有了更多弹性,为支持这样的弹性,势必给Point2d带来空间和存取时间的额外负担:
1. 导入一个和Point2d有关的virtual table,用来存放它所声明的每一个virtual function的地址。这个table的元素数目一般而言是被声明的virtual functions的数目,再加上一个或两个slots(用以支持runtime type identification)
2. 在每一个class object中导入一个vptr,提供执行期的链接,使每一个object能够找到相应的virtual table
3. 加强constructor,使它讷讷够为vptr设定初值,让它指向class所对应的virtual table。这可能意味着在derived class和每一个base class的constructor中,重新设定vptr的值,其情况视编译器的优化的积极性而定。
4. 加强destructor,使它能抹消“指向class之相关virtual table”的vptr。要知道,vptr很可能已经在derived class destructor中被设定为derived class的virtual table地址。记住,destructor的调用次序是反向的:从derived class到base class,一个积极的优化编译器可以压抑那些大量的指定操作。
每一个Point3d class object内含一个额外的vptr member(继承自Point2d);多了一个Point3d virtual table,此外,每一个virtual member function的调用也比以前复杂了。

多重继承(Multiple Inheritance)
单一继承提供一种“自然多态(natural polymorphism)”形式,base class和derived class的objects都是从相同地址开始的,其差异只在于derived object 比较大,用以容纳它自己的nonstatic data members。
对于多重继承,复杂度在于derived class和其上一个base class乃至上上一个 base class之间的“非自然”关系,如下:
    class Point2d
    {
    public:
      //...(拥有virtual接口,所以Point2d对象中会有vptr)
    protected:
      float _x, _y;
    };
    class Point3d : public Point2d
    {
    public:
      //...
    protected:
      float _z;
    };
    class Vertex
    {
    public:
      //...(拥有virtual接口,所以Vertex对象之中会有vptr)
    protected:
      Vertex* next;
    };
    class Vertex3d : public Point3d, public Vertex
    {
    public:
      //...
    protected:
      float mumble;
    };
对于将Vertex3d对象地址指定给最左端(即第一个)base class类型的指针,情况将和单一继承时相同,因为二者都指向相同的起始地址。至于第二个或者后续的base class的地址指定操作,则需要将地址修改,加上(或减去,如downcast的话)介于中间的base class subobject(s)大小
      Vertext3d v3d;
      Vertex *pv;
      Point2d *p2d;
      Point3d *p3d;
那么下面这个指定操作:
     pv = &v3d;
需要这样的内部转化,虚拟码:
    pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));
下面的指定操作:
    p2d = &v3d;
    p3d = &v3d;
都只是需要简单拷贝地址就行。
如果有下面两个指针,且进行指定操作:
    Vertex3d *pv3d;
    Vertex *pv;
    pv = pv3d;
不能只是简单地被转化为:
    pv = (Vertex*)((char*)pv3d + sizeof(Point3d));
这里面还要判断pv3d是否为0,正确的内部转化应该再添加一个条件测试:
    pv = pv3d ? (Vertex*)((char*)pv3d + sizeof(Point3d)):0;
至于reference则不需要针对可能的0地址做测试,因为reference不可能参考到no object


虚拟继承(Virtual Inheritance)
        在虚拟继承中,如果继承有多个相同的subobject,则在derived class中只需保存一份。一般的实现方法:Class如果内含一个或多个virtual base class subobject,像istream那样,那么将被分割成两部分:一个不变局部和一个共享局部。不变局部中的数据,不管后续如何衍化,总拥有固定的offset(从object的开头算起),所以这部分数据可以被直接存取。至于共享局部,所表现的就是virtual base class subobject。这一部分的数据,其位置会因为每次排上操作而有所变化,所以他们只能被间接存取。各家编译器实现技术之间的差异在于间接存取的方法不同。有如下虚拟继承层次结构:
    class Point2d
    {
    public:
       ...
    protected:
      float _x, _y;
    };
    class Vertex : public virtual Point2d
    {
    public:
      ...
    protected:
      Vertex *next;
    };
    class Point3d : public virtual Point2d
    {
    public:
      ...
    protected:
      float _z;
    };
    class Vertex3d : public Vertex, public Point3d
    {
    public:
      ...
    protected:
      float mumble;
    };
一般的布局策略是先安排好derived class的不变部分,然后在简历其共享部分。编译器在每个derived class object中安插一些指针,每个指针指向一个virtual base class,存取寄存得来的virtual base class members可以使用相关指针间接完成(理想状态是不在virtual base class中设置member),如下:
    void Point3d::operator += (const Point3d &rhs)
    {
      _x += rhs._x;
      _y += rhs._y;
      _x += rhs._z;
    };
在cfront编译器策略里,这个运算符会被内部转换为:
    //虚拟码
    __vbcPoint2d->_x += rhs.__vbcPoint2d->_x;//vbc意为virtual base class
    __vbcPoint2d->_y += rhs.__vbcPoint2d->_y;
    _z += rhs._z;
这里表现出来2个主要缺点:
1. 每一个对象必须对其每一个virtual base class背负一个额外指针,而我们希望class object有固定的内存容量,而不是随着virtual base class的数目有所改变
2. 由于虚拟继承串联的加长,导致间接存取的层次增加,从而导致存取时间变长,而我们希望存取时间是固定的。
针对这两个问题各个编译器有各自的做法:
MetaWare和其他编译器仍使用cfront的原始模型来解决第2个问题,他们经由拷贝操作来取得所有的nested virtual base class指针,放到derived class object中,用空间上的代价解决“固定存取时间”这个问题,如下图:

另一种解决方案是将virtual base class offset和virtual function entry混在一起,具体做法:在virtual function table中放置virtual base class的offset(而不是地址),如下图:

以上方法都是实现模型,而不是一种标准,每一种模型都是用来解决“存取shared subobject内的数据(其位置会因每次派生操作而又变化)”所引发的问题。最理想的情况是virtual base class 中不存放members,只是提供接口。

指向Data Member的指针
指向data member的指针是一个有用的语言特性,特别是要详细调查class members的底层布局的话。这样的调查可用以决定vptr是放在class的起始处或是尾端。另一个用途是决定class的access section次数。
有以下注意点:如何区分一个“没有指向任何data member”的指针和一个指向“第一个datamember”的指针,如下:
    float Point3d::*p1 = 0;
    float Point3d::*p2 = &Point3d::x;//“指向Point3d data member”的指针类型
    if(p1 == p2)
    {
      ...
    }
为了区分p1和p2,每一个真正的member offset值都被加上1,因此不论编译器或使用者都必须记住,在真正使用该值以指出一个member之前,请先减掉1。





    



本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

其实在 c++语言里面const修饰的才算是一个真正的常量,在 c 语言中 const 可以说是个“冒牌货”。为什么会这样?其实是 c++ 编译器对 const 进行了加强,当 c++ 编译器遇到常量声明时,不会像 c...

关键字: c++ C语言 const

返回函数的引用去初始化一个新的引用这个和前面一样,都是不会产生副本,但是现在是用返回值去初始化一个引用声明c,也就是说这时候变成了变量temp的别名,在c的生命周期内temp是一直有效的,这样做完全可以。

关键字: c++ 返回值 引用声明

C++是一种面向对象的高级程序设计语言,是C语言的超集。

关键字: c++ C语言

分析:这是Adobe 公司2007 年校园招聘的最新笔试题。这道题除了考察应聘者的C++ 基本功底外,还能考察反应能力,是一道很好的题目。 在Java 中定义了关键字final ,被final 修饰的

关键字: c++ class

泛型算法中的定制操作很多算法都会比较输入序列中的元素,通过定制比较动作,可以控制算法按照编程者的意图工作。本文以string排序为例进行说明,首先是缺省的排序动作: vector v{"This","

关键字: c++

为什么是lambda?讲了这么多天的lambda表达式,有一个很基本的问题没有回答:为什么叫lambda表达式呢?首先这个lambda就是罗马字母λ,lambda表达式即λ表达式。数学上有一个概念叫λ

关键字: c++

        假设我们有个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的Widget 上进行某些带有优先权的处理:int priority () ; void processWi

关键字: c++ effective

判断链表中是否有环最经典的方法就是快慢指针,同时也是面试官大多想要得到的答案。       快指针pf(f就是fast的缩写)每次移动2个节点,慢指针ps(s为slow的缩写)每次移动1个节点,如果快

关键字: c++ 链表 快慢指针

转载请注明出处:http://blog.csdn.net/callon_h/article/details/52073268 引子 上一篇博客从内核驱动到android app讲述了android通过

关键字: c++ java

在网上看到一段读写bmp格式图像的代码,本文对这段代码分成两个函数封装起来方便使用,一个函数是读取bmp格式的图像,一个是向指定文件写入bmp格式的图像。前提我们不需要知道这段代码是如何读取bmp格式

关键字: bmp c++
关闭
关闭