The Semantics of Data

        • 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

时间: 2024-10-10 20:49:50

The Semantics of Data的相关文章

【C++】深度探索C++对象模型读书笔记--Data语意学(The Semantics of data)

1. 一个空类的大小是1 byte.这是为了让这一类的两个对象得以在内存中配置独一无二的地址. 2. Nonstatic data member 放置的是“个别的class object”感兴趣的数据,static data members则放置的是“整个class”感兴趣的数据. 3. C++对象模型把nonstatic data members直接放在每一个classs object之中.对于继承而来的nonstatic data members(不管是virtual 还是nonvirtua

Data Types

原地址: Home / Database / Oracle Database Online Documentation 11g Release 2 (11.2) / Database Administration Data Types Each value manipulated by Oracle Database has a data type. The data type of a value associates a fixed set of properties with the va

Parallelized coherent read and writeback transaction processing system for use in a packet switched cache coherent multiprocessor system

A multiprocessor computer system is provided having a multiplicity of sub-systems and a main memory coupled to a system controller. An interconnect module, interconnects the main memory and sub-systems in accordance with interconnect control signals

From COM to COM 侯捷 1998.06.12

摘要: 本文簡介 C++ Object Model 和 Component Object Model 的基本概念,並引介四本書籍: 1. Inside The C++ Object Model 2. Essential COM 3. Inside COM 4. Understanding ActiveX and OLE -- A Guide for Developers & Managers 將近 8 年的時間,我把自己放逐在 Windows 領域裡,縱情學習與研究.我應該算是幸運的一群人之一,

【深度探索C++对象模型】第一章 关于对象

第一章 关于对象(Object Lessons) -- 本书作者:Stanley B.Lippman 一.前言 什么是 C++ 对象模型:简单的说,就是 C++ 中面向对象的底层实现机制. 本书组织: 第 1 章,关于对象(Object Lessons),介绍 C++ 对象的基础概念,给读者一个粗略的了解. 第 2 章,构造函数语意学(The Semantics of Constructors),构造函数什么时候会被编译器合成?它给我们的程序效率带来了怎样的影响? 第 3 章,Data语意学(T

Note for Computer Networks_1

Network Hardware 1. A computer network is a collection of computers connected by communication links 2. communication link: 1.Point to Point 2.Broadcast 3. Nodes: the entities in  the network(hosts, routers, servers, devices, programs, etc) Edges/Lin

[转]《深度探索C++对象模型》读书笔记[一]

前 言 Stanley B.Lippman1.   任何对象模型都需要的三种转换风味: ü        与编译器息息相关的转换 ü        语言语义转换 ü        程序代码和对象模型的转换 2.   C++对象模型的两种解释 ü        语言中直接支持面向对象程序设计的部分 ü        对于各种支持的底层实现机制 3.   C++ class的完整virtual functions在编译时期就固定下来了,程序员没有办法在执行期动态增加或取代其中某一个.这使得虚拟函数调

AVPacket

AVPacket注解 AVPacket 是一个结构体,存储压缩数据.可作为编码器的输出,解码器的输入. 对于 Video 一般包含一个压缩帧,对于 Audio 可能包含多个压缩帧. 编码器允许输出空 packets,没有包含压缩数据,仅包含附加数据,比如在编码结尾更新参数. AVPackets 是 FFmpeg 中为数不多结构体,它的size的公有 ABI 的一部分,只能被 libavcodec 和 libavformat 在栈上分配. This structure stores compres

单例 对类变量的修改 批修改

#!python# -*- coding:utf-8 -*-# 场景:# 目的:通过单例实现客户端调用sdk时,sdk中的方法对客户端数据的批处理 # 参考:# {# Python单例模式(Singleton)的N种实现 - 知乎# https://zhuanlan.zhihu.com/p/37534850 # 设计模式(Python)-单例模式 - 简书# https://www.jianshu.com/p/ec6589e02e2f # http://xiaorui.cc/2016/04/10