The Semantics of Data

来源:互联网 发布:调星仪软件下载 编辑:程序博客网 时间:2024/06/05 11:38

        • The Semantics of Data
          • 0引例
          • 1 The Binding of a Data Member
          • 2Data Member Layout
          • 3Data Member 的存取
          • 4继承与Data Member
          • 5Object Member Efficiency
          • 6Pointer to Members

时隔很久,再次拾起<<深度探索C++对象模型>>一书.期间因为学习<<C++ Primer>>的缘故暂时放下了.现在学习遇到平台期,故又重新拾起。收获颇丰,故于诸君共勉。

The Semantics of Data

3.0.引例
class X{};class Y :public virtual X{};class Z :public virtual X{};class A :public Y, public Z{};

如上的X,Y,Z,A分别是多大呢?

首先是A的大小。我们通过sizeof进行测试,发现大小为1.原因是因为编译器安插一个char字节进去用于区分两个空对象。
X obj1,obj2;
安插一个字节使得他们获得独一无二的内存地址。那么下面我们猜测下X,Y的大小。
我VS 2013平台上大小是4.书上却存在一种大小是8的情况。
下面介绍下决定对象大小的几个因素:
1.支持虚机制带来的负担,比如虚函数,虚基类。
2.Alignment的限制,比如调整字节以达到最大的运输量。
3.编译器提供的特殊优化处理,比如对空虚基类的优化的处理。
对于Y的实例化,在编译器不优化的情况下,我们的Y因空的原因多一个char字节。又因为支持虚基类,产生了一个指向虚机类的指针(假定指针是4字节)。加上边界调整一共是8个字节。
但是微软的编译器会对空类进行优化。因为Y虚拟继承自X,Y中多了一个指向虚基类的指针,这导致Y不在是空的了,所以编译器优化了那个char字节,同时也边界调整也不在需要了,所以我们会看见4字节的结果。补充两张图片可以说明一切。
图片:
现在我们关注A的大小,那么应该是多大呢?我测试得到的结果是8,书上的例子得到的是12,下面就解释下原因。
1.考虑到虚基类的性质,X只存在一份实例。占据一个字节。
2.Y中有一个指向虚基类的指针占据4个字节,同理Z.
3.A自身大小为0.我存在点疑惑,为何不是1字节?
4.边界调整
所以一共是12个字节。对于微软的编译器,X的一个自己被拿掉了,所以边界调整也不需要了,一共是8个字节。

3.1. The Binding of a Data Member
extern  float x;class Point3d{public:    Point3d(float, float, float);    float X(){ return x; }    void X(float new_X) const{ x = new_X; }private:    float x, y, z;};

如果问我们Point3d::X() 函数中返回的x是哪个,我们肯定说是类中定义的x.这个因为我们知道C++ 中作用域查找规则,但是以前的编译器却是指向了全局的x.因此导致了两种防御性程序设计风格。

class Point3d{    float x, y, z;public:    Point3d(float, float, float);    float X(){ return x; }    void X(float new_X) const{ x = new_X; }};//在类的一开始就声明数据成员。

第二种就是把所有的inline function 声明到class之外。目的显而易见。
然而这种做法应该早就消失了,因为现在的C++ Standard 规定了内联函数即使是在类内定义的话,那么对其进行评估求值是要等到看见类声尾部的}括号才开始。所以防御性的设计风格可以随风而去了。
然而这个是对于类的数据成员而言,对于member function 的参数表而言却并非如此。

