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 table
,
n
表示其上一层基类的数目。
对于上述
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
)局部变量
一般而言,内联函数中的每个局部变量都必须被放在函数调用的一个封闭区间中,拥有独一无二的名称。
如果内联函数以单一表达式扩展多次,那么每次都需要一组局部变量。以分离多个式子被扩展多次,那么只需要一组局部变量,就可以重复使用了。