《深度探索c++对象模型》(三)

2018-12-21  本文已影响0人  野渡渡

前述

本章的的主题是Data语意学,主要是探究编译器对class中的Data member的绑定、布局和存储等操作,最后探究Data member存取和多种继承方式之间的效率关系,以及指向Data member的指针的效率问题。


参考书籍及链接:《深度探索c++对象模型》


0、本章基础

1. 空类对象的大小是多少?

class X { };//空类

对于空类,它有一个隐藏的1byte大小,那个被编译器安插进去的一个char,这使得这一class的两个objects得以在内存中配置独一无二的地址。

2. class object的size会受到哪些因素的影响?

会影响class object的size的因素有如下三个,编译器:

3. 各种类型data member的存放。

nonstatic直接放在class object之中。static data member放置在程序的一个global data segment中,不会影响个别class object的大小。无论class产生多少个object,甚至是0个,其static data members永远也只存在一份实例。但是一个template classs的static data members的行为稍有不同。

一、Data member的绑定

1. member function取用的是global还是local data member?

当member funtion取用Data时,优先考虑member data,人们称这种情况为“member rewriting rule”,意思是对于member functions本身的分析,会直到整个class的声明都出现了才开始。在一个inline member function躯体之内的一个data member绑定操作,会在整个class声明之后才发生。

以前人们提倡两种程序设计风格,即将所有的data members放在class声明起始处,或者把所有的inline function都放在class声明之外。就是为解决绑定问题,但这种情况在c++ 2.0之后已经解决了。

2. member function的argument list的情况又是怎么样的呢?

与取用data member不同的是,argument list中的名称还是会在它们第一次 遭遇时被适当地决议(resolved)完成。

typedef int length;

class Point3d{
public:
    void mumble(length val) { _val=val;} //length被决议为global
    length mumble() {return val;}
    // ...
private:
    typedef float length;//这样的声明将使先前的参考操作不合法
    length _val;
    // ...
};

虽然编译器能处理,但还是提倡一种防御性程序风格:即总是把“nested(嵌套的) type声明”放在class的起始处。

二、Data member的布局

1. Data member是怎样被放置的?

关于data member的布局,记住以下三点:

三、Data member的存取

1. 经由一个class object和一个指针存取data member,有重大差异吗?

答案是显然的,这跟data member的类型和class的继承等都有关系,分如下两种情况讨论:

四、“继承”与Data Member

C++ standard未强制指定derived class members和base class members的排列顺序,理论上编译器可以自由安排之。在大部分编译器上头,base class members总是先出现,但属于virtual base class的除外。“继承”会对Data Member的布局有什么影响?接下来分四种情况进行讨论。

1. 第一种情况:只要继承不要多态。

这种情况不会存储时间上的额外负担,由于base class和derived class的objects都是从相同的地址开始,其差异只在于derived object 比较大,用以容纳自建的nonstatic data members,把一个derived class object指定给base class 的指针或引用,并不需要编译器去调停或修改地址,可以提供了最佳执行效率。

2. 第二种情况:加上多态。

加上virtual function接口后,弹性增加了,但也同时增加了空间和存取时间上的额外负担,如何取舍,视多态程序所带来的利益。可能带来的额外负担如下:

3. 第三种情况:多重继承。

对于单一继承,如果没有virtual function,那么编译器就不需要做其他工作;但如果base class没有virtual function而derived class有,并且vptr放在object首部,那么当把一个derived object转换为其base object时,就需要编译器对vptr进行调整。在既是多重继承又是虚拟继承的情况下,编译器的需要做的会更多。
对一个多重派生对象,将其地址指定给“最左端(也就是第一个)base class的指针”,情况将和单一继承时相同,因为二者都指向相同的起始地址。至于第二个或后继的base class的地址指定操作,则需要将地址修改为:加上(或减去)介于中间的base class subobjects大小。比较需要注意的是,如果在取drived class object的地址时进行偏移计算时,若其为指针,就需要判断其是否为0,若为0则基类object的地址也应为0。当然,这些都是编译器的工作,我们需要了解,但不需要自己去实现。

如果要存取第二个(或后继)base class中的一个data member会是怎样的情况?需要付出额外的成本吗? 不,members的位置在编译期就固定了,因此,存取members只是一个简单的offset运算,就像单一继承一样简单,不管是经由一个指针,一个reference或是一个object来存取。

4. 第四种情况:虚拟继承。

虚拟继承的出现是为了避免多个相同base class subobject的出现,将其只保留一份,从而减少空间浪费。
class如果含有一个或多个virtual base class subobjects将被分割为两部分:一个不变区域和一个共享区域。不变区域中的数据,总是能有固定的offset,这部分可以被直接存取,至于共享部分,所表现的就是virtual base class subobject ,这个部分数据,其位置因为每次派生操作而有变化,所以只能间接存取。

一般而言,virtual base class最有效的一种运用形式就是:一个抽象的virtual base class,没有任何data members。

五、对象成员的效率

程序员如果只关心起程序效率,应该实际测试,不能光凭推论、常识判断或假设。
参考书籍作者所做的测试表明,虚拟继承所造成确实会严重影响data member的存取效率。

五、指向Data members的指针(Pointer to Data Members)

1. 如果获取Data member的偏移值?偏移值应该为多少?

通过如(&Point3d::z)这样的操作可以获得data member的偏移值。实际测试表明所获得的offset比预想大1,这是为什么?实际上这样做的目的是为了区分一个“没有指向任何data member”的指针,和一个指向“第一个data member”的指针的情况。比如:

float Point3d::*p1 = 0;//“没有指向任何data member”的指针
float Point3d::*p2 = &Point3d::x;//指向“第一个data member”的指针

if(p1 == p2) //如何区分?
{
    cout << "p1 & p2 contain the same value --" ;
    cout << " they must address the same member!" << endl;
}

因此,不论编译器或使用者都必须记住,在真正使用该值以指出一个member之前,请先减掉1。

2.“指向Member的指针”对数据的存取有什么影响?

无继承时,指向member的指针对数据的存取操作,首先需要计算offset-1,其次具体的object需要用offset计算地址,会极大地降低效率,但目前的一些编译器提供了对应的优化,可以使其像直接通过对象取值一下快速。
有继承时,data member是直接放在class object中的,理论上不会影响代码的效率,但继承的使用会妨碍优化的效果。

上一篇 下一篇

猜你喜欢

热点阅读