指向 Data Member 的指针及相关的效率影响

指向 data member 的指针是一个颇有用处的语言特性, 特别是如果你需要详细调查 class members 的底层布局的话。这个调查可以帮助你决定 vptr 是放在尾端还是起始处。 另一个用途是可以用来决定 clas 中 access sections 的次序。
考察以下代码, 其中有一个 virtual function, 一个 static data member, 以及三个坐标值:

class Point3d
{
public:
    virtual ~Point3d();
    //...
protected:
    static Point3d origin;
    float _x, _y, _z;
};

每一个 Point3d class object 含有三个坐标值, 以及一个 vptr, 至于 static data member origin, 将被放在 class object 之外, 唯一可能因编译器不同而不同的是 vptr 的位置。C++ standard 允许 vptr 被放在对象中的任何位置, 但是实际上, 所有编译器不是把 vptr 放在头部就是把它放在尾部。
那么, 齐某个坐标成员的地址, 代表什么意思? 例如, 以下操作所得到的值代表什么:

&Point3d::_z;

上述操作将得到 _z 坐标在 class object 中的偏移量, 最低限度其值将是 _x 和 _y 大小总和, 因为 C++ 语言要求同一个 access level 中的 members 的排列次序应该和声明次序相同。
然而 vptr 的位置就没有限制, 再次重复, 实际上 vptr 不是放在对象的头部, 就是放在对象的尾部。 在一部 32位的机器上,每一 float 是 4 bytes, 所以我们应该期望刚才获得的值要不就是 8, 要不就是 12。但这比期望还是少 1, 也就是实际应该是 1, 5, 9 或 5, 9, 13等等, 为啥 Bjarne 要这么做呢?
问题在于, 如何区分一个没有指向任何 data member 的指针和一个指向第一个 data member 的指针?
考察以下代码:

float Point3d::*p1 = 0;
float Point3d::*p2 = &Point3d::_x;

问题来了,如何区分 p1 与 p2? 为了区分 p1 与 p2, 没一个真正的 member offset 的值都被加上 1, 因此不论编译器或使用这都必须记住, 在真正使用该值以指出一个 member 之前, 请先减掉 1。
另外, 理解 指向 data member 的指针后, 我们就会发现要解释:

&Point3d::_z;
&origin._z;

之间的差异就非常明确了, 取一个 nonstatic data member 的地址 将会得到它在 class 中的 offset, 而取一个绑定于 class object 身上的 data member 的地址将会得到该 member 在内存中的真正地址。把 &origin.z 所得结果减去 _z 的偏移值,并加 1, 就会得到 origin 的起始地址。 上一行的返回值的类型应该是 float* 而不是 float Point3d::* 由于上述操作所参考的是一个特定实例, 所以取一个 static data member 的地址, 意义也相同。
在多重继承之下,若要将第二个(或后继) base class 的指针和一个与 derived 绑定的 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::val2;
    //bmp == 1
    //但是在 Derived 中, val2 == 5
    Func1(bmp, pd);
}

当 bmp 被作为 FUnc1() 的第一个参数时, 它的值就必须因介入的 Base1 class 的大小调整, 否则 Func1 中这样的操作:
pd->*dmp;
将存取到 Base1::val1, 而非程序员所以为的 Base2::val2。要解决这个问题, 必须:

//编译器进行的内部转换
Func1(bmp + sizeof(Base1), pd);
//防范措施
Func1(bmp ? bmp + sizeof(Base1) : 0, pd);

我实际写了几行代码来打印上述各个 member 的 offset 值:

std::cout << &Base1::val1   << "\n";
std::cout << &Base2::val2   << "\n";
std::cout << &Derived::val1 << "\n";
std::cout << &Derived::val2 << std::endl;

经过 Visual C++ 12.0 编译后, 执行的结果都是 1.
指向 Member 的指针的效率问题
下面的测试企图获得一些测试数据, 让我们了解, 在 3D 坐标点的各种 class 的实现方式下, 使用指向 members的指针所带来的影响。 一开始的两个例子并没有继承关系, 第一个例子是要取得一个已绑定的 member 的地址:

float *ax = &pA.x;
//施以赋值加法、减法操作
*bx = *ac - *bx;
*by = *ax + *bx;
*bz = *az + *by;

第二个例子则是针对三个 members, 取得指向 data member 的指针的地址:
float Point3d::* ax = &Point3d::_x;
而赋值、加法和减法等操作, 都是使用指向 data member 的指针的语法, 把数值绑定到对象 pA 和 pB 中:

pB.*bx = pA.*ax - pB.*bz;
pB.*by = pA.*ay + pB.*bx;
pB.*bz = pA.*az + pB.*by;

根据具体实验发现, 为每一个 member 存取操作加上一层间接性(经由已绑定的指针), 会使执行的时间多出一倍不止, 以指向 member 的指针来存取数据, 再一次用掉了双倍时间, 要把指向 member 的指针绑定到 class object 的身上, 需要额外的把 offset 减 1。值得注意的是, 在优化之后, 这三种存取效率变得一致, 但只有 NCC 编译器除外。
简单的说, 不考虑继承时, 优化后除了 NCC 编译器, 其他编译器下三种方式效率相同, 在不优化的前提下,效率: 直接存取 > 使用指针存取 > 使用对象指针存取。
当考虑继承时, 一般的继承并不影响代码的效率, 但是如果是虚拟继承, 那么因为每一层虚拟继承都导入一个额外层次的间接性, 如:

