爱技术 & 爱分享
爱蛋蛋 & 爱生活

Data Member 指针

这个系列已经断更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 访问(仅针对以此种模型实现虚拟继承的编译器来说)。

对于这一情况这里就没有再去验证了,感兴趣的话可以动手试试。

赞(1)
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。编程日志 » Data Member 指针
分享到: 更多 (0)

游戏 && 后端

传送门传送门