条款16 指向成员函数的指针并非指针
获取非静态成员函数的地址时,得到的不是一个地址,而是一个指向成员函数的指针。
- class Shape {
- public:
- //...
- void moveTo( Point newLocation );
- bool validate() const;
- virtual bool draw() const = 0;
- //...
- };
- class Circle : public Shape {
- //...
- bool draw() const;
- //...
- };
- //...
- void (Shape::*mf1)( Point ) = &Shape::moveTo; // 不是指针
指向成员函数的指针的声明语法和指向常规函数的指针的语法一样,都不困难(不可否认,从目前的情况来看,指向常规函数的指针的语法已经够糟糕的了。参见“处理函数和数组声明符”[条款17])。与指向数据成员的指针一样,我们需要做的就是使用classname::*而不是* 来指明所指向的函数是classname的一个成员。然而,和指向常规函数的指针不同,指向成员函数的指针可以指向一个常量成员函数:
- bool (Shape::*mf2)() const = &Shape::validate;
和指向数据成员的指针的情形一样,为了对一个指向成员函数的指针进行解引用,需要一个对象或一个指向对象的指针。对于指向数据成员的指针的情形,为了访问该成员,需要将对象的地址和成员的偏移量(包含于指向数据成员的指针中)相加。对于指向成员函数的指针的情形,需要将对象的地址用作(或用于计算,参见“指针比较的含义”[条款28])this指针的值,进行函数调用,以及作为其他用途。
- Circle circ;
- Shape *pShape = ˆ
- (pShape->*mf2)(); // 调用Shape::validate
- (circ.*mf2)(); // 调用Shape::validate
->*和.*操作符必须加上圆括号,因为它们比()操作符优先级低,而且在调用函数之前首先应该找到这个函数。这与在(a+b)*c之类的表达式中的括号用法类似。在该表达式中,我们希望确保较低优先级的加法在较高优先级的乘法之前执行。
注意,不存在什么指向成员函数的“虚拟”指针。虚拟性是成员函数自身的属性,而不是指向它的指针所具有的属性。
- mf2 = &Shape::draw; // draw是虚函数
- (pShape->*mf2)(); // 调用Circle::draw
这也解释了为何一个指向成员函数的指针,通常不能被实现为一个简单的指向函数的指针。一个指向成员函数的指针的实现自身必须存储一些信息,诸如它所指向的成员函数是虚拟的还是非虚拟的,到哪里去找到适当的虚函数表指针(参见“编译器会在类中放东西”[条款11]),从函数的this指针加上或减去的一个偏移量(参见“指针比较的含义”[条款28]),以及可能还有其他一些信息。指向成员函数的指针通常实现为一个小型结构,其中包含这些信息。当然,也可以使用其他一些实现。解引用和调用一个指向成员函数的指针通常涉及到检查这些存储的信息,并有条件地执行适当的虚拟或非虚拟的函数调用序列。
和指向数据成员的指针一样,指向成员函数的指针也表现出一种逆变性,即存在从指向基类成员函数的指针到指向派生类成员函数指针的预定义转换,反之则不然。这很好理解:如果你考虑到基类成员函数会试图通过其this指针只访问基类成员,然而派生类函数可能会试图访问基类中不存在的成员的话。
- class B {
- public:
- void bset( int val ) { bval_ = val; }
- private
- int bval_;
- };
- class D : public B {
- public:
- void dset( int val ) { dval_ = val; }
- private:
- int dval_;
- };
- B b;
- D d;
- void (B::*f1)(int) = &D::dset; // 错误!不存在这种反向转换
- (b.*f1)(12); // 哎呀!访问不存在的dval成员!
- void (D::*f2)(int) = &B::bset; // OK,存在这种转换
- (d.*f2)(11); // OK,设置继承来的bval_数据成员