pB.bx
//会被转换为
&pB->__vbcPoint + ( bx - 1 );
//而不是最直接的
&pb + ( bx - 1);

因此效率会受到影响。
以上。

时间: 2024-08-14 23:42:26

指向 Data Member 的指针及相关的效率影响的相关文章

C++对象模型——Data Member的存取(第三章)

3.3    Data Member的存取 已知下面这段代码: Point3d origin; origin.x = 0.0; x的存取成本是什么? 答案视x和Point3d如何声明而定,x可能是个 static member,也可能是个nonstatic member.Point3d可能是个独立(非派生)的 class,也可能从另一个单一的base class 派生而来;虽然可能性,但它甚至可能是从多重继承或虚拟继承而来.下面数节将依次检验每一种可能性. 先看这样一个问题,如果有两个定义,or

Data Member 的存取

考察以下代码: Point3d origin; origin.x = 0.0; 此例中 x 的存取成本是什么? 答案则是视 x 和 Pointd 而定(别打脸, 我知道这是废话). 具体的呢? 因为 x 可能是个 static member, 也可能是个 nonstiatic member; Point3d 可能是个独立的 class, 也可能是另一个 单一的class 派生而来:甚至可能是从多重继承或虚拟继承而来(请不要小看其他人的代码中的可能性, 你都很有可能不知道 C++ 还能这么写, 有

C++对象模型——&quot;继承&quot;与Data Member(第三章) .

3.4 "继承"与Data Member 在C++继承模型中,一个derived class object所表现出来的东西,是其自己的members加上其base class members的总和.至于derived class members和base class members的排列次序并未在C++ Standard中强制指定:理论上编译器可以自由安排.在大部分编译器上,base class members总是先出现,但属于 virtual base class的除外. 了解这种继

继承与 Data Member(2)

加上多态的情况如果我要处理一个坐标点, 而不在意这是一个 Point2d 或 Point3d 实例, 那么就需要在继承关系中提供一个 virtual function 接口: class Point2d { public: Point2d(float x = 0.0, float y = 0.0) :_x(x), _y(y){}; //x 和 y 的存取函数与前一个博客中相同 //由于对不同维度的点, 这些函数操作固定不变, 所以不必设为 virtual virtual float Z()(fl

深入了解Windows句柄到底是什么(句柄是逻辑指针,或者是指向结构体的指针,图文并茂,非常清楚)good

总是有新入门的Windows程序员问我Windows的句柄到底是什么,我说你把它看做一种类似指针的标识就行了,但是显然这一答案不能让他们满意,然后我说去问问度娘吧,他们说不行网上的说法太多还难以理解.今天比较闲,我上网查了查,光是百度百科词条“句柄”中就有好几种说法,很多叙述还是错误的,天知道这些误人子弟的人是想干什么. 这里我列举词条中的关于句柄的叙述不当之处,至于如何不当先不管,继续往下看就会明白: 1.windows 之所以要设立句柄,根本上源于内存管理机制的问题—虚拟地址,简而言之数据的

009实现一个算法来删除单链表中的一个结点,只给出指向那个结点的指针(keep it up)

呵呵,这个题不能直接删除已知的结点,因为是单链表,不知道前驱,只知道 后继结点,直接删除会使链表断开.不过我们可以删除已知结点的后继结点, 把后继结点的值赋值给已知结点. #include <iostream> struct Node { int data; Node* next; }; bool removeNode(Node* vNode) { if (vNode == NULL || vNode->next == NULL) return false; Node* pNext =

C++对象模型——Data Member的绑定(第三章)

3.1    Data Member的绑定 (The Binding of a Data Member) 考虑下面这段代码: // 某个foo.h头文件,从某处含入 extern float x; // 程序员的Point3d.h文件 class Point3d { public: Point3d(float, float, float); // 问题:被传回和被设定的x是哪一个x? float X() const { return x; } void X(float new_x) const

Data 语意学---Data member的存取效率

<深度探索C++对象模型> 对于data member来说,有两种情况 static data member数据 每一个static data member只有一个实体,存放在程序的data segment之中,无论以何种方式,无论类的继承关系如何复杂,存取路径都是非常直接 Nonstatic data members 直接存放在一个class object之中,是属于一个对象的,是需要一个叫做偏移量的值来索引的. 尤其是虚拟继承,虚拟继承将为"经由base class subobj

老问题了,函数返回指向常字符串的指针,形如 char *func()

摘自<c专家编程>,作为备忘 1. 直接返回简单的字符串 char *func() { return "Simple string demo!\n"} 这是最简单的解决方案,字符串常量存储在只读存储区,如果字符串需要通过计算得到,这种方式就不能使用 2. 使用全局字符数组 这种方式很容易理解,不多解释.但是全局变量尽量少用! 3. 使用静态数组,形如 char *func() { static char buffer[BUF_SIZE]; ... return buffer