今天和小伙伴讨论了第三章data语意学的指一些的知识,感觉很有必要总结一下,似乎不总结知识就会溜走,所以冒夜写一下吧。
首先看这样的一个继承的例子:
class X{ };
class Y: public virtualX { };
class Z: public virtualX { };
class A: public Y,publicZ { };
分别对X Y Z A取sizeof,结果可能是1, 8, 8, 12。当然我的机器是1, 4, 4, 8。一般来说老的机器会是第一种结果,因为编译器比较老。
解释第一个问题:为什么X的sizeof会是1。这是因为无论老的编译器还是新的编译器,都会给空类X安插一个char,这样会保证类X的两个objects在内存中得以配置独一无二的地址。
注意:这里开始先说老编译器
第二个结论:老的编译器会对所有的空类安插一个char。这样Y的内存就有一个char(1字节),以及一个指向虚基类的指针(4字节),为了对齐需要,又加3个字节,所以Y总共是8个字节。同理Z也是8个字节。
第三个结论:class A的大小。A的大小由以下决定:X的大小+Y的单纯大小(意思是要减去为了满足虚继承基类X而配置的大小)+Z的单纯大小。注意X是虚基类,虚基类在派生体系中只会有一份实例,这样即使A继承了Y, 又继承了Z, 但是它只有一份X的实例(1个字节),加上对齐3个字节,所以是4个字节。Y的纯大小,Y的总大小是8个字节,为了满足虚继承基类X而配置的头部指针(4字节),那么单纯大小是4字节,Z的单纯大小也是4字节,所以A的大小12字节。这样就回答了老的编译器1,8,8,12的情况。
注意:这里开始说新的编译器1,4,4,8的情况。也是我自己测得的情况。
首先,类X的大小依旧是1字节。因为安插一个char。
其次,Y没有必要再安插一个char了。新的编译器将空的虚基类视为派生类的最开头的部分(这里这部分理解为Y的member),派生类Y在内存里已经至少有4个字节了,那么类Y所对应的objects在内存里已经可以区分开来,所以不需要再安插char了。这样Y和Z就都是4个字节了(都是为了满足虚继承而配置一个指针)。
最后,A的8个字节。因为A继承Y和Z,所以A的大小是YZ之和为8,同时class X的实例安插的char可以被拿去了,因为A在内存里已经不空了,所以A是8字节。
最终结论:一个类到底多大。答:容纳的所有的nonstatic data members(非静态数据成员),同时加上编译器为了支持某些语言特性而自动加上的额外的data members(如vptr)。最后加上alignment(边界对齐)的需要。
数据成员的绑定问题。
这里遇到这样的一个问题,代码如下:
extern floatx;
class Point3d{
public:
Point3d(float,float,float);
//问题:被传回和被设定的x是哪个x呢
floatX() const { returnx; }
voidX(float new_x)const{ x = new_x; }
//..
private:
floatx, y, z;
};
老式的编译器在处理时,遇到第一个x出现的那一行会把x解析为全局的那个x,这样就引起了一些错误。
新式的编译器是直到遇见类声明的右括号以后才对类的函数进行数据绑定操作。这样避免了数据的绑定错误。
然而新式的编译器在面临这样的数据类型的问题时(问题如下),
typedef intlength;
class Point3d{
public:
//喔欧:length 第一次遇见,就被决议(resolved)为global
//_val会被决议为Point3d::_val,这个没问题。
voidmumble(length val){_val = val; }
length mumble(){ return _val; }
//..
private:
typedeffloatlength;
length _val;
//..
};
在这样的问题里,成员的解析没有问题,因为新式的编译器等类全部声明完以后才解析_val,所以_val被正确解析为类内的_val。然而数据类型length却不能被正确解析为我们所想要的类的length. 这种情况下,即使在新式的编译器内,也应该采取防御性程序风格:总是把“nested type 声明”放在class 的起始处。这样可以确保绑定的正确性。
数据成员的布局问题。
看这样的一个类,他的数据成员怎么布局呢。
class Point3d{
public:
// ..
private:
float x;
static List<Point3d*> *freeList;
float y;
static const int chunkSize = 250;
float z;
};
答:所有静态数据成员都不会放在对象的布局之中,非静态数据成员在类对象中的排列顺序和其被声明的顺序一致。
另,如前所述,编译器会增加一些内部使用的数据成员,比如vptr。C++标准允许编译器随意放置这些内部产生出来的数据成员,一般情况下,有的编译器会把vptr放在类对象的最前端。
对section的处理,
例子代码如下:
class Point3d{
public:
// ..
private:
float x;
static List<Point3d*> *freeList;
private:
float y;
static const int chunkSize = 250;
private:
float z;
};
结论:编译器会把各个access section连锁在一起,依照声明的顺序连成一个区块。由此section的多少并不会带来的负担。即:在一个section里声明8个变量,和在8个section各声明一个变量,得到的对象的大小是一样的。
数据成员的存取差别问题。
即考虑他们
origin.x = 0.0;
pt->x = 0.0;
的存取差别。
对于静态数据成员:
静态的member其实并不在class object之中,因此存取静态的数据成员并不需要通过class object。所以存取一个静态数据成员时,不需要额外付出什么成本,它在程序的全局区,取出其地址直接存即可。小补充:如果有两个classes,他们都声明了一个static member freeList,那么如果他们都直接放在程序的data segment时,就会导致名称冲突。这时候编译器会暗中对每一个static data member编码,以获得一个独一无二的程序识别代码,这种方法统称name-mangling。
对于非静态的数据成员:
首先要知道,非静态的数据成员其实是有一个隐含的类对象指针,即this指针。对于非静态数据成员的存取操作,需要类对象的地址加上数据成员的偏移地址。数据成员的偏移地址在编译期即可获知,即使数据成员是经过派生派生派生等而来的,其偏移地址一样可以获得,由此存取origin.x时,只需对象origin的地址,和x的偏移,所以存取origin.x=0.0时,其效率和存取一个C struct member 或 一个 nonderived class的member是一样的。
虚拟继承时,情况是不同的,考虑以下代码:
Point3d *pt3d;
pt3d->_x = 0.0;
这样的话,当_x是一个struct member, 一个class member, 单一继承、多重继承的情况下都完全相同。但是如果 _x是一个virtual base class 的 member,存取速度会稍微慢一些。
Point3d origin, *pt=&origin;
origin.x = 0.0;
pt->x = 0.0;
有什么重大的差异?
答案:当Point3d是一个derived class(派生类),并且其继承结构里有一个virtual base class(虚基类),同时x就是从该virtual base class 继承而来的member时,会有重大差异。因为这时候我们不能说pt必然指向哪一种class type(因此,我们就不知道编译时期,这个member x的真正的偏移地址),所以这个存取操作必须延迟至执行期,经由一个额外的间接引导,才能够解决。但是如果用origin就不会出现这样的问题,其类型无疑是Point3d class, 而它继承自virtual base class 的 members的偏移地址,在编译时期就已经固定。用我的话说就是:pt被声明为一个Point3d 的指针,这没问题,但是pt可以指向其他的类类型的对象,比如指向Point3d的派生类的对象,或者派生类的派生类的对象等等。不同类类型的x的偏移位置是不同的,所以在编译期,看到了pt,只能看到他声明的是Point3d 的指针,但是不能根据Point3d类的x偏移位置去计算x的偏移位置。只能放在运行时去确定,所以在这种情况下,效率会减慢一些。这也是多态的一个代价(此句待验证)。
先到这,如有错误或不对之处,欢迎探讨和纠正。