《深度探索C++对象模型》—Function语意学(The Semantics of Function)

Function语意学(The Semantics of Function)

Member的各种调用方式

Nonstatic Member Functions

C++的设计准则之一:nonstatic member function 至少必须和一般的nonmember function有相同的效率。

编译器内部会将“member函数实体”转换为对等的“nonmember函数实体”:

//1. 改写函数签名,安插一个this参数到member function中,以指明所属对象
//non-const nonstatic member
Point3d Point3d::magnitude();
//-->
Point3d Point3d::magnitude(Point3d *const this); //this指针是const


//---------------------------------------------
//const nonstatic member
Point3d Point3d::magnitude() const;
//-->
Point3d Point3d::magnitude(const Point3d *const this);

//2. 将每一个对nonstatic data member的存取操作改为经由this指针来存取

{
    return sqrt(
    this->_x * this->_x +
    this->_y * this->_y +
    this->_z * this->_z );
}

//3. 将member function重写成一个外部函数,并对函数名字进行mangling处理
extern magnitude__7Point3dFv(
    register Point3d *const this);

于是,每一个调用的转换如下:

obj.magnitude();
//-->
magnitude__7Point3dFv( &obj );

ptr->magnitude();
//-->
magnitude__7Point3dFv( ptr );

C++由于要支持重载,所以需要使用mangling来制造独一无二的函数名。如果你声明extern “C”,就会压抑nonmember functions的“mangling”效果。

(函数签名包括:函数名称、参数数目、参数类型),因此返回类型声明错误,将无法检查出来!

Virtual Member Function

假设normalize()是一个虚函数,则以下调用:

ptr->normalize();

将会被内部转化为:

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

其中vptr是由编译器产生的指针,指向virtual table(事实上其名称也会被“mangled”,因为在一个复杂的class派生体系中,可能存在有多个vptrs);

1是virtual table slot的索引值,关联到normalize()函数;

第二个ptr表示this指针。

以上是通过指针(或引用)来访问虚函数的转换,如果是直接通过对象来操纵,这时由于对象本身并不具备多态性,所以对于虚函数的调用跟普通的nonstatic member function并无差别:

//Point3d obj;
obj.normalize();
//-->
normalize__7Point3dFv(&obj);

//(*obj.vptr[1])(&obj); //语意正确,但是没有必要

Static Member Functions

如果normalize是一个静态成员函数,则如下转换:

obj.normalize();
//->
normalize__7Point3dSFv();

ptr->normalize();
//->
normalize__7Point3dSFv();

Static Member Functions的主要特性:它没有this指针。

以下次要特性根源于其主要特性:

  1. 它不能直接存取其class中的nonstatic members;
  2. 它不能被声明为const、volatile或virtual;
  3. 它不需要经由class object才被调用。

Static Member Functions由于缺乏this指针,因此差不多等同于nonmember function。它提供了一个意想不到的好处:成为一个callback函数。

Virtual Member Function

为了支持virtual function机制,必须首先能够对于多态对象有某种形式的“执行期类型判断法”,比如以下调用需要ptr在执行期的某些相关信息:

ptr->z();

以找到并调用z()的适当实体(比如是基类的z()还是某个派生类的z())。

要明确z()实体,我们需要知道:

  1. ptr所指对象的真实类型,这可使我们选择正确的z()实体;
  2. z()实体的位置,以便我们能够调用它。

下面我们只讨论单继承体系下的虚函数。

虚函数的实现:

  1. 为每个有虚函数的类配一张虚函数表,它存储该类类型信息和所有虚函数执行期的地址。
  2. 为每个有虚函数的类插入一个指针(vptr),这个指针指向该类的虚函数表。
  3. 给每一个虚函数指派一个在表中的索引。

用这种模型来实现虚函数得益于在C++中,虚函数的地址在编译期是可知的,而且这一地址是固定不变的。而且表的大小不会在执行期增大或减小。

一个类的虚函数表中存储有类型信息(存储在索引为0的位置)和所有虚函数地址,这些虚函数地址包括三种:

  1. 这个类定义的虚函数,会改写(overriding)一个可能存在的基类的虚函数实体——假如基类也定义有这个虚函数。
  2. 继承自基类的虚函数实体——基类有定义,而这个类却没有定义,直接继承之。
  3. 一个纯虚函数实体。用来在虚函数表中占座,有时候也可以当做执行期异常处理函数。

每一个虚函数都被指派一个固定的索引值,这个索引值在整个继承体系中保持前后关联,例如,假如z()在Point虚函数表中的索引值为2,那么在Point3d虚函数表中的索引值也为2。

当一个类单继承自有虚函数的基类的时候,将按如下步骤构建虚函数表:

  1. 继承基类中声明的虚函数——这些虚函数的实体地址被拷贝到继承类中的虚函数表中对于的slot中。
  2. 如果有改写(override)基类的虚函数,那么在1中应将改写(override)的函数实体的地址放入对应的slot中而不是拷贝基类的。
  3. 如果有定义新的虚函数,那么将虚函数表扩大一个slot以存放新的函数实体地址。

我们假设z()函数在Point虚函数表中的索引为4,回到最初的问题——要如何来保证在执行期调用的是正确的z()实体?其中微妙在于,编译将做一个小小的转换:

ptr->z();
//被编译器转化为:
(*ptr->vptr[4])(ptr);

这个转换保证了调用到正确的实体,因为:

虽然我们不知道ptr所指的真正类型,但它可以通过vptr找到正确类型的虚函数——在整个继承体系中z()的地址总是被放在slot 4。

关于RTTI及类型信息

上面我谈到虚表中索引为0的地方存放的是类型信息,它主要用来支持RTTI:

执行期类型识别(Runtime Type Identification RTTI)

  1. RTTI 只支持多态类,也就是说没有定义虚函数是的类是不能进行 RTTI的。
  2. 对指针进行dynamic_cast失败会返回NULL ,而对引用的话,识别会抛出 bad_cast exception。
  3. typeid 可以返回const type_info&,用以获取类型信息。

1是因为RTTI的实现是通过vptr来获取存储在虚函数表中的type_info* ,事实上为非多态类提供RTTI,也没有多大意义。
2是因为指针可以被赋值为0,以表示 no object,但是引用不行。
3是因为虽然第一点指出RTTI只支持多态类,但typeid和type_info同样可用于内建类型及所有非多态类。与多态类的差别在于,非多态类的type_info对象是静态取得(所以不能叫“执行期类型识别”),而多态类的是在执行期获得。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值