using length = int;class Point3d{public:    void mumble(length val){ _Val = val; }//length 的类型是什么?    length mumble(){ return _Val; }private:    using length = float;    length _Val;};

在我的编译器观察到length 的类型是int,并非如我们所料想的一样是int.所以上面提到的决议规则不适用。所以防御性的风格还是有必要的。

using length = int;class Point3d{public:    using length = float;    void mumble(length val){ _Val = val; }    length mumble(){ return _Val; }    //the type of length is float,not int ;private:    length _Val;};
3.2.Data Member Layout
class Point3d{public:    /**/private:    float x;    static std::list<Point3d*>* freeList;    float y;    static const int chunkSize = 250;    float z;};

考虑如类的的对象中会有什么。大部分人都会知晓,静态数据成员是属于所有对象的,不单属于某一个对象。C++ Standard要求,在同一个access section 中,数据成员的排列只要满足较晚出现的成员具有高地址即可。也就是说排列不一定是连续的,中间可能夹杂其他东西。同时编译器为了支持一些特性,会合成一些成员插入到对象中,但是并未规定插入到哪里。
一个判读地址的函数:

template<typename class_type,typename data_type1,typename data_type2>char& access_order(data_type1 class_type::*mem1, data_type2 class_type::* mem2){    assert(mem1 < mem2);    return mem1 < mem2 ? "mem1 occurs first \n" : "mem2 occurs occurs first \n";}/*Call the function */access_order(&Point3d::y, &Point3d::z);//now,the class type is Point3d,data_type is float .

在开始新的讨论之前,我很好奇&Point3d::y是什么鬼?
是y在内存中的地址嘛?而且我们通常存取非静态成员是通过对象,但是这个地方却是通过域操作符,煞是奇怪。先埋个伏笔,后面会介绍到。

3.3.Data Member 的存取

Point3d origin ;origin.x=0.0;
Point3d* pt=&origin; pt->x;
上述二者之间是否存在很大差异呢?
1.Static Data Member :我们一开始就提到过,静态数据成员不在类的对象之中。但是我们却可以通过对象对其进行存取,同时我们也看过通过域操作员直接进行存取。对于第一种方式,在编译器内部会被转化成对静态成员的直接参考操作。
origin.chunkSize; 等价于 Point3d::chunkSize;
通过指针存取也是进行同等的转换操作。从指令执行的角度来看,这是C++中唯一一种通过指针和通过一个对象对存取数据成员,结论完全相同的唯一一种情况。
如果chunkSize是从某复杂继承体系中继承而来的成员,那么存取操作依然如此直接的,因为它独立于对象之外。
如果通过函数存取静态数据成员会是什么情况呢?
foobar().chunkSize;C++ Standard 中规定,foobar必须被求值,但是其值是被丢弃的。最终仍是转换为:Point3d::chunkSize;
对一个静态数据成员取地址操作获得的是其在内存中的真是地址,得到的是一个指向其数据类型的指针,不是指向其class member的指针。原因还是静态成员不在对象之中。
&Point3d::chunkSize;的到的指针类型是const int*;并不是int Point3d::*;
如果两个类都声明同名的静态数据成员,那么如何区分?编译器会对静态数据成员进行编码(name-mangling)这样大家都是独一无二的,后面我们会经常遇到编码技术,所以编译器究竟做了什么真是很难知道。

name-mangling 有两个工作:1是使用一个算法推到出独一无二的名称,2是编译系统必须和使用者交谈,那些独一无二的名称可以被推到回原来的名称。
2.nonstatic data member :我们可以通过类的实例进行隐式或显式的存取(隐式指的是this指针).排除复杂继承的情况,其存取操作同结构体无大区别。
origin.y=0.0; 会转换为*(&origin+&Point3d::y-1);关于为什么减去1,留给自己去探究吧。我只知道,指向data member 的指针其offset总是被加1。

3.4.继承与Data Member:

C++ 继承模型中,一个derived class object 所表现出的东西,是其自己的members 加上其base classes members 总和。至于派生类和基类的成员排列顺序,并未进行强制规定。大部分编译器是把base classes members 放在开头。但是遇到了vitrual 特性之后,一切就变得复杂了。
1.只要继承不要多态:
在此中情况下是比较简单的。派生类对象的内存模型符合基类数据成员+自身的数据成员。但是是否考虑过如下一点,如果基类中含有边界调整的内容,那么会被派生类继承而来嘛?但是是肯定的。简单阐释下原因,当我们用基类指针指向派生类时,为了不发生错误,所以边界调整的内容也必须要继承下来。详细的示例可以参考此书。
2.加上多态:
这种情形就不能以基本的对象成员模型进行量化了,因为编译器要合成一个vptr 成员,我们必须妥善安置这个指针。关于把vptr 放在哪里一直是编译器领域的一个主要话题。一开始的cfront 编译器是放在尾部,这样可以保留base class C struct 布局。但是后来C++中出现新的特性,比如虚拟继承。此时有人就把指针放在头部。我们上述讨论的话题是基于如下的情况:基类中没用虚函数,但是派生类中定义了虚函数。
当我们的基类中定义了虚函数,那么虚指针可以跟随基类一起继承而来,如果派生类中定义了自己的虚函数,那么在相应的索引位置上进行替换,这个模型现在比较常见。
3.多重继承:
多重继承的情况很复杂。但是仍然满足基类的对象要被完整的继承而来。继承的顺序有一定的影响,比较复杂,感兴趣的可以自己研究。
4.虚拟继承:
偶然看过有人称为钻石模型,最近简单的例子就是C++中的ios 库的继承模型了,是典型的虚拟继承。在虚拟继承中我们知道一点,虚基类只会在派生类中存在一份实例,然而如何实现却是大难点。有的编译器是通过安插指向虚基类的指针解决问题,但是间接存取却带来了性能上的麻烦。还有一种解决方式是把信息索引存在一个指针里面。这个地方说的指针也是虚指针,但其同时包含了虚机类的偏移信息。具体的可以上图一看,感兴趣的可以细细研究。

3.5.Object Member Efficiency:

直接说结论,在优化的模式下,封装不会造成存取成本的负担。也就是说在通过函数存取对象成员抑或是通过对象直接存取,效率进化相等。但是遇到虚特性一切又变得很复杂。

3.6.Pointer to Members:

考虑如下代码,得到的应该是什么:
& Point3d::z 大部分肯定会说是地址,但是细细问下是z在内存中的地址嘛?答案是否定的,我们获得不是z 在内存中的地址,而是其在对象中的偏移位置+1bytes.你知道为何要加上一个字节嘛?这个问题在于如何区分一个没有指向任何data members 的指针,和一个指向第一个data member 的指针。考虑如下代码:

int Point3d::* p1=0;int Point3d::* p2=&Point3d::x;p1==p2 ?

我们如何区分p1和p2(我们假设x成员在对象的头部,即偏移位置是0)。所以CPP的设计者决定对便宜位置加上1.不得不承认是个极好的解决办法。
下面解释下&origin.z;&Point3d::z 的区别。其实很容器猜到了。对第一个进行取址获得是在内存中真是地址,不再是什么偏移量之类的东西。
最后说一点,间接存取肯定会造成性能上负担。例如上文提到的,通过指向data members's pointer 进行存取会造成负担.int Point3d::*ax=&Point3d::x 然后我们以pa.*ax的方式进行存取操作势必会带来极大的负担。

后记:其实最近我一直在纠结是否应该写下去,这篇文章的前半部分我是早一个星期前写下的,当时由于种种原因写了一半。当时我就放在桌面上时刻提醒自己。但是有那么些瞬间我开始怀疑自己,是否有必要写下去。因为我很清楚的意识到写的这些玩意可能效果微乎其微,但是不写却又是如鲠在咽。最后强迫症烦了,所幸写完。其实我一直期望看似无用功的东西能给我带来一点欢愉,那么我将感到很开心。哈哈,博客马上都要成为写心情的地方了。
May 3, 2015 9:16 PM
By Max

1 0
原创粉丝点击