`
xitonga
  • 浏览: 587574 次
文章分类
社区版块
存档分类
最新评论

深度探索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。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics