指向数据成员的指针,是一个有点神秘又颇有用处的语言特性,特别是如果你需要详细调查class members的底层布局的话。这样的调查可以用于决定vptr是放在class的起始处或者尾端。另外一个用途是可以用来决定class中的access sections的次序。
考虑下面的Point3d声明。其中有一个virtual function,一个static data member,以及三个坐标:
class Point3d{
public:
virtual ~Point3d();
//…
protected:
static Point3d origin;
float x,y,z;
}
每一个Point3d的对象含有三个坐标值,依次为x、y、z,以及一个vptr。至于静态数据成员origin,将被放在class object之外。唯一可能因编译器不同而不同的是vptr的位置。C++标准允许vptr被放在对象中的任何位置:在起始处,在尾端,或者是在各个members之间。然而实际上,所有编译器不是把vptr放在对象的头部,就是放在对象的尾部。
那么,取某个坐标成员的地址,代表什么意思呢?例如,以下操作所得到的值代表什么:
&Point3d::z;
上述操作将得到z坐标在class object中的偏移量(offset)。最低限度其值将是x和y的大小总和,因为C++语言要求同一个access level中的members的排列次序应该和其声明次序相同。在一台32位机器上,每一个float是4个字节,所以我们应该期望刚才获得的值要不是8,就是12(在32位机器上,一个vptr是4个字节)。
然而,这样的期望还少了1个字节。对于C和C++程序员来说,这多少算是个有点年代的错误了。如果vptr放在对象的末尾,则三个坐标值在对象布局中的偏移量分别为0、4、8;如果vptr放在对象的开头,则三个坐标值在对象布局中的偏移量分别为4、8、12。然而你若去取data members的地址,传回的值总是多1,也就是1、5、9或5、9、12等等。
#include <iostream>
class Point3d{
public:
virtual ~Point3d(){};
//…
public://如果换成private或者protected,则报错
static Point3d origin;
float x;
float y;
float z;
};
int main()
{
printf("&Point3d::x = %p/n", &Point3d::x);
printf("&Point3d::y = %p/n", &Point3d::y);
printf("&Point3d::z = %p/n", &Point3d::z);
std::cout<<"&Point3d::x = "<<&Point3d::x<<std::endl;
std::cout<<"&Point3d::y = "<<&Point3d::y<<std::endl;
std::cout<<"&Point3d::z = "<<&Point3d::z<<std::endl;
return 0;
}
输出结果为:
&Point3d::x = 00000004
&Point3d::y = 00000008
&Point3d::z = 0000000C
&Point3d::x = 1
&Point3d::y = 1
&Point3d::z = 1
Press any key to continue
在vc6.0下,并没有增加1,原因可能是visual c++做了特殊的处理。
在vc6.0下,通过printf或者cout的形式,都可以正常运行,只不过,得到的结果不一致。使用std::cout时,都输出的是1,应该作何解释呢?
以上程序,如果数据成员为private或者protected的,则无法编译通过,而书上的例子,却是protected,作者的测试程序可能是怎样的呢?
(以上程序在vc6.0,virsual studio2008,DEV-C++下测试过,与《深入探索C++对象模型》P131对应的说明有些出入)
为啥传回的值会多1个字节呢?这一个字节,主要用来区分“没有指向任何数据成员的指针”和“指向第一个数据成员的指针”这两种情况。考虑下面这样的例子:
float Point3d::*p1 =0;
float Point3d::*p2 = &Point3d::x;
//Point3d::* 的意思是“指向Point3d data member”的指针类型
if( p1 == p2 ){
cout <<” p1 & p2 contain the same value.”;
cout <<”they must address the same member!”<<endl;
}
为了区分p1和p2,每一个真正的member offset值都被加上1。因此,不论编译器或者使用者都必须记住,在真正使用该值以指出一个member之前,请先减去1。
在充分认识“指向数据成员的指针”之后,要解释:
&Point3d::z;和 &origin.z
之间的差异,就非常明确了:取一个非静态数据成员的地址,将会得到它在class中的offset,取一个绑定于真正class object身上的数据成员的地址,将会得到该数据成员在内存中真正的地址。&origin.z的返回值类型应该是:float * 而不是:float Point3d::* 。
#include <iostream>
class Point3d{
public:
virtual ~Point3d(){};
//…
public:
float x;
float y;
float z;
};
int main()
{
Point3d origin;
printf("&origin.z = %p/n", &origin.z);
return 0;
}
输出结果为:
&origin.z = 0013FF7C
参考资料:
《深度探索C++对象模型》