C++中的成员函数

成员函数

假设有一个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++支持三种类型的成员函数:staticnonstaticvirtual,每一种被调用的方式都不相同。

 

非静态成员函数(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 functionconst,则变成:

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指针,其次要的特性统统根源于这个主要特性:

它不能够直接存取其class中的nonstatic members

它不能够被声明为constvolatilevirtual

它不需要经由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包括:

这个类所定义的函数实体,它会改写一个可能存在的base class virtual function函数实体。

继承自base class的函数实体,这是在derived class决定不该写virtual function时才会出现的情况

一个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()被赋值slot3z()被赋值slot4X()slot是多少?答案是没有,因为它并不是虚函数。在上图中,可以清楚地看到相关的内存布局及其virtual table

 

当一个类派生于Point时,会发生什么事情?例如,类Point2d

class Point2dpublic 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;

}

一共有三种可能性:

它可以继承base class所声明的virtual functions的函数实体。正确地说,是该函数实体的地址会被拷贝到派生类的virtual table相对应的slot之中。

它可以使用自己的函数实体。这表示它自己的函数实体地址必须放在对应的slot之中。

它可以加入一个新的virtual function。这时候virtual table的尺寸会增大一个slot,而新的函数实体地址会被放进该slot之中。

Point2dvirtual tableslot1中指出destructor,在slot2中指出mult()(取代pure virtual function)。它自己的y()函数实体地址放进slot3,继承自Pointz()函数实体地址则放在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放置继承自Point2dy()函数地址,slot4放置自己的z()函数地址。

 

现在,如果有如下的语句:

ptr->z()

那么,如何有足够的知识在编译时期设定virtual function的调用呢?

一般而言,我们并不知道ptr所指对象的真正类型。然而,我们知道,经由ptr可以存取到该对象的virtual table

虽然不知道哪一个z()函数实体会被调用,但我们知道每一个z()函数地址都被放在slot4

这些信息使得编译器可以将该调用转化为:

( *ptr->vptr[4] )( ptr );

在这个转化中,vptr表示编译器所安插的指针,指向virtual table4表示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

pbase1不需要调整this指针(因为Base1已经指向Derived对象都起始处)。其virtual table slot需放置真正的destructor地址。

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 tablesn表示其上一层base classes的数目。对于本例而言,会有两个virtual table被编译器产生出来:

一个主要实体,与Base1(最左端base class)共享;

一个次要实体,与Base2(第二个base class)有关。

针对每一个virtual tableDerived对象中有对应的vptrvptrs将在constructor(s)中被设立初值(经由编译器所产生出来的码)

 

虚继承下的Virtual Functions

《深入探索C++对象模型》P168~169

 

 

 

参考资料:

《深度探索C++对象模型》

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值