这个系列已经断更2年多了,如今再更一篇,虽然已经学习完这部分的知识很久了。了解这部分知识可能不会在你实际工作中起到直接作用,不过能对C++对象的底层实现有所了解在定位问题上能提供很大的帮助。接下来有时间的话,会尽量把这一系列完结,把之前做的笔记变成文章分享出来。
指向 Data Member的指针,这是一个比较少见的概念,不过在某些情况,你想了解对象的底层布局的时候比较有用。
有这么一个类
class Point3D
{
public:
virtual ~Point3D();
//...
protect:
static int w;
float x,y,z;
}
Point3D origin;
每一个Point3D的实例对象都包含3个坐标值,依次为x,y,z以及一个vptr(可能在实例内存布局中的任何位置,但是现代编译器几乎都放在首部或者尾部)。static member 被放置在实例对象之外。
Nonstatic Data Member
那么&Point3D::z
的含义是什么呢?
上述操作得到的是data member z在 class object中的偏移位置(offset)。在上述的情况下,最小值为x和y的大小总和,因为C++语言标准要求同一个access level的members的排列顺序应该与其声明顺序相同。
int main()
{
printf("%p\n", &Point3D::x);
printf("%p\n", &Point3D::y);
printf("%p\n", &Point3D::z);
return 0;
}
在64位环境下,Visual Studio 2019和 GCC 9.2编译,上述代码的输出都是
0000000000000008
000000000000000C
0000000000000010
但是早期的编译器可能会输出
0000000000000009
000000000000000D
0000000000000011
为什么会有这样的情况呢? 请假设上面的析构函数非虚,即~Point3D();
,则在64位环境下,Visual Studio 2019和 GCC 9.2编译,上述代码的输出都是
0000000000000000
0000000000000004
0000000000000008
那么如何区分p1与p2呢?
float Point3D:: *p1 = 0;
float Point3D:: *p2 = &Point3D::x;
所以历史上编译器就把每一个data member的offset都加上1,即输出:
0000000000000001
0000000000000005
0000000000000009
但是现代处理器都对这种情况做了特殊处理,所以不能再复现这种情况了。
注意,下面的输出方式有问题。
int main()
{
cout << (&Point3D::x) << endl;
cout << (&Point3D::y) << endl;
cout << (&Point3D::z) << endl;
return 0;
}
在64位环境下,Visual Studio 2019和 GCC 9.2编译,上述代码的输出都是
1
1
1
那么我们再来看看&Point3D::x
和&origin.x
的差异。取一个class的nonstatic data member的地址会得到其在对应class中的offset,取一个class object的nonstatic data member的地址会得到这个object的member在内存中的真实地址。
所以&origin.z
减去&Point3D::z
就得到origin在内存中的起始地址。
并且&origin.z
的类型为float *
而不是float Point3D::*
。
Static Data Member
取static data member的地址,无论是&Point3D::w
形式还是&orgin.w
得到的都是class object origin的data member w在内存中的地址,而不是偏移。
Multiple-Inheritance
这一部分要讲的是,在多重继承下将第二个(或后继)的指向base class 的 data member的指针
做实参传递给形参是指向drived class 的 data member的指针
时。需要改变offset值而变得相当复杂。
struct Base1 { int val1; };
struct Base2 { int val2; };
struct Drived : Base1, Base2 { int val3; };
void func1(int Drived::* dmp, Drived* pd)
{
//期待的是Drived clas的member的指针
//但若是传进来的是Base2的member的指针会怎样呢?
printf("%p\n", dmp);
pd->*dmp;
}
void func2(Drived* pd)
{
int Base2::* bmp = &Base2::val2;
//val2在Base2中的offset是0,但是在Drived中的offset是4
printf("%p\n", bmp);
func1(bmp, pd);
}
void func3()
{
printf("%p\n", &Base1::val1);
printf("%p\n", &Base2::val2);
printf("%p\n", &Drived::val1);
printf("%p\n", &Drived::val2);
printf("%p\n", &Drived::val3);
}
int main()
{
Drived objDrived;
func2(&objDrived);
func3();
return 0;
}
在64位环境下,Visual Studio 2019和 GCC 9.2编译,上述代码的输出都是
0
4
0
0
0
0
8
说明这里是经过调整的。否则的话func1中取到的应该是val1,并且dmp会是0。
但是这里有点不解的是&Drived::val2
的值居然是0,有点费解,但是&Drived::val3
的输出是8,说嘛的确前面是存在val1和val2的,只能猜测为编译器有过特殊处理了。
Data Member指针的效率
Point3D pA;
Point3D pB;
float *ax=&pA.x;
float *ay=&pA.y;
float *az=&pA.z;
float *bx=&pB.x;
float *by=&pB.y;
float *bz=&pB.z;
//直接取class object data member的地址,然后进行数据运算操作
*bx = *ax - *bz;
*by = *ay + *bx;
*bz = *az - *by;
float Point3D::*px = &Point3D::x;
float Point3D::*py = &Point3D::y;
float Point3D::*pz = &Point3D::z;
//与先取class data member 的指针然后再与具体对象进行绑定再进行数据运算操作
pB.*px = pA.*px - pB.*pz;
pB.*py = pA.*py - pB.*px;
pB.*pz = pA.*pz - pB.*py;
单继承情况下,对比直接取class object data member的地址进行数据运算操作,与先取class data member 的指针然后再与具体对象进行绑定再进行运算操作,未优化时后者效率较差,但是经过优化后,效率一致。
虚拟继承下这个差距更为明显,并且经过优化后差距依然很明显,说明编译器对这种情况依然没有有效的手段处理。因为虚拟继承时,每一层虚拟继承都要导入一个额外的层次间接性,经由virtual base class point 访问(仅针对以此种模型实现虚拟继承的编译器来说)。
对于这一情况这里就没有再去验证了,感兴趣的话可以动手试试。