Inside the C++ Object Model学习笔记[Chap4.0-4.2]

来源:互联网 发布:设置桌面软件 编辑:程序博客网 时间:2024/05/16 15:26

4 函数语意学

4.0 引言

Point3D的指针和对象:

Point3D opd;

Point3D *ppd;

调用其成员函数时:

opd.normalize();

ppd->normalize();

将有什么动作发生及其区别?显然根据成员函数的三种不同方式:staticnonstaticvirtual,每一种类型被调用的方式各不相同。如下几节将详细描述各种不同的调用方式。

4.1 成员函数的各种调用方式

4.1.1 非静态成员函数【多重继承的问题呢?】

       C++的设计准则之一:非静态成员函数至少必须和非成员函数具有相同的效率。因此实际上对于非静态成员函数来说,在内部其实是被转化未非成员的形式,如:

float Point3D::magnite() {

       return sqrt( _x * _x + _y * _y + _z * _z );

}

转化步骤分为三步:

1. 改写函数原型,安插一个额外的参数this指针到成员函数中,用以提供一个存取管道:

Point3D Point3D::magnite( Point3D *const this ) { …… }

如果是const成员函数,则相应为:

Point3D Point3D::magnite( const Point3D *const this ) { …… }

2. 将每一个“对成员数据对象的存取操作”改为经由this指针来存取:

Point3D Point3D::magnite( Point3D *const this ) {

       return sprt( this->_x * this->_x +

                       this->_y * this->_y +

                       this->_z * this->_z );

}

3. 将成员函数重写为一个外部函数,对函数名称做mangling处理,使之在程序中为独一无二的名称:

extern magnite_7Point3DFv( register Point3D *const this );

这样将函数转换完毕,对函数的调用操作:

opd.magnite();   转变为:magnite_7Point3DFv( &opd );

ppd->magnite();  转变为:magnite_7Point3DFv( ppd );

4.1.2 虚拟成员函数

首先说明对于非多态行为,使用虚拟成员函数没有一点好处,反而会付出空间和存取时间的代价,因此在不涉及多态行为,绝不要使用虚函数。

在调用虚函数时,会转化成指向虚表vtbl的函数指针,如:

ppd->magnite();  转变为:( * ppd->vptr[ index ]) ( ppd );

其中,vptr是编译器产生的指针,指向类的vtbl,而index则是对应于magnite()在类中位置的索引值,后一个ppd表示this指针。

更详细的描述见4.2节。

4.1.3 静态成员函数

对于静态成员函数,它只能操作类中的static数据成员,不管采用指针(引用)或者对象的调用操作,都将被转换为一般的非成员函数调用,与类对象无关,如:

opd.normalize();

ppd->normalize();

都转变为:

magnite_7Point3DFv( );

静态成员函数的特性有:

1. 它没有this指针;

2. 不能直接存取类中的非静态数据成员;

3. 不能被声明为constvolatilevirtual

4. 不需要经由类对象调用。

如果去一个静态成员函数的地址,获得的将是其在内存中的位置。由于它没有this指针,所以其地址类型并不是一个指向类成员函数的指针,而是一个非成员函数指针,如:

&Point3D::static_fun();

得到的数值,其类型为:

unsigned int ( * ) ();

而不是:

unsigned int ( Point3D::* ) ();       

4.2 虚拟成员函数

虚拟成员函数的一般实现模型:每一个class有一个vtbl,内含该class之中有作用的虚函数地址,然后每个类对象有一个vptr,指向相应的vtbl。本节分别从单一继承,多重继承与虚拟继承上深究模型的实现。

4.2.1 单一继承下的虚函数

C++中,多态表示“以一个公共基类的指针或引用,寻址出一个派生类对象”的意思。识别一个类是否支持多态,唯一适当的办法就是看它是否有任何虚函数,只要类拥有一个虚函数,它就需要额外的执行期信息。

在实现上,可以在每一个多态的类对象身上增加两个成员用以准备函数地址:

1. 一个字符串或者数字,表示类的类型;

2. 一个指针,指向某表格,表格中带有程序的虚函数的执行期地址。

那么表格中的虚函数地址是如何构造的呢?在C++中,虚函数可以在编译时期获知,此外,这一组地址是固定不变的,执行期不可能新增或替换。由于程序执行时,表格的大小和内容都不会改变,所以其建构和存取都可以由编译器完全掌握,不需要执行期的任何介入。

现在将函数地址准备完毕,但如何找到这些地址呢?分如下两步:

1. 为了找到表格,每一个类对象被安插一个由编译器内部产生的指针,指向该表格;

2. 为了找到函数地址,每一个虚函数被指派一个表格索引值。

一个类中只有一个虚表,每一个表格内含其对应的类对象中所有的活动虚函数实体的地址,这些虚函数包括:

1. 类定义的函数实体,它会改写一个可能存在的基类虚函数实体;

2. 继承自基类的函数实体,在派生类中决定不改写虚函数时才会出现这种情况;

3. 一个pure_virtual_called()函数实体,既可以扮演纯虚函数的空间保卫者角色,也可以当作执行期异常处理函数。

关于单一继承的布局图示见书P157――图4.1。对于函数调用,如:ptr->z();尽管不知道ptr所指对象的真正类型,但是经由ptr可以存取到该对象的vtbl,同时尽管不知道哪一个z()函数实体会被调用,但知道每一个z()函数的地址在什么地方,因此编译器会内部转化为(伪码):( * ptr->vptr[ index ]) ( ptr );

对于单一继承,虚函数机制的行为十分良好,但是对于多重继承与虚拟继承,则困难的多。

4.2.2 【未】多重继承下的虚函数

多重继承下支持虚函数,复杂性主要围绕在第二个及后继的基类身上,以及“必须在执行期调整this指针”这一点上。

(需要仔细阅读理解)

4.2.3 虚拟继承下的虚函数

作者并没有多做解释,只是提出忠告:非常复杂,同时不要在一个虚基类中声明非静态数据成员。