考察以下代码:
Point3d origin; origin.x = 0.0;
此例中 x 的存取成本是什么? 答案则是视 x 和 Pointd 而定(别打脸, 我知道这是废话)。 具体的呢? 因为 x 可能是个 static member, 也可能是个 nonstiatic member; Point3d 可能是个独立的 class, 也可能是另一个 单一的class 派生而来;甚至可能是从多重继承或虚拟继承而来(请不要小看其他人的代码中的可能性, 你都很有可能不知道 C++ 还能这么写, 有时在这句话之后还能加一个“我勒个去”) 而下面, 咱们就来检验这每一种可能性。
在开始之前, 先抛出一个问题, 如果我们有两个定义, origin 和 pt:
Point3d origin, *pt = &origin; //用他们来存取 data members, 像这样: origin.x = 0.0; pt->x = 0.0;
通过 origin 存取和 pt 存取, 有什么重大差异呢? 让我们在最后得出答案。
Static Data Members
static data members 被编译器提出于 class 之外, 并被视为一个 global 变量(但只在 class 生命范围内可见)。 每一个 member 的存取许可(无论是 public, private 还是 protected), 以及与 class 的关联, 都不会导致时间或是空间上的额外负担(无论是个别的 class object 还是 static data member 本身), 因为每一个 static data member 都只有一个实体, 存放在程序的 data segment 之中, 每次取用 static member, 就会被内部转化为唯一的 extern 实体的直接参考操作, 如:
//origin.chunkSize = 250; Point3d::chunkSize = 250; //pt->chunkSize = 250; Point3d::chunkSize = 250;
从指令执行的观点来看, 这是 C++ 语言中通过一个指针和通过一个对象来存取 member, 结论完全相同的唯一一种情况。 这是因为经由 member selection operators(说人话就是 ‘.‘ 运算符) 对一个 static data member 进行存取操作只是语法上的一种变异形式而已。 member 其实并不在 class object 中, 因此 存取 static members 并不需要通过 class object。
那么如果 chunkSize 是一个从复杂关系中继承而来的 member, 又当如何? 或许它是一个 virtual base class 的 virtual base class(或更加复杂的情况) 的 member 也说不定, 那咋办呢?哦, 无所谓, 程序之中对于 static members 还是只有唯一一个实体, 而其存取路径依然是那么直接。
那么如果 static data member 的存取是经由函数调用(或其他某些语法) 而被存取呢? 例如:
Foobar().chunkSize = 250;
调用 Foobar() 会发生什么事? 在 C++ 的标准中, 没人知道会发生什么事,因为 ARM 并未指定 Foobar() 是否必须被求值(evaluated)。 cfront 的做法就是把它丢掉(= =!) 但 C++ Standard 明确要求 Foobar() 必须被求值, 哪怕其结果毫无用处, 下面就是一种可能的转化:
//你看到的 //Foobar().chunkSize = 250; //实际上可能的代码 //对表达式求之后, 丢弃结果 (void) Foobar(); Point3d.chunkSize = 250;
若取一个 static data member 的地址, 会得到一个指向其数据类型的指针, 而不是一个指向其 class member 的指针, 因为 static member 并不在内含在一个 class object 之中,例如:
&Pointd::chunkSize; //会得到如下的内存地址: const int*
那如果有两个 classes, 每一个都声明了一个 static member freeList, 那么当他们都被放在程序的 data segment 时, 就会导致名称冲突, 对此编译器的解决方案是暗中对每一个 static data member 编码(这个手法有一个很美的名称: name-mangling) 疑惑的一个独一无二的程序识别代码, 有多少编译器就有多少种 name-mangling 做法。所谓的 name-mangling 做法主要就是两点:
1. 一种算法, 推导出独一无二的名称;
2. 万一编译系统必须要和使用者交谈,那些独一无二的名称可以轻易被推导回到原来的名称。
Nonstatic Data Members
Nonstatic data members 直接存放在每一个 class object 之中。 除非经由明确的(explicit) 或暗喻的(implicit) class object, 不然没有办法存取它们。只要程序员在一个 member function 中直接处理一个 nonstatic data member, 所谓 implicit class object 就会发生, 考察以下代码:
//你看到的 Point3d Point3d::Tranlate(const Point3d &pt) { _x += pt._x; _y += pt._y; _z += pt._z; } //实际可能的代码 //member function 的内部转化 Point3d Point3d::Transelate(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 的偏移量, 例如:
origin._y = 0.0;
那么地址 &origin + (&Point3d::_y - 1);
要注意的是其中的 -1 操作, 指向 data member 的指针, 其偏移量(offset) 的值总是被加上 1, 这样可以使编译系统区分出 “一个指向 data member 的指针, 用以指出 class 的第一个 member” 和 “一个指向 data member 的指针, 没有指出任何member” 两种情况。其中指向 data members 的指针将在以后的博客中探讨。
每一个 nonstatic data member 的偏移量在编译时起即可获知, 甚至如果 member 属于一个 base class subobject(派生自单一或多重继承串链) 也是一样, 因此存取一个 nonstatic data member 的效率 == C struct member == nonderived class 的 member 。
但是对于虚拟继承略有不同, 虚拟继承将为经由 base class subobject 存取 class members 导入一层新的间接性, 例如:
Point3d *pt3d; pt3d->_x = 0.0;
其执行效率在 ——x 是一个 stuct member, 一个 classmember, 单一继承, 多重继承的情况下都完全相同, 但如果 ——x 是一个virtual base class 的member, 存取速度会慢一点。
回到一开始的问题, 从 origin 存取和从 pt 存取 有什么重大差异? 答案是当 Pointd 是一个derived class, 而在其继承结构中有一个 virtual base class, 且被存取的 member 是一个从该 virtual base class 继承来的 member 时, 就会产生重大差异。 因为这个时候我们无法确定 pt 到底指向哪一种 class type(这就导致我们无法知道编译期这个 member 真正的 offset 位置), 所以这个存取必须延迟到执行期, 经由一个额外的间接导引, 才能解决。 但如果用 origin 就不存在这样的问题,因为 origin 的归属毫无疑问, 而他继承自 virtual base class, member 的 offset 位置也在编译时期就固定了, 一个积极进取的编译器甚至可以静态的经由 origin 就解决掉对 _x 的存取。