“The Semantics of Function”,本篇的架构很简单,说的是member functions在nonstatic,static和virtual三种状态下的调用方式。
首先来一个开胃菜:假设类Point3d有data members x, y, z,有member function如下,
float
Point3d::magnitude() const
{
return sqrt(x*x + y*y + z*z);
}
问你能从这个函数看出什么?
当然,我们无法确定它是virtual还是nonvirtual,但可以确定它一定不是static,因为,
- static function无法直接存取nonstatic数据;
- static function不能声明为const。
(1)Nonstatic member function的调用方式
C++的设计准则之一就是:nonstatic member function至少必须和一般的nonmember function有相同的效率。
也就是说,如果有如下两个函数,
float magnitude3d ( const Point3d *_this ) { ... };
float Point3d :: magnitude3d const () { ... };
选择member function不会带来任何额外负担。为什么呢?
因为编译器内部已经将“member function实例”转换成了“nonmember function实例”,member function转换过程如下:
- 1 改写函数signature(也就是改写函数原型);
安插一个额外参数(隐式的this指针)到member function中,它的作用是使class object可以通过它来存取数据。比如对non-const成员与const 成员的扩张如下,
// non-const nonstatic member function augmentation
Point3d
Point3d::magnitude( Point3d *const this )
//const nonstatic member augmentation
Point3d
Point3d::magnitude( const Point3d *const this )
ps:
还记得const放在函数声明后面是什么意思吗?
it means that function is not allowed to change any class members(except ones that are marked "mutable").
const放在前面呢?
const before an argument in a function definition means the same as const for a variable, the value is not allowed to change.
具体的请看:[http://stackoverflow.com/questions/3141087/what-is-meant-with-const-at-end-of-function-declaration]
既然说了那么多题外话,那也就提一下突破const限制的mutable C++ 关键字吧!
mutable与const相反,是“可变的”,它永远处于一个可变状态,即使身在一个const function中。
- 2 对nonstatic data member的存取改为经由this指针来操作
{
return sqrt( this->x * this->x + this->y * this->y + this->z * this->z );
}
- 3 通过“name mangling”,将member function重写成一个外部函数
name mangling的作用,是将member的函数名和参数编码到一起,形成一个独一无二的命名。
于是,函数名称变成了
extern magnitude__7Point3dFv ( register Point3d *const this );
那么,
obj.magnitude();
ptr->magnitude();
经过改写之后,变成了
magnitude__7Point3dFv( &obj );
magnitude__7Point3dFv( ptr );
可见,没有增加任何额外负担。
(2)static member function的调用方式
有人会问,为什么会需要一个static的成员函数,我们来分析一下原因:
我们从上面得知,所有对nonstatic data member的存取都需要借助object来进行,this指针把“member function中要存取的nonstatic data member”和“object内对应的members”绑定在一起。然而,如果存取static data member,因为它在class之外,所以就不需要this指针,也不需要绑定类内数据成员。但我们需要对各种类型的data members的存取方式做出统一,这样一来,就需要一个member function,使它可以绑定在class object上(但的确不需要this指针)。
由于static member function没有this指针的特点,随之而来,它衍生出了几条其它特征:
- 不能存取class中的nonstatic members;
- 不能被声明为const, volatile或virtual;
- 不需要经过class object才被调用——虽然大部分时候需要通过object。
那我们就来看,编译器如何转换一个static member function:
unsigned int
Point3d::
object_count ()
{
return _object_count;
}
会被转化成:
unsigned int
object_count_5Point3dSFv ()
{
return _object_count_5Point3d;
}
这里,SFv表示static member function, void argument list。与nonstatic member function的转化比起来,可以清晰的看到,this指针已经不再存在,不需要object便可以直接访问static data member。
所以,当我们写下如下代码时,
& Point3d::object_count();
我们得到的是nonmember function的函数指针,类型是,
unsigned int (*) ();
而不是得到一个指向class member function的指针,它的指针是 unsigned int ( Point3d::* ) ();类型的。
所以,正因为static member function缺乏this指针,因此差不多等同于nonmember function;而这个意想不到好处是,可以成为一个callback函数,使得我们可以将C++和C-based C Window系统结合,也使得更成功的用在线程函数上。
(3)virtual member function的调用方式
在这一节,我们分单一继承、多重继承、虚拟继承三个方面来探究这个模型。
1 单一继承
Point3d obj;
Point3d *ptr = &obj;
ptr->z();
看这个代码,z是virtual function;对于ptr->z()的多态特性(也就是virtual function机制),我们知道它是执行期的特征,我们必须要解决的两个问题是:
- a. ptr是个什么对象?
- b. 从哪里获取z()?
要回答这两个问题,我们必须添加额外的执行期信息来确定,并且,如果一个class拥有virtual function,那么我们就需要这个额外信息,如果没有,那么在编译时期,我们就可以办妥一切事情,执行时也就不需要额外的附加信息了。
首先来看第一个问题,ptr是个什么对象?
很简单,它的信息我们在执行期才可以得到。
第二个问题,编译时期如何找到z()?
要回答这个问题,我们先来看一段代码:
class Point {
public:
virtual ~Point (); // slot 1
virtual Point& mult( float ) = 0; // pure virtual function, slot 2
// ...
float x() const { return x; }
virtual float y() const { return 0; } //slot 3
virtual float z() const { return 0; } //slot 4
protected:
Point( float x = 0.0 );
float _x;
}
我们已经知道,virtual functions存放在virtual function中,由class object中的vptr指向。安插vptr和放置virtual functions,当然是编译时期要做的事;执行期,便是要激活virtual table slot中的virtual function。我们看下图:
我们会发现,在继承过程中,需函数对应slot的索引值不会改变,上述代码中4个slot的索引已经在程序中标出,所以,我们在编译时期可以清楚的知道,要调用的z()函数都放在slot 4中,因此编译器可将调用转化为:
( *ptr->vptr[ 4 ] )( ptr );
这样,在单一继承下,virtual function机制便可以很好的运行起来了。
2 多重继承
这个有点麻烦了,因为子类必须维护多个virtual table,看下图:
这种情况下,编译器将负担给了“第二个或后继的base class”,通过在执行期调整this指针,来调用子类中的虚函数。
对如上继承关系,编译器将支持virtual function的困难度放在了Base2 subonject身上,它需要解决3个问题:
- virtual destructor;
Base1 *pbase1 = new Derived();
Base2 *pbase2 = new Derived();
// ...
delete pbase1;
delete pbase2;
如上代码中,对于pbase1的delete,由于Base1是第一个base class,所以不需要调整this指针;而pbase2需要调整。这里面涉及了trunk技术用来以适当的offset调整this指针完成virtual function的跳跃,具体见书上p163;
- 被继承下来的Base2::mumble()
- clone();(图中~clone应改为clone)
以上两种情况同样需要指针的调整,不细说了;
3 虚拟继承
我放个图在这里,用来大致描述一下virtual function调用的机制。由于译者对这里也有疑惑,所以想要深究的同学应该去查阅其他资料或论文。