条款15 指向类成员的指针并非指针
“指向类成员的指针”这个描述中有“指针”这个术语,其实,这是不合适的,因为它们既不包含地址,行为也不像指针。
如果你已经熟稔常规指针的声明语法,那么声明一个指向成员的指针,语法并不是太可怕:
- int *ip; // 一个指向int的指针
- int C::*pimC; // 一个指针,指向C的一个int成员
所要做的全部事情就是使用classname::*而不是普通的*,来表明你在指向classname的一个成员。在其他方面,语法与常规的指针声明符一样。
- void * * *const* weird1;
- void *A::*B::*const* weird2;
其中weird1的类型为:一个指针,指向一个常量指针,后者又指向一个指针,后者又指向一个指针,后者则指向void。而weird2的类型为:一个指针,指向一个常量指针,后者指向B的一个成员,后者指向一个指针,后者指向A的一个成员,该成员为一个指向void的指针(没有别的意思,只是举个例子,在现实编程(www.cppentry.com)中不会看到这么复杂、这么愚蠢的声明)。
一个常规的指针包含一个地址。如果解引用该指针,就会得到位于该地址的对象:
- int a = 12;
- ip = &a;
- *ip = 0;
- a = *ip;
与常规指针不同,一个指向成员的指针并不指向一个具体的内存位置。它指向的是一个类的特定成员,而不是指向一个特定对象里的特定成员。通常最清晰的做法是将指向数据成员的指针看作为一个偏移量。当然,事情未必一定如此,因为C++(www.cppentry.com)标准对于一个指向数据成员的指针究竟该如何实现只字未提。标准只是说明了它的语法形式以及必须表现出来的行为。然而,大多数编译器都将指向数据成员的指针实现为一个整数,其中包含被指向的成员的偏移量,另外加上1(加1是为了让值0可以表示一个空的数据成员指针)。这个偏移量告诉你,一个特定成员的位置距离对象的起点有多少个字节。
- class C {
- public:
- //...
- int a_;
- };
- int C::*pimC; // 一个指针,指向C的一个int成员
- C aC;
- C *pC = &aC;
- pimC = &C::a_;
- aC.*pimC = 0;
- int b = pC->*pimC;
将pimC的值设置为&C::a_时,实际上是将pimC设置为a_在C内的偏移量。说得更明白一些,除非a_是静态成员,否则在表达式&C::a_中使用&并不会带来一个地址,而是一个偏移量。注意,这个偏移量适用于类型C的任何对象,换句话说,如果在一个C对象内成员a_距离起点的偏移为12字节,那么在任何其他C对象中,a_距离起点的偏移都是12字节。
给定一个成员在类内的偏移量,为了访问位于那个偏移量的数据成员,我们需要该类的一个对象的地址。这时候就需要.*和->*这两个看上去非同寻常的操作符闪亮登场了。当写下pC->*pimC时,其实是请求将pC内的地址加上pimC内的偏移量,为的是访问pC所指向的C对象中适当的数据成员。当写aC.*pimC时,是在请求aC的地址加上pimC中的偏离量,也是为了访问pC所指向的C对象中适当的数据成员。
指向数据成员的指针不如指向成员函数的指针那么常用,但前者对于描述“逆变性”(contravariance)的概念很方便。在C++(www.cppentry.com)中,存在从指向派生类的指针到指向其任何公有基类的预定义转换。我们常说在派生类与其公共基类之间存在着is-a关系,并且这种关系在对问题领域进行分析时会自然而然地出现(参见“多态”[条款2])。例如,可以宣称一个Circle是一个Shape(通过公有继承),而且C++(www.cppentry.com)通过提供从Circle*到Shape*的隐式转换来支持这个宣称。注意,不存在从Shape*到Circle*的隐式转换,这样的转换毫无意义,因为可能存在许多不同类型的Shape,它们并不全是Circle。(说“一个Shape是一个Circle”,听起来很弱智,不是吗?)
在指向类成员的指针的情况下则恰恰相反:存在从指向基类成员的指针到指向公有派生类成员的指针的隐式转换,但不存在从指向派生类成员的指针到指向其任何一个基类成员的指针的转换。这个逆变性概念看来有违直觉,不过,如果回忆起指向数据成员的指针并非指向一个对象的指针,而只是对象内的一个偏移,就会明白了。
- class Shape {
- //...
- Point center_;
- //...
- };
- class Circle : public Shape {
- //...
- double radius_;
- //...
- };
因为一个Circle也是一个Shape,所以一个Circle对象内包含一个Shape子对象。因而,Shape内的任何偏移量在Circle内也是一个有效的偏移量。
- Point Circle::*loc = &Shape::center_; // OK,从基类到派生类的转换
然而,一个Shape未必是一个Circle,因此一个Circle的成员的偏移量在Shape内未必是一个有效的偏移量。
- double Shape::*extent =
- &Circle::radius_; // 错误!从派生类到基类的转换
说一个Circle里包含其基类Shape中的所有数据成员,这是说得通的(也就是说,它从Shape那里继承了那些成员),C++(www.cppentry.com)支持我们的说法,它提供了从指向Shape的成员的指针到指向Circle的成员的指针的隐式转换。说一个Shape包含Circle中所有的数据成员,是说不过去的(Shape没有从Circle那里“继承”任何东西),C++(www.cppentry.com)不允许从指向Circle成员的指针转换到指向Shape成员的指针,以此来提醒我们这一点。