成员函数
假设有一个Point3d的指针和对象:
Point3d obj;
Point3d *ptr = &obj;
当进行如下操作:
obj.mormalize();
ptr->normalize();
时,会发生什么事情呢?其中的Point3d::normalize()定义如下:
Point3d Point3d::normalize() const{
register float mag = magnitude();
Point3d normal;
normal._x = _x/mag;
normal._y = _y/mag;
normal._z = _z/mag;
return normal;
}
而其中的Point3d::magnitude()又定义如下:
float Point3d::magnitude() const{
return sqrt( _x*_x + _y*_y + _z*_z );
}
答案是:需要视实际情况而定,C++支持三种类型的成员函数:static、nonstatic、virtual,每一种被调用的方式都不相同。
非静态成员函数(Nonstatic Member Functions)
C++的设计准则就是:非静态成员函数至少必须和一般的非成员函数有相同的效率。也就是说,如果我们要在以下两个函数之间做选择:
float magnitude3d( const Point3d *_this ){…}
float Point3d::magnitude3d() const{…}
那么,选择成员函数不应该带来什么额外负担。这是因为编译器内部已经将“member函数实体”转换为对等的“nonmember函数实体”。
举个例子,下面是magnitude()的一个nonmember定义:
float magnitude3d( const Point3d *_this ){
return sqrt( _this->_x*_this->_x +
_this->_y*_this->_y +
_this->_z*_this->_z );
}
乍看之下似乎非成员函数比较没有效率,它间接地经由参数取用坐标成员,而成员函数却是直接取用坐标成员。然而实际上成员函数被内化为非成员的形式,下面就是转化步骤:
1、改写函数的signature以安插一个额外的参数到成员函数中,用以提供一个存取管道,使class object得以调用该函数。该额外参数被称为this指针:
Point3d Point3d::magnitude( Point3d *const this )
如果member function是const,则变成:
Point3d Point3d::magnitude( const Point3d *const this )
2、将每一个“对非静态数据成员的存取操作”改为经由this指针来存取:
{
return sqrt( this->_x*this->_x +
this->_y*this->_y +
this->_z*this->_z );
}
3、将成员函数重新写成一个外部函数。对函数名称进行“mangling”处理,使它在程序中独一无二:
extern magnitude__7Point3dFv( register Point3d *const this );
现在这个函数已经转换好了,而其每一个调用操作也都必须转换。于是:
”obj.magnitude();”变成了:”magnitude__7Point3dFv(&obj);”
”ptr->magnitude();”变成了:”magnitude__7Point3dFv(ptr);”
前面提及的normalize()函数会被转化为下面的形式,其中假设已经声明有一个Point3d copy constructor,而named returned value(NRV)的优化也已施行:
void magnitude__7Point3dFv( register const Point3d *const this, Point3d &__result )
{
Register float mag = this->magnitude();
__result.Point3d::Point3d();
__result.x = this->_x/mag;
__result.y = this->_y/mag;
__result.z = this->_z/mag;
}
静态成员函数(Static Member Functions)
静态成员函数由于缺乏this指针,因此差不多等同于非成员函数。如果Point3d::normalize()是一个静态成员函数,以下两个调用操作:
obj.normalize();
ptr->normalize();
将被转化为一般的nonmember函数调用,像这样:
//obj.normalize();
normalize__7Point3dSfv();
//ptr->normalize();
normalize__7Point3dSfv();
静态成员函数的主要特性就是它没有this指针,其次要的特性统统根源于这个主要特性:
n 它不能够直接存取其class中的nonstatic members
n 它不能够被声明为const、volatile或virtual
n 它不需要经由class object才被调用--虽然大部分时候它是这样被调用的
一个静态成员函数,会被提到class声明之外,并给予一个经过“mangling”的适当名称。例如:
unsigned int Point3d::object_count()
{
Return _object_count;
}
会被cfront转化为:
//在cfront之下的内部转化结果
unsigned int object_count_5Point3dSFv()
{
Return _object_count_5Point3d;
}
其中SFv表示它是个static member function,拥有一个空白(void)的参数链表。
如果取一个静态数据成员的地址,得到的将是其在内存中的位置,也就是其地址。由于静态成员函数没有this指针,所以其地址的类型并不是一个“指向类成员函数的指针”,而是一个“非成员函数指针”。也就是说:
&Point3d::object_count();
会得到一个数值,类型是:
unsigned int(*)();
而不是:
unsigned int( Point3d::* )();
虚拟成员函数(Virtual Member Functions)
虚函数的一般实现模型是:每一个类有一个虚表,内含该类之中各虚函数的地址,然后每一个对象有一个vptr,指向虚表的所在。在这一小节,将根据单一继承、多重继承和虚拟继承等各种情况,从细节上探讨该实现方式。
在单一继承的情况下,一个class只会有一个virtual table,每一个table内含其对应的class object中所有active virtual function函数实体的地址。这些active virtual function包括:
n 这个类所定义的函数实体,它会改写一个可能存在的base class virtual function函数实体。
n 继承自base class的函数实体,这是在derived class决定不该写virtual function时才会出现的情况
n 一个pure_virtual_called()函数实体,它既可以扮演pure virtual function的空间保卫角色,也可以当做执行期异常处理函数。
每一个虚函数都被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的virtual function的关联。例如,在我们的Point class体系中:
class Point{
public:
virtual ~Point();
virtual Point& mult(float) = 0;
//...
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;
}
virtual destructor被赋值slot1,而mult()被赋值slot2。此例并没有mult()的函数定义,所以pure_virtual_called()的函数地址会被放在slot2中。如果该函数意外地被调用,通常的操作是结束掉这个程序。y()被赋值slot3而z()被赋值slot4。X()的slot是多少?答案是没有,因为它并不是虚函数。在上图中,可以清楚地看到相关的内存布局及其virtual table。
当一个类派生于Point时,会发生什么事情?例如,类Point2d:
class Point2d:public Point{
public:
Point2d( float x=0.0, float y=0.0 ):Point(x),_y(y){}
~Point2d();
Point2d& mult(float);
//...
float x() const { return _x; }
float y() const { return 0; }
//...
protected:
float _x;
}
一共有三种可能性:
n 它可以继承base class所声明的virtual functions的函数实体。正确地说,是该函数实体的地址会被拷贝到派生类的virtual table相对应的slot之中。
n 它可以使用自己的函数实体。这表示它自己的函数实体地址必须放在对应的slot之中。
n 它可以加入一个新的virtual function。这时候virtual table的尺寸会增大一个slot,而新的函数实体地址会被放进该slot之中。
Point2d的virtual table在slot1中指出destructor,在slot2中指出mult()(取代pure virtual function)。它自己的y()函数实体地址放进slot3,继承自Point的z()函数实体地址则放在slot4。
类似情况,Point3d派生自Point2d,如下:
class Point3d : public Point2d{
public:
Point3d( float x=0.0, float y=0.0, float z=0.0 ):Point2d(x,y),_z(z){}
~Point3d();
Point3d& mult(float);
//...
float z() const { return _z; }
//...
protected:
float _z;
}
其virtual table中的slot1放置Point3d的析构函数,slot2放置Point3d::mult()函数地址。Slot3放置继承自Point2d的y()函数地址,slot4放置自己的z()函数地址。
现在,如果有如下的语句:
ptr->z();
那么,如何有足够的知识在编译时期设定virtual function的调用呢?
n 一般而言,我们并不知道ptr所指对象的真正类型。然而,我们知道,经由ptr可以存取到该对象的virtual table。
n 虽然不知道哪一个z()函数实体会被调用,但我们知道每一个z()函数地址都被放在slot4。
这些信息使得编译器可以将该调用转化为:
( *ptr->vptr[4] )( ptr );
在这个转化中,vptr表示编译器所安插的指针,指向virtual table;4表示z()被赋值的slot编号。唯一一个在执行期才能知道的东西是:slot4所指的到底是哪一个z()函数实体?
在一个单一继承体系中,vritual function机制的行为十分良好,不但有效率而且很容易塑造出模型来。但是在多重继承和虚拟继承之中,对virtual functions的支持就没有那么美好了。
多重继承下的Virtual Functions
在多重继承中支持virtual functions,其复杂度围绕在第二个以及后继的基类身上,以及“必须在执行期调整this指针”这一点上。以下面的class体系为例:
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;
};
该多重继承体系的虚表布局情况如下所示:
首先,把一个从堆中配置而得的Derived对象的地址,指定给一个Base2指针:
Base2 *pbase2 = new Derived;
新的Derived对象的地址必须调整,以指向其Base2 subobject。编译时期会产生以下的代码:
Derived *temp = new Derived;
Base2 *pbase2 = temp ? temp + sizeof(Base1) : 0;
当程序员要删除pbase2所指的对象时:
delete pbase2;
指针必须再次被调整,以便再一次指向Derived对象的起始处。
一般规则是,经由指向“第二或后继之base class”的指针(或引用)来调用derived class virtual function,那么该调用操作所需“必要的this指针调整”操作,必须在执行期完成。也就是说,offset的大小,以及把offset加到this指针上头的那一小段程序代码,必须由编译器在某个地方插入。
调整this指针的另外一个负担是,由于两种不同的可能:(1)经由derived class(或第一个base class)调用,(2)经由第二个(或其后继)base class调用,同一函数在虚表中可能需要多笔对应的slots。例如:
Base1 *pbase1 = new Derived;
Base2 *pbase2 = new Derived;
//…
delete pbase1;
delete pbase2;
虽然两个delete导致相同的Derived destructor,但它们需要两个不同的virtual table slots:
n pbase1不需要调整this指针(因为Base1已经指向Derived对象都起始处)。其virtual table slot需放置真正的destructor地址。
n pbase2需要调整this指针,其virtual table slot需要相关的thunk地址。
Thunk解释:所谓thunk是一小段assembly代码,用来(1)以适当的offset值调整this指针,(2)跳到virtual function去。例如,经由一个Base2指针调用Derived destructor,其相关的thunk可能看起来是下面这个样子:
//虚拟C++代码
pbase2_dtor_thunk:
this += sizeof( base1 );
Derived::~Derived( this );
Thunk技术允许virtual table slot继续内含一个简单的指针,因此多重继承下不需要任何空间上的额外负担。slots中的地址可以直接指向virtual function,也可以指向一个相关的thunk(如果需要调整this指针的话)。
在多重继承下,一个derived class内含n-1个额外的virtual tables,n表示其上一层base classes的数目。对于本例而言,会有两个virtual table被编译器产生出来:
n 一个主要实体,与Base1(最左端base class)共享;
n 一个次要实体,与Base2(第二个base class)有关。
针对每一个virtual table,Derived对象中有对应的vptr。vptrs将在constructor(s)中被设立初值(经由编译器所产生出来的码)。
虚继承下的Virtual Functions
《深入探索C++对象模型》P168~169
参考资料:
《深度探索C++对象模型》