The Semantics of Data

时隔很久,再次拾起<<深度探索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

请按以下描述,自定义数据结构,实现一个circular bufferIn computer science, a circular buffer, circular queue, cyclic buffer or ring buffer is a data structure that uses a single,fixed-size buffer as if it were connected end-to-end. This structure lends itself easily to bufering data streams. There were earlycircular buffer implementations in hardware. A circular buffer first starts out empty and has a set length. In the diagram below is a 7-element buffeiAssume that 1 is written in the center of a circular buffer (the exact starting locatiorimportant in a circular buffer)8Then assume that two more elements are added to the circulal2buffers use FIFOlf two elements are removed, the two oldest values inside of the circulal(first in, first out) logic. n the example, 1 8 2 were the first to enter the cremoved,leaving 3 inside of the buffer.If the buffer has 7 elements, then it is completely full6 7 8 5 3 4 5A property of the circular buffer is that when it is full and a subsequent write is performed,overwriting the oldesdata. In the current example, two more elements - A & B - are added and theythe 3 & 475 Alternatively, the routines that manage the buffer could prevent overwriting the data and retur an error or raise an exceptionWhether or not data is overwritten is up to the semantics of the buffer routines or the application using the circular bufer.Finally, if two elements are now removed then what would be retured is not 3 & 4 but 5 8 6 because A & B overwrote the 3 &the 4 yielding the buffer with:
最新发布
05-26
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值