zhouqijie

成员函数的各种调用方式

原始的“C with class”只支持non-static成员函数。虚函数诗在80年代中期加入的,static成员函数是最后引入的一种函数类型。

Non-Static 成员函数

  1. 改写函数的函数原型(signature)以安插一个额外的参数(即this指针)到member函数中,以提供一个存取管道,使得类对象得以将此函数调用。
  2. 将每一个“对non-static 数据成员的存取操作”改为经由this指针来存取。
  3. 将member函数重写为一个外部函数。将函数名称经过“mangling”处理,使它独一无二。

一般而言,member的名称前面会加上class名称,形成独一无二的命名。

由于成员函数可以重载,所以需要更广泛的mangling手法,所以可以再把参数类型也编码进去。

虚拟成员函数

ptr->normalize();
//如果normalize是一个虚函数,会被转化为  
(*ptr->vptr[1])(ptr);

CRE/GPT:虚函数理论上可以声明为inline。但是在实际使用中,当通过基类指针或引用来调用时(CRE:或者说表现出多态性时),通常不会内联。

Static 成员函数

主要的特性就是它没有this指针。所以不能存取类中的non-static成员。也不能被声明为const、virtual、volatile。它不需要经由类对象被调用–虽然大部分时候它是这样被调用的。

如果取一个static成员函数的地址,获得的将是其在内存中的位置,也就是其地址。由于static成员函数没有this指针,所以其地址的类型并不是一个“指向成员函数的指针”,而是一个“non-member函数指针”。





虚成员函数

不是所有的对象都需要执行期额外信息,只有支持某种形式的“执行期多态”时才需要这份信息。识别一个class是否支持多态,唯一方法就是看看它是否有任何虚函数。只要class有一个虚函数,他就需要额外的执行期信息。

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

每一个虚函数表内含其对应的类对象中所有active虚函数实例的地址。这些active虚函数包括:

  1. 这一class所定义的函数实例。它会重写一个可能存在的基类虚函数实例。
  2. 继承自基类的函数实例。这是派生类决定不改写虚函数时才会出现的情况。
  3. 一个pure_virtual_called()函数实例,他可以扮演pure virtual函数的空间保卫者角色,也可以当作执行期异常处理函数。

每个virtual函数都被指派一个固定的索引值。

CRE:经由对象调用虚函数,只需要vtable指针以及函数在表中的slot,就可以在编译时期设定虚函数调用。

多重继承下的虚函数

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

Base2* ptrBase2 = new Derived();

//新的Derived对象的地址必须调整以指向其Base2的subobject。编译时期会产生以下的代码:  
// Derived* tmp = new Derived();
// Base2* ptrBase2 = temp ? temp + sizeof(Base2) : 0;
//如果没有这样的调整,指针的任何非多态运用都将失败

//当程序员要删除ptrBase2所指对象时:  
delete ptrBase2;
//指针必须被再一次调整,以求再一次指向Derived对象的起始处。  
//然而上述的offset加法却不能在编译时期直接设定,因为ptrBase2所指的真正对象只有在执行期才能确定。    

一般规则是,经由指向“第二或后继基类”的指针/引用来调用派生类虚函数。

CRE:注意,多重继承一般会有多个vptr。





函数的效能

inline函数在优化过后,效率会提升非常多。(inline函数不只能够节省一般函数调用所带来的额外负担,也提供了程序优化的额外机会)

非成员函数、静态成员函数、非静态成员函数都被转化为相同的形式。所以三者的效率完全相同。

CRE:从测试结果来看,虚函数调用代价相对以上三种较高,多重继承的代价更高,虚拟继承的代价还要更高。





指向成员函数的指针

取一个non-static成员函数的地址,如果该函数时non-virtual函数,得到的结果是它在内存中的真正地址。然而这个值还需要绑定某个对象才能调用。

//声明:
float (Vec3:: * memberFunc)() = &Vec3::GetMagnitude;
//调用方式:
float result = (vecPtr ->*memberFunc)();
float result = (vec.*memberFunc)();

使用一个成员函数指针,如果并不用于虚函数、多重继承、虚基类等情况的话,并不会比普通函数指针成本更高。虚函数和多重继承等情况会使成员函数指针复杂化。





inline函数

关键字inline只是一项请求。如果这项请求被接受,编译器必须认为它可以用一个表达式合理地将这个函数扩展开。(在某个层次上其执行成本比一般的函数调用以及返回机制所带来的负荷低)

一般而言,处理一个inline函数有两个阶段:

  1. 分析函数定义,以决定函数的本质inline能力(instrinsic inline ability)
  2. 真正的inline函数展开操作是在调用那一点上。这会带来参数的求值(evaluation)操作以及临时对象的管理。

形式参数

在inline函数展开期间,每个形式参数都会被对应的实际参数取代。

有些实际参数带有副作用,比如可能会导致多次求值。一般而言面对这种情况,通常都会引入临时对象。

局部变量

一般而言,inline函数中的每一个局部变量都必须放在函数调用的一个封闭区段中,拥有一个独一无二的名称。

CRE:可能会使用mangling,或者单独的代码块包裹。

补充

  1. inline函数对于封装提供了一种必要的支持,可以有效地存取封装于类中的私有数据。它同时也是前置处理宏(#define)的安全替代品。
  2. inline中再有inline,可能会使一个表面上看起来平凡的inline却因连锁复杂度而无法展开。

(END)