第三章 数据语义学
本章着重介绍了C++的类数据对象的布局。
对于一般的类来说,影响内存布局的几个因素主要有:
1、 语言本身的负担,如vptr等;
2、 编译器对特殊情况的处理;
3、 字节对齐的限制。
这些都影响了类的大小。
C++标准没有明确规定编译器各种数据的编排规则,只是规定“较晚出现的成员具有较高的地址”。所以,在一个public里声明八个数据和声明八个public的数据占用的空间是一样的。不用在意成员在类中的顺序。
下面通过一些比较将本段讲的一些内容反映出来。
1、静态数据成员和一般类成员的区别:
静态数据成员是储存在数据区的,而一般类成员存储在栈中;
如果两个类中声明了相同的静态成员,编译器会自动mangling;
普通成员调用依赖于类对象的存在,而静态成员不是;
对于一个普通的类成员,其声明如下:
Point3d Point3D::translate(const Point3d &pt)
{
x+=pt.x;
y+=pt.y;
z+=pt.z;
}
通过编译器的转换,变为如下形式:
Point3d Point3D::translate(Point3d *const this, const Point3d &pt)
{
This->x+=pt.x;
This->y+=pt.y;
This->z+=pt.z;
}
就是说,每个成员函数都加了一个this指针,以实现对类成员的调用。
2、直接取类数据于指针取类数据的区别:
对一个非静态成员进行存取操作,就是将类对象的地址加上数据成员的偏移地址。
对于一般的类来说,通过a.get()和a->get()取是没有区别的。但是对于有虚拟基类的情况,就有区别了。普通的取法在编译的时候,各个成员的offset就已经定了,直接取出即可;对于指针来说,由于编译的时候不能确定指针到底指向哪个类型,所以只有执行期才能进行存取操作。
类设计时的问题:考虑效率,或者类结构清晰性之间的矛盾。
对于有三个数据成员的类,到底是放到一个类中呢还是使用两个继承呢?
如
Class A
{
……
Private:
Int x,
Char y, z;
};
或者:
Class A
{
……
Private:
Int x;
};
Class B : public A
{
……
Private:
Char y;
};
Class C : public B
{
……
Private:
Char z;
};
这两种设计各有优越性,第一钟效率较高,第二种比较清晰。
第一种占用的空间:4+1+1=6=8;
第二种占用的空间:
类A: 4; 类B:占1补3 = 4; 类C:占1补3 = 4。共占用12字节。
为什么第二种方法中不将前一个类中未占用的补齐呢?作者还解释了这样做的无奈。由于要考虑到mebemvise的拷贝构造方式,如果补齐,当A类向B类或C类拷贝时,就会出现数据错误。
多重继承的数据成员布局
这里解释了这样的问题:当一个类从多个类继承时,并且这些类可能从其他类中继承的情况下,类的数据结构如何布局?
答案是,先将基类的成员依次排列,最后放本类的数据成员。
问题是有多个基类,这些基类的顺序是怎样的呢?C++标准并没有定义。但是一般来说,编译器会把先定义的基类的数据成员放在前面。
由此,写代码的时候,如果需要将派生类转换为基类类型时,必须注意以下两点:
1、 如果在派生类中,该基类成员排在所有数据最前面,则直接转换,不需要任何代价;
2、 如果在派生类中,该基类成员没有排在最前面,则编译器会自动转换,加上一定的位移。这样做是有代价的。
虚拟继承的数据成员布局
当一个类A继承自两个基类B、C,而B、C都继承自同一个父类D时,会遇到这样的问题:类A中可能有两个类D的对象。要解决这个问题,引入了虚拟继承的概念。
传统的解决方法是,编译器对每一个基类对象加一个指针。但是,如果继承链过长,会导致指针增多;且类对象的负担不能固定。
目前的编译器想到了很多解决的方法来解决这个问题。
微软用的方法是利用了virtual base class table,将虚拟基类的指针放在该表中,通过该表得到指针实现基类的访问。
Solaris的做法有些不一样。虽然也是用了virtual base class table,Solaris巧妙的使用了索引,正索引将访问到虚函数,而负索引则取到想要的虚拟基类的位移。注意是位移。
最后说明,基于以上的分析,取虚拟基类的数据成员代价是比较高的。最好声明的虚拟基类没有任何数据成员。
对象成员的效率
对变量、数组、继承、虚拟继承的应用,使用相同的算法,效率是不一样的。
很明显,变量、数据、继承(包括多重继承,但作者不确定,要视编译器而确定),效率都是一样的。虚拟继承的效率令人失望。
指向数据成员的指针
使用指向数据成员的指针,关键是看这个类的vptr放在类的哪个部位。一般放在头部或尾部。
对于某些编译器,往往还用在位移上加一个字节。加这个字节的目的是,区分没有指向任何数据成员的指针和已经指向一个数据成员的指针。因为,如果没有这个字节,当指针指向第一个类成员时,有可能和空指针的内容相同。
在VC编译器中,没有该附加的字节。
#include <stdio.h>
class Pointnd
{
public:
virtual test(){int i = 0;};
// static Pointnd origin;
float x, y, z;
};
void main()
{
Pointnd origin;
printf("::x = %p/n", &Pointnd::x);
printf("::y = %p/n", &Pointnd::y);
printf("::z = %p/n", &Pointnd::z);
printf("origin.x = %p/n", &origin.x);
printf("origin.y = %p/n", &origin.y);
printf("origin.z = %p/n", &origin.z);
}
输出结果:
::x = 00000004
::y = 00000008
::z = 0000000C
origin.x = 0012FF74
origin.y = 0012FF78
origin.z = 0012FF 7C