前言
通过指向Data Members的指针,我们可以更加细致地探究对象布局,比如说了解vptr是在顶端还是底端,又或者说了解class中access sections的次序。
问题实例
考虑下面的Point3d,其内含一个virtual function,一个static member,以及三个坐标值:1
2
3
4
5
6
7
8class Point3d{
public:
virtual ~Point3d();
...
private:
static Point3d origin;
float x,y,z;
};
该class object内部会含有vptr,我们不能确定的是其位于头部还是尾部。
在一个32位的机器上,一个float是4bytes,一个vptr也是4bytes。因此,三个坐标值在对象中的偏移量应该是0,4,8(vptr位于尾部)或4,8,12(位于头部)。但如果你去取data members的地址,传回的值会总是多1,也就是1,5,9或5,9,13。下面将详细描述原因。
问题剖析
如何区分一个“没有指向任何data member”的指针和一个“指向第一个data member的指针”?考虑如下例子:1
2
3
4
5
6float Point3d::*p1 = 0;//Point3d::*的意思在于“指向Point3d data member”的指针类型
float Point3d::*p2 = &Point3d::x;
if(p1==p2){//如何区分p1 p2?
cout << "contain same value" << endl;
}
为了区分p1与p2,每一个真正的member offset值都会被加1,因此编译器和开发者都应当明确,在真正使用该值以指出一个member之前,先减1。
真正Data地址与offset
在明确了“指向data members的指针”之后,我们发现解释& Point3d::z
与& origin.z
之间的差异则非常容易。
取一个nonstatic data member的地址将获得其在class中的offset,而取一个绑定于真正object身上的data,将会得到该member在内存中的真正地址。。那么我们执行1
& origin.z - & Point3d::z
返回得到的就是origin的其实地址,但其类型是float *
而非float Point3d::*
。
多重继承
在多重继承下,如果需要将后继base class的指针和一个“derived class object data member”相结合,会由于需要加入offset而变得相当复杂,举例而言:1
2
3
4
5
6
7
8
9
10
11
12
13struct Base1 {int val1;};
struct Base2 {int val2;};
struct Derived:Base1,Base2 {...};
void func1(int derived::*dmp,Derived *pd){
//期待的是一个指向derived的data member指针
//但实际传入的可能是一个指向base class的data member的指针
pd->*dmp;
}
void func2(Derived *pd){
//bmp==1,但在derived中,val2==5
int Base2::*bmp = &Base2::val2;
func1(bmp,pd);
}
当bmp被作为func1的第一个参数时,其值必须受到调整,否则pd->*dmp
将存取到Base::val1
,而非开发者所以为的Base::val2
。为了解决这个问题,必须由编译器转为:1
2//非null判定
func1(bmp?bmp+sizeof(Base1):0,pd);
“指向Members的指针”的效率问题
接下来作者以一系列数据比较了不同编译器优化下经由指针存取members的效率。