3.6 指向Data Membrs的指针

指向Data Members的指针有两个用处:第一,可以调查class members的底层布局;第二。可以用来决定class 中的accesss section的顺序。

考虑如下的Point3d的声明。其中有一个virtual function,一个static data member,以及三个坐标值:

class Point3d
{
public:
	virtual Point3d();
	//...
protected:
	static Point3d origin;
	float x,y,z;
};

每一个Point3d class object含有三个坐标值,依次为x,y,z,以及一个vptr。至于static data member origin,则将放在class object 之外。唯一可能因编译器的不同而不同的是vptr的位置。C++ Standard允许vptr被放在对象的任何位置:在起始处,在尾端,或是在各个members之间。然而实际上,所有编译器不是把vptr放在对象的头,就是放在对象的尾。

在一台32位机器上,每一个float是4bytes,所以我们应该期望针对z取地址的值应该是8或者12(如果vptr放在前面)。但是作者的机器却每次多1,针对三个坐标值对象布局中offset 0,4,8;如果vptr放在对象的前端,三个坐标值就是4,8,12。作者的机器却是1,5,9和5,9,13。(我的centos7 64bit测试的结果是0x08,0xC,0x10,vptr占8bytes,就如侯杰老师所说,他在Microsoft Visual C++5.0测试中为4,8,C一样,原因可能编译器做了特殊处理,其道理对于empty virtual base class想接近)。

多一字节为了区分“没有指向任何data member”的指针,和一个指向指向“第一个data member”的指针。例如:

float Point3d::* p1 = 0;
float Point3d::* p1 = &Point3d::x;

if(p1 == p2)
{
	cout << "p1 & p2 contain the same value ==";
	cout << "they must address the same member!" << endl;
}

为了区分p1和p2,没一个真正的member offset值会被加上1,因此bi俺一起或使用者都必须记住,真正使用该值指出一个member之前,请先减掉1。

对于nonstatic data member取地址将会得到它在class中的offset,取一个“绑定于真正class object身上的data member”的地址(和static data member的地址),将会得到member在内存中真正的地址。

在多重继承下,若要将第二个(后继)base class的指针,和一个“于derived class object绑定”的member结合起来,那么将会因为“需要加入offset”而变得相当复杂。

struct Base1{int val1};
struct Base2{int val2};
struct Derived: Base1,Base2{...};

void func1(int Derived::* dmp,Derived* pd)
{
	//期望第一个传进来是一个指向Derived class 之 member的指针
	//如果传进来的是一个指向base class 之 member会怎样?
	pd->*dmp;
}

void func2(Derived* pd)
{
	//bmp将成为1
	int Base2::*bmp = &Base2::val1;
	
	//但是在Derived中,val2 == 5
	func1(bmp,pd);
}

当bmp被视为func1的第一个参数时候,它会因介入的base1 class大小而调整,否则func1的这样的操作pd->*dmp将存取的是val1,而非程序员所以为的Base::val2。要解决这个问题,必须:

func1(bmp + sizeof(Base1),pd);

//不保证bmp不是为0,所以
func1(bmp ? bmp + sizeof(Base1) : 0,pd);

“指向Members的指针”的效率问题

下面的测试数据,在3D坐标点的各种class 的实现方式下,使用“指向member的指针”所带来的影响。一开始的例子并没有继承关系,第一个例子要取得一个“已绑定的member”的地址:

float* ax = &pA.x;

//然后赋值操作
*bx = *ax - *bz;
*by = *ay + *bx;
*bz = *az + * by;

第二例子针对三个members,取得”指向data members之指针“的地址:

float Point3d::*ax = &Point3d::x;

//然后赋值操作
pB.*bx = pA.*ax - pB.*bz;
pB.*by = pA.*ay + pB.*bx;
pB.*bz = pA.*az + pB.*by;

3.6 指向Data Membrs的指针

 未优化的结果如图,为每一个”member“加上一层间接操作(经由已绑定的指针),会使执行时间多出一倍不止。以”指向member的指针“来存数据,再一次几乎用掉了双倍的时间。要把”指向member的指针“绑定到class object身上,需要额外的把offset减1。

针对继承对于“指向data member的指针”所带来的效率影响。

第一个例子,独立的Point class被重新设计为一个三层的单一继承,每一个class 有一个member。

第二个例子,仍然是三层单一继承,但导入一层虚拟继承:Poin2d虚拟派生自Point。结果,每次对Point::x的存取,将是对一个virtual base class data member的存取。

最后一个例子,实用性很低,加上第二层虚拟继承,使Point3d虚拟派生自Point2d。测试结果如下:

3.6 指向Data Membrs的指针

由于被继承的data member是直接存放在class object之中,所以继承的引入一点也不会影响效率。虚拟继承索带来的主要影响是,它妨碍了优化的有效性。每一层的虚拟继承都会导入一个额外的层次间接性。 每次存取Point::x,像这样:

pB.*px;

//转换成如下
&pB->_vbcPoint + (bx - 1);

//而不是如下
&pB + (bx -1);

额外的间接性会降低“把所有都搬移到缓存器中执行”的优化能力。