这次我们想讨论一下,Data Member 的存取开销。
如下代码:
Point3D origin;
origin.x=0.0;
x的存取开销是什么? 这个问题不能一概而论,必须视 x 和 Point3D如何定义而定。x可能是 static member 也可能是 nonstatic member。而Point3D可能是一个独立的class , 也可能是从另一个单一的 base class 派生而来,甚至也有可能是从多重继承和虚拟继承而来。
再者 以下两者有什么重大差异?
Point3D origin , *pt =&origin;
origin.x=0.0;
pt->x=0.0;
Static Data Members
我们都知道,Static Data Member 是被提出于 class 之外的,并被视为一个 global 变量(但只在 class 范围内可见)。每一个 Static Data Members 的读写权限(private,protect,public),以及与 class 的关联,并不会导致任何的额外开销(时间空间上的)。
每一个 Static Data Members 都只有一个实例,存放在 程序的 data segment 之中。每次使用该变量时,都会被转换为对唯一extern实例的直接操作。
所以如果 x 是 Static Data Members 那么无论是Point3D::x=0.0
,origin.x=0.0;
还是pt->x=0.0;
效率都是一样的,并且不会因为 class 的缘故而带来额外开销。
什么? 你说他是多重继承或者是虚继承过来的怎么办? 没关系的,只要你敢来我都一样对待,都是转换为访问唯一实例,存取路径还是那么的直接。
再来个更狠的,如果是写成这样子呢,foo().x =2;
(假设 foo() 返回Point3D类型的object)。C++ Standard 明确规定 foo()
函数是必须被调用的。
那么以上的情况,其实是可能被转换成了 (void)foo(); Point3D::x=2;
。
而且 如果是对一个 class 的 static data member 取址的话,会的到一个指向其数据类型的指针,而不是指向其 class member 的指针,因为 static member 并不含在一个 class object 之中;
&Point3D::x;
会的到类型如下的内存地址 const float*
。
Nonstatic Data Member
Nonstatic Data Member直接存放在每一个 class object之中。除非经由显式或隐式的class object,否则的话 无法直接操作它们。在class member function中处理 Nonstatic Data Member 被称为隐式 class object 操作。
比如:
Point3D Point3D::Translate(const Point3D &pt)
{
x+=pt.x;
y+=pt.y;
z+=pt.z;
}
上面我们看到的x ,y ,z的直接操作,其实是经过 this 指针隐式完成的。 所以事实上这个函数是这样子的:
Point3D Point3D::Translate(Point3D *const this,const Point3D &pt)
{
this.x+=pt.x;
this.y+=pt.y;
this.z+=pt.z;
}
所以说欲对一个 nonstatic data member 进行存取操作,编译器需要把 class object 起始地址,加上 data member 的偏移位置(offset)。
origin.y=0.0
那么 &origin.y
的地址将等于: &origin+(&Point3D::y-1);
这里减1 是因为指向data member的指针,其offset值总是被加上1的,这样可以是编译器区分出,”一个指向 data member 的指针,用以指出 class 的第一个 member”,和”一个指向data member的指针,没有指出任何 member”两种情况。
每一个 nonstatic data member的偏移位置(offset)在编译时期即可获知,甚至如果member 属于一个 base class subobject(派生自单一或者多重继承串链)也是一样的。因为,操作一个 nonstatic data member,其效率和存取一个 C struct member 或者 一个 nondrived class 的 member是一样的。
现在我们看看虚拟继承的情况。 虚拟继承将由”经由 base class subobject 操作 class member”,所以有一层的新的间接性:
Point3D *pt;
pt->x=0.0;
上述代码在,x是一个 struct member 和一个class member、单一继承、多重继承的情况下是完全相同的。 但,如果x 是一个 virtual base class 的member,操作时会有额外的开销的,这是因为虚继承下的间接性导致的。
那么我们再来看一下origin.x=0.0;
和pt->x=0.0;
的差异吧。
以上两者的差异只有当 Point3D是一个 drived class 而且其继承结构中有一个 virtual base class ,并且操作的 member 是一个从 virtual base class 继承而来的member时才会体现。
因为此时我们不能明确的指出,pt到底是指向哪一种 class type,所以在编译时期我们也不知道这个member的真正offset位置,所以这个操作必须推迟到执行期,然后经由一个额外的间接引导才能完成。
但是对于 origin来说,就不会有这个问题,其类型已经被指定为 Point3D class,而即使它继承自 virtual base class, member的 offset 的位置在编译时期就固定了。所以如果更为聪明的编译器,甚至可以静态的由origin 就解决掉对 x 的操作。