C++基础系列--指向数据成员的指针

指向数据成员的指针,是一个有点神秘又颇有用处的语言特性,特别是如果你需要详细调查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++对象模型》


你可能感兴趣的:(C++基础系列--指向数据成员的指针)