4.2 虚拟成员函数

虚函数的一般实现模型:
1. 每一个类有一个虚表virtual table),内含该类的active虚函数的地址。
2. 每一个该类的对象有一个vptr,指向虚表。
其中active虚函数包括:
1. 这个类定义的虚函数实体,它会改写(overriding)一个可能存在的基类虚函数实体
2. 继承自基类的虚函数实体,这是派生类决定不改写基类虚函数时出现的情况
3. 一个pure_virtual_called()函数实体,它既可以扮演纯虚函数的空间保卫者,也可以当做执行期异常处理函数
C++中表示“以一个public base class的指针或引用,寻址出一个derived class object
识别一个class是否支持多态,唯一的适当方法就是看它是否有任何的虚函数,只要class有一个虚函数,它就需要这份额外的执行期信息以支持多态。
1)单一继承下的虚函数
对于以下代码,其虚表布局如图4.1.
class Point { 
public: 
   virtual ~Point(); 
 
   virtual Point& mult( float ) = 0; 
   // ... other operations ... 
 
   float x() const { return _x; } 
   virtual float y() const { return 0; } 
   virtual float z() const { return 0; } 
   // ... 
 
protected: 
   Point( float x = 0.0 ); 
   float _x; 
}; 
class Point2d : public Point { 
public: 
   Point2d( float x = 0.0, float y = 0.0 ) 
      : Point( x ), _y( y ) {} 
   ~Point2d(); 
 
   // overridden base class virtual functions 
   Point2d& mult( float ); 
   float y() const { return _y; } 
 
   // ... other operations ... 
 
protected: 
   float _y; 
}; 
class Point3d: public Point2d { 
public: 
   Point3d( float x = 0.0, 
            float y = 0.0, float z = 0.0 ) 
      : Point2d( x, y ), _z( z ) {} 
   ~Point3d(); 
 
   // overridden base class virtual functions 
   Point3d& mult( float ); 
   float z() const { return _z; } 
 
   // ... other operations ... 
protected: 
   float _z; 
}; 
对于下面语句:
ptr->z();
如何有足够的知识在编译时期设定虚函数的调用(即编译器如何转换)?
1. 一般而言,不知道ptr所指对象的真正类型,但知道经由ptr可以存取到该对象的虚表(virtual table
2. 不知道究竟是哪一个z()函数实体被调用,但知道每一个z()函数地址都放在slot 4
根据上述信息,编译器将其调用转换为:
( *ptr->vptr[4] )( ptr );
唯一在执行期才能知道的是:slot 4所指的到底是哪个z()函数实体。
2)多重继承下的虚函数
多重继承下支持虚函数,其复杂度在于第二个及后继的基类身上,以及必须在执行期调整this指针。
class Base1 {
public:
Base1();
virtual ~Base1();
virtual void speakClearly();
virtual Base1 *clone() const;
protected:
float data_Base1;
};
class Base2 {
public:
Base2();
virtual ~Base2();
virtual void mumble();
virtual Base2 *clone() const;
protected:
float data_Base2;
};
class Derived : public Base1, public Base2 {
public:
Derived();
virtual ~Derived();
virtual Derived *clone() const;
protected:
float data_Derived;
};
在多重继承下,一个派生类内含n-1个额外的virtual tablen表示其上一层基类的数目。
对于上述Derived类,有两个virtual table被编译出来:
1. 一个主要表格vtbl__Derived,与Base1(最左端基类)共享
2. 一个次要表格vtbl__Base2__Derived,与Base2(第二个基类)有关
当你将一个Derived对象地址指定给一个Base1指针或Derived指针时,被处理的virtual table是主要表格vtbl__Derived
当你将一个Derived对象地址指定给一个Base2指针时,被处理的virtual table是次要表格vtbl__Base2__Derived
其虚表布局如图4.2
有三种情况,第二或后继的基类会影响对虚函数的支持:
第一种情况:通过一个“指向第二个或后继的基类”的指针,调用派生类虚函数。
例如:
Base2 *ptr = new Derived;
// 调用Derived::~Derived
// ptr 必须向后调整 sizeof( Base1 ) 个字节以指向Derived对象的起始处
delete ptr;
第二种情况:通过一个“指向派生类”的指针,调用从第二个或后继的基类中继承而来的虚函数。
例如:
Derived *pder = new Derived;
// 调用 Base2::mumble()
// pder 必须向前调整 sizeof( Base1 ) 个字节以指向Base2 subobject的起始处
pder->mumble();
第三种情况:通过一个“第二个或后继的基类”的指针,调用clone()函数。
Base2 *pb1 = new Derived;
// 调用 Derived* Derived::clone()
// 返回值必须调整,以指向 Base2 subobject
Base2 *pb2 = pb1->clone();
3)虚拟继承下的虚函数
较复杂,略。

4.3 函数的效率

4.4 指向成员函数的指针

1. 取一个非静态成员函数的地址,如果它不是虚函数,则得到的是它在内存中真正的地址。然而这个值也是不完全的,它需要绑定在某个类对象地址上,才能够通过它调用该函数。
2. 取一个静态成员函数的地址,则它就是其在内存中的地址,其地址的类型并不是一个“指向类成员函数的指针”,而是一个“非成员函数指针”。(见4.1
3. 取一个非静态成员函数的地址,如果它是虚函数,见后文描述。
指向member function的指针的声明语法,以及指向member selection运算符的指针,其作用是作为this指针的空间保留者(这句话啥意思,没明白)。这就是为什么static member function(没有this指针)的类型是函数指针,而不是指向member function的指针。
使用一个成员函数指针,如果不用于虚函数,多重继承,虚基类等情况,并不会比一般的指针成本高。
1)指向虚函数的指针
对一个虚函数取地址,所能获得的是它在virtual table中的索引值。
2)多重和虚继承下的指向成员函数的指针
为了成员函数的指针也能支持多重和虚继承,Stroustrup设计了如下结构
// 一般结构,用于支持多重和虚继承下指向成员函数的指针  
struct __mptr { 
   int delta; //表示this指针的offset
   int index; 
   union { 
      ptrtofunc  faddr; //index-1时表示非虚函数地址,否则表示虚函数在virtual //table中的索引值
      int        v_offset; //存放一个虚(或多重继承中的第二或后继的)基类的vptr位置
   }; 
}; 

4.5 内联函数

一般而言处理内联函数有两个阶段
1.分析函数定义,以决定函数的内联的本资。如果判断其不可内联就会转化成静态函数。
2.真正的内联函数的扩展操作是在调用点上,这样会带来参数求值操作和临时对象管理。
1)形式参数
在内联扩展期间,发生了什么?
1. 如果实际参数是一个常量表达式,我们可以在替换之前先完成其求值操作;后继的inline替换,就可以把常量直接绑上去
2. 若实际参数会带来副作用,则需要引入临时对象
3. 若既不是常量表达式,也不是带有副作用的表达式,则直接替换之
2)局部变量
一般而言,内联函数中的每个局部变量都必须被放在函数调用的一个封闭区间中,拥有独一无二的名称。
如果内联函数以单一表达式扩展多次,那么每次都需要一组局部变量。以分离多个式子被扩展多次,那么只需要一组局部变量,就可以重复使用了。