深度探索C++对象模型:4.Function语意学
第四章: Function语意学
Nonstatic Member Functions(非静态成员函数)
Point3d obj;
Point3d *ptr = &obj;
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() const {
return sqrt(_x*_x+_y*_y+_z*_z);
}
C++的设计准则之一就是:nonstaticmember function至少必须和一般的nonmember function有相同的效率。这是因为编译器内部已将“member函数实例”转换为对等的“nonmember函数实例”。
举个例子,下面是magnitude()的一个nonmember定义:
float magnitude3d(constPoint3d *_this)const {
return sqrt(_this->_x *_this->_x +
_this->_y*_this->_y +
_this->_z*_this->_z );
}
而我们的member function版本将按如下步骤转换:
1.改写函数的signature以安插一个额外参数(this指针)到member function中,结果如下:
Point3d Point3d::magnitude(const Point3d * constthis)
之所以有两个const,是因为第一个const表示magnitude函数具有const性质,第二个const表示this是const指针。
2.将每一个对nonstatic data member的存取操作改为由this指针存取,结果如下
{
return sqrt(this->_x*this->_x +
this->_y *this->_y+
this->_z *this->_z);
}
3.将member function重新写成一个外部函数,并将函数名称经过magling(所谓的Name Mangling是指在member的名称上加上class名称以及member function的signature)处理,使它成为在程序中独一无二的语汇:
extern magnitude_7Point3dFv(
register Point3d* constthis);
现在这个函数已经村换号,而其每一个调用也都必须转换,如下:
obj. magnitude ()变为magnitude_7Point3dFv(&obj);
ptr-> magnitude()变为 magnitude_7Point3dFv(ptr);
normalize()函数将会被转换为一下形式,其中假设NRV可行:
void normalize_7Point3dFv(registerconst Point3d*constthis, 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;
return;
}
Virtue Member Function(虚拟成员函数)
如果normalize()是一个虚拟成员函数,那么下面的调用
ptr->normalize();
将会被内部转化为
( * ptr->vptr[ 1 ])( ptr );
其中:
Vptr表示右边一起产生的指针,指向virtual table。
1是virtual table slot的索引值,关联到normalize();
第二个ptr表示this指针。
但对于以下调用:
Obj.normalize();
上述调用的函数实例只可以是Point3d::normalize(),所以编译器会将它转化如下:
Normalize_7Point3dFv( &obj );
像上面经由一个class object调用一个虚函数,这种操作应该总是被编译器对待一般的nonstaticmember function一样加以决议。
Static Member Function(静态成员函数)
如果Point3d::normalize()是一个static memberfunction,一下两个调用:
obj.normalize();
ptr->normalize();
将被转换为一般的nonmember函数调用,像这样:
normalize_7Point3dSFv();
normalize_7Point3dSFv();
独立于class object之外的存取class object的static data member操作主要有两种方法:如下
1.程序上的解决之道是将0强制转换为一个class指针,因而提供一个this指针实例,如下面这个例子:
class Point3d{
public:
int object_count(){
cout<< x << endl;
return x;
}
private:
static intconst x = 5;
};
int main(){
((Point3d*)0 )->object_count();
return 0;
}
结果输出5.
2.语言层面上的解决之道,引入static member function。Static member function的主要特性就是它没有this指针。以下次要特性统统根源于主要特性:
1)他不能够直接存取其class中的nonstaticmembers
2)他不能够被声明为const、volatile或virtual
3)它不需要经由class object才被调用---虽然大部分时候它是这样被调用的
取一个static member function的地址,获得的将是其在内存中的位置,也就是其地址。由于static member function没有this指针,所以其地址的类型是一个nonmember函数指针,并不是指向class member function的指针。
Static member function由于缺乏this指针,因此差不多等同于nonmember function。
4.2 virtual member functions虚拟成员函数
Virtual function的一般实现模型(无继承):每一个class有一个virtual table,内含class之中有作用的virtual function的地址,然后每个object有一个vptr,指向virtual table的存在。这一节会讨论单一继承,多重继承和虚拟继承这三种情况。
单一继承:
在编译时期:会准备好虚函数的地址值存放在虚函数表中,并且表中的这组地址是固定不变的,表本身的大小也不会改变;为了找到表格会为每个class object安插一个指针指向该表格。
执行期要做的是在特定的virtual table slot中**virtual function。
在单一继承中,一个class只会有一个virtual table。每一个table内含其对应 的class object中所有的activate virtual function函数实例的地址。这些activevirtual function包括:
1)这一class所定义的函数实例。它会改写一个可能存在的baseclass virtual function函数实例。
2)继承自base class的函数实例
3)一个pure_virtual_called()函数实例。
举例如下:
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 table如下图所示:
如果有Point2d派生自Point如下:
class Point2d:public Point{
public:
Point2d( float x = 0.0, floaty = 0.0)
:Point(x),_y(y){}
~Point2d();
Point2d &mult(float);
float y() const {return _y;}
protected:
float _y;
};
一共有三种可能性:
1)他可能继承base class声明的virtualfunction的函数实例,则该函数实例的地址会被拷贝到derived class的virtual table的相应slot中。
2)它可能使用自己的函数实例,表示它自己的函数实例必须放在相应slot中。
3)它可能加入一个新的virtual function,这个时候virtual table的尺寸会增大一个slot,而新的函数实例会被放进slot中。
Point2d的内存布局和virtual table如上图所示。
类似的情况,Point3d派生自Point2d。
class Point3d:public Point2d{
public:
Point3d( float x = 0.0, floaty = 0.0, float z =0.0)
:Point2d(x,y),_z(z){}
~Point3d();
Point3d &mult(float);
float z() const {return _z;}
protected:
float _z;
};
Point3d的内存布局和virtual table如上图所示。
现在如果有这样的式子:
ptr->z();
一般而言,每次调用z()时,并不知道ptr所指对象的真正类型,但是可以知道经由ptr可以存取到该对象的virtual table,可以知道每一个z()函数地址被放在slot 4中。所以编译器可以将该调用转化为:
(*ptr->vptr[ 4 ] )( ptr );
在这一转换中,唯一在执行期才能知道的东西是:slot 4所指的到底是哪一个z()函数实例。
多重继承下的virtual function
已知如下多重继承
class Base1{
public:
Base1( );
virtual voidspeakClearly(){};
virtual Base1* clone( ) const{};
protected:
float data_Base1;
};
class Base2{
public:
Base2( );
virtual void mumble(){};
virtual Base2* clone( ) const{};
protected:
float data_Base2;
};
class Derived:public Base1,public Base2{
public:
Derived( ){};
virtual Derived* clone( ) const{};
protected:
float data_Derived;
};
“Derived支持virtual functions”的困难度,统统落在Base2 subobject身上。有三个问题需要解决。
1)virtual destructor
2)被继承下来的Base2::mumble()
3)一组clone()函数实例
首先,我们把一个从heap中配置而得的Derived对象的地址,指定给一个Base2指针:
Base2* pbase2=new Derived;
新的Derived对象的地址必须调整,以指向其Base2 subobject。编译时期会产生以下的代码:
Derived* temp=new Derived;
Base2* pbase2=temp?temp+sizeof(Base1):0;
如果没有这样的调整,指针的任何“非多态运用”(像下面那样)都将失败:
//即使pbase2被指定一个Derived对象,这也应该没有问题
Pbase2->data_Base2;
当程序员要删除pbase2所指的对象时:
//必须首先调用正确的virtual destructor函数实体
//然后施行delete运算符
//pbase2可能需要调整,以指出完整对象的起始点
Delete pbase2;
指针必须被再一次调整,以求再一次指向Derived对象的起始处(推测它还指向Derived对象)。然而上述的offset加法却不能够在编译时期直接设定,因为pbase2所指向的真正对象只有在执行期才能确定。
一般规则是,经由指向“第二或后继之base class”的指针(或reference)来调用derived class virtual function。
//如下例一样
Base2 *pbase2=new Derived;
...
Delete pbase2;
该调用操作所连带的“必要的this指针调整”操作,必须在执行器完成。也就是说,offset的大小,以及把offset加到this指针上头的那一小段程序代码,必须由编译器在某个地方插入。问题是,在那个地方?
Bjame原先实施与cfront编译器中的方法是将virtualtable加大,使它容纳此处所需的this指针,调整相关事物。每一个virtual table slot,不再只是一个指针,而是一个聚合体,内含可能的offset以及地址。于是virtual function的调用操作由:
(*pbase2->vptr[1])(pbase2);
改变为:
(*pbase2->vptr[1].faddr)
(pbase2+pbase2->vptr[1].offset);
其中faddr内含virtualfunction地址,offset内含this执政调整值。
这个做法的缺点是,它相当于连带处罚了所有的virtual function调用操作,不管它们是否需要offset的调整。
比较有效的方法是利用所谓的thunk。
所谓thunk是一小段assembly代码,用来(1)以适当的offset值调整this指针,(2)跳到virtual function去。例如,经由一个Base2执政调用Derived destructor,其相关的thunk可能看起来是这个样子:
Pbase2_dtor_thunk:
This+=sizeof(base1); //总感觉这里应该是this-=sizeof(base1) ???
Derived::~Derived(this);
Thunk技术允许virtual table slot继续内含一个简单的指针,因此多重继承不需要任何空间上的额外负担。Slots中的地址可以直接指向virtual function,也可以指向一个相关的thunk(如果需要调整this指针的话)。于是,对于那些不需要调整this指针的virtual function而言,也就不需承载效率上的额外负担。
调整this指针的第二个额外负担就是,由于两种不同的可能:(1)经由derived class(或第一个base class)调用,(2)经由第二个(或其后继)base class调用,同一函数在virtual table中可能需要更多笔对应的slots。例如:
Base1* pbase1=new Derived;
Base2* pbase2=new Derived;
Delete pbase1;
Delete pbase2;
虽然两个delete操作导致相同的Derived destructor,但它们需要两个不同的virtual tableslots:
1、pbase1不需要调整this指针(因为Base1是最左端base class之故,它已经指向 Derived对象的起始处)。其virtual table slot需放置真正的destructor地址。
2、Pbase2需要调整this指针。其virtual table slot需要相关的thunk地址。
在多重继承之下,一个derived class内含n-1个额外的virtual tables,n表示其上一层base classes的数目(因此,单一继承将不会有额外的virtual tables)。对于本例之Derived而言,会有两个virtual tables被编译器产生出来:
1、一个主要实体,与Base1共享。
2、一个次要实体,与Base2有关。
针对每一个virtual tables,Derived对象中有对应的vptr。Vptrs将在constructor(s)中被设立初值(经由编译器所产生出来的代码)。
用以支持“一个class拥有多个vritualtables”的传统方法是,将每一个tables以外部对象的形式产生出来,并给予独一无二的名称。例如,Derived所关联的两个tables可能有这样的名称:
Vtbl__Derived:
Vtbl__Base2__Derived;
于是当你将一个Derived对象地址指定给一个Base1指针或Derived指针时,被处理的virtual table是主要表格vtbl__Derived。而当你将一个Derived对象地址指定给一个Base2指针时,被处理的virtual table是次要表格vtbl_Base2_Derived。
开节之前,我们曾提到有三种情况会影响对virtual functions的支持。
第一种情况是,通过一个“指向第二个base class”的指针,调用derived class virtualfunction。例如:
Base2 *ptr=new Derived;
//调用Derived::~Derived
//ptr必须被向后调整sizeof(Base1)个bytes
Delete ptr;
从图4.2之中,你可以看到这个调用操作的重点:ptr指向Derived对象中的Base2 subobject;为了能够正确执行,ptr必须调整指向Derived对象的起始处。
第二种情况是第一种情况的变化,通过一个“指向derived class”的指针,调用第二个base class中一个继承而来的virtual function。在此情况下,derived class指针必须再次调整,以指向第二个base subobject。例如:
Derived* pder=new Derived;
//调用Base2::mumble( )
//pder必须被向前调整sizeof(Base1)个bytes
Pder->mmble( );
第三种情况发生于一个语言扩充性质之下,允许一个virtual function的返回值类型有所变化,可能是base type,也可能是public derived type。这一点可以通过Derived::clone( )函数实体来说明。Clone函数的Derived版本传回一个Derived class指针,默默地改写了它的两个base class函数实体。当我们通过“指向第二个base class”的指针来调用clone( )时,this指针的offset问题于是诞生:
Base2* pb1=new Derived;
//调用Derived* Derived::clone( )
//返回值必须被调整,以指向Base2 subobject
Base2 *pb2=pb1->clone( );
当进行pb1->clone( )时,pb1会被调整指向Derived对象的起始地址,于是clone( )的Derived版会被调用;它会传回一个指针,指向一个新的Derived对象;该对象的地址在被指定给pb2之前,必须先经过调整,以指向Base2 subobject。
虚拟继承下的virtual functions
考虑下面的virtual base class派生体系,从Point2d派生出Point3d:
class Point2d{
public:
Point2d(float=0.0,float=0.0);
virtual ~Point2d( );
virtual void mumble();
virtual float z( );
//...
protected:
float _x,_y;
};
class Point3d:publicvirtual Point2d{
public:
Point3d(float=0.0,float=0.0,float=0.0);
~Point3d( );
float z( );
protected:
float _z;
};
虽然Point3d有唯一一个baseclass,也就是Point2d,但Point3d和Point2d的起始部分并不像“非虚拟的单一继承”情况那样一致。由于Point2d和Point3d的对象不再相符,两者之间的转换也就需要调整this指针。至于在虚拟继承的情况下要清除thunks,一般而言已经被证明是一项高难度技。
指向Member Function的指针
取一个nonstatic memberfunction的地址,如果该函数是nonvirtual,得到的结果是它在内存中真正的地址,然而这个值也不是完全的。它也需要被绑定于某个class object的地址上,才能够通过它调用函数。所有的nonstaticmember function都需要对象的地址。
一个指向member function的指针,其声明语法如下:
Double //函数返回类型
(Point::* //类的名称,::符号,以及*符号
pmf) //指针名称
(); //函数的参数列表
然后我们可以这样定义并初始化该指针:
Double(Point::*coord)() = &Point::x;
也可以赋值:
Coord =&Point::y;
欲调用它,可以这么做:
(orgin.*coord)();
Ptr->(*coord)();
这些操作会被编译器转换如下:
(coord)(&origin);
(coord)(ptr);
支持指向virtual memberfunction的指针
多态仍然运行,举例如下:
float(Point::* pmf)() = &Point::z;
Point *ptr = new Point3d;
(ptr->*pmf)();//被调用的是Point3d::z()
然而面对一个virtual function,其地址在编译时期是未知的,所能知道的仅是virtualfunction在其相关的virtual table中的索引值。也就是说,对一个virtual table function取其地址,所能获得只是一个索引值。例如,假设我们有以下Point声明
class Point{
public:
virtual ~Point();
float x();
float y();
virtual float z();
}
取其destructor的地址:&Point::~Point得到的结果是1。取x()或y()的地址得到的是函数在内存中的地址,因为他们不是virtual。取z()的地址得到的是2。通过pmf来调用z(),会被转换如下:
(*ptr->vptr[(int)pmf])(ptr);
所以pmf的内部定义必须允许能够判断出两种不同类型,因为nonvirtual的函数指针为内存地址而virtual函数指针为索引值。
在多重继承之下:指向MemberFunction的指针
为了让指向member function的指针能支持多重继承和虚拟继承,stroustup设计了下面的结构体:
struct _mptr{
int delta;
int index;
union{
ptrtofunc faddr;
int v_offset;
};
};
Index和faddr分别持有virtualtable索引和nonvirtual member function地址。在此模型下,像这样的调用操作:
(ptr->*pmf)();
会变成:
(pmf.index<0) ?
(*pmf.faddr)(ptr) :
(*ptr->vptr[pmf.index](ptr));
而microsoft导入所谓的vcall thunk。在此策略下,faddr被指定的要不就是真正的member function地址,要不就是vcall thunk的地址。于是virtual或nonvirtual函数的调用操作透明化,vcall thunk会选出并调用相关的virtual table中的适当slot。