Table of Contents
1.1 Nonstatic Member Functions
1. Member的各种调用方式
C++支持三种类型的member functions: static、nonstatic和virtual,每一种类型被调用的方式都不相同。
1.1 Nonstatic Member Functions
C++的设计准则之一就是:nonstatic member function至少必须和一般的nonmember function有相同的效率。下面是一个nonmember function的定义:
float magnitude3d(const Point3d* _this)
{
return sqrt( _this->_x * _this->_x +
_this->_y * _this->_y +
_this->_z * _this->_z );
}
乍见之下似乎nonmember function比较没有效率,它间接地经由参数取用坐标成员,而member function确是直接取用坐标成员。然而实际上member function被内化为nonmmeber的形式。下面就是转化步骤:
1>改写函数的signature以安插一个额外的参数到member function中,用以提供一个存取管道,使class object得以调用该函数。该额外参数被称为this指针:
// non-const nonstatic member的增长过程
Point3d Point3d::magnitude(Point3d* const this)
如果member function是const,则变成:
// const nonstatic member的增长过程
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 );
在函数内直接return创建的对象比先创建,再return有效率:
Point3d Point3d::normalize() const
{
Point3d normal:
normal._x = _x/2;
normal._y = _y/2;
normal._z = _z/2;
return normal;
}
// 直接建构“normal”值比较有效率
Point3d Point3d::normalize() const
{
return Point3d( _x/2, _y/2, _z/2 );
}
1.2 名称的特殊处理(Name Mangling)
class member的名称前面会被加上class名称,形成独一无二的命名。
class Bar { public: int ival; ... }
class Foo : public { public: int ival; ... }
// Foo 的内部描述
class Foo
{
public:
int ival_3Bar;
int ival_3Foo;
...
};
不管你要处理哪一个ival,通过“name mangling”,都可以绝对清楚地指出来。由于member functions可以被重载,所以需要更广泛的mangling手法,以提供绝对独一无二的名称。
class Point
{
public:
void x(float newX);
float x();
...
};
//内部描述
class Point
{
public:
void x__5PointFf(float newX);
float x__5PointFv();
};
两个实体如果拥有独一无二的name mangling,那么任何不正确的调用操作在链接时期就因无法决议(resolved)而失败。但是它只可以捕捉函数signature(函数名称+参数数目+参数类型)错误;如果“返回类型”声明错误,就没办法检查出来。
1.3 Virtual Member Functions
如果normalize()是一个virtual member function,那么以下的调用:
ptr->normalize();
将会被内部转化为:
(*ptr->vptr[1])(ptr);
其中:
1>vptr表示由编译器产生的指针,指向virtual table。事实上,其名称也会被“mangled”,因为在一个复杂的class派生体系中,可能存在有多个vptrs。
2>1是virtual table slot的索引值,关联到normalize()函数。
3>第二个ptr表示this指针。
使用class scope operator明确调用一个virtual funciton,其决议(resolved)方式会和nonstatic member function一样:
//明确地调用操作会压制虚拟机制
register float mag = Point3d::magnitude();
<==> register float mag = magnitude__7Point3dFv(this);
1.4 Static Member Functions
如果Point3d::normalize()是一个static member function,以下两个调用操作将被转换为一般的nonmember函数调用:
obj.normalize();
==> normalize__7Point3dSFv();
ptr->normalize();
==> normalize__7Point3dSFv();
在引入static member functions之前,C++语言要求所有的member functions都必须经由该class的object来调用。而实际上,只有当一个或多个nonstatic data members在member function中被直接存取时,才需要class object。Class object提供了this指针给这种形式的函数调用使用。这个this指针把“在member function中存取的nonstatic class members”绑定于“object内对应的members”之上。如果没有任何一个members被直接存取,事实上就不需要this指针,因此也就没有必要通过一个class object来调用一个member function。不过C++语言到当前为止并不能够识别这种情况。
如果class的设计者把static data member声明为nonpublic(这一直被视为是一种好的习惯),那么他就必须提供一个或多个member functions来存取该member。因此,虽然你可以不靠class object来存取一个static member,但其存取函数却得绑定于一个class object之上。
static member functions的主要特性就是没有this指针。以下的次要特性统统根源于其主要特性:
1>它不能直接存取其class中的nonstatic members;
2>它不能被声明为const、volatile或virtual;
3>它不需要经由class object才被调用——虽然大部分时候它是这样被调用的。
static member function由于缺乏this指针,因此差不多等同于nonmember function。
2. Virtual Member Functions
在C++中,多态(polymorphism)表示“以一个public base class的指针(或reference),寻址出一个derived class object”的意思。
在C++中,virtual functions可以在编译时期获知,此外,这一组地址是固定不变的,执行期不可能新增或替换它。由于程序执行时,表格的大小和内容都不会改变,所以其建构和存取皆可以由编译器完全掌握,不需要执行期的任何介入。
为了找到表格,每一个class object被安插上一个由编译器内部产生的指针,指向该表格。为了找到函数地址,每一个virtual function被指派一个表格索引值。
这些工作都由编译器完成。执行期要做的,只是在特定的virtual table slot中激活virtual function。
2.1 单继承下的Virtual Functions
单继承下,一个class只会有一个virtual table。每一个table内含其对应的class object中所有active virutal functions函数实体的地址。这些active virtual functions包括:
1>这个class所定义的函数实体。它会改写(overriding)一个可能存在的base class virtual function函数实体;
2>继承自base class的函数实体。这是在derived class决定不改写virtual function时才会出现的情况;
3>一个pure_virtual_called()函数实体,它既可以扮演pure virtual function的空间保卫者角色,也可以当做执行期异常处理函数(有时候会用到)。
每一个virtual function都被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的virtual function的关联。
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;
};
class Point2d : public Point
{
public:
Point2d(float x = 0.0, float y = 0.0)
: Point(x), _y(y) { }
~Point2d();
//改写base class virtual functions
Point2d& mult(float);
float y() const { return _y; }
protected:
float _y;
};
class Point3d : public Point2d
{
public:
Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
: Point2d(x, y), _z(z) { }
~Point3d();
//改写base class virtual functions
Point3d& mult(float);
float z() const { return _z; }
protected:
float _z;
};
对于class Point,virtual destructor被赋值slot 1,而mult()被赋值slot2。此例并没有mult()的函数定义,所以pure_virtual_called()的函数地址会被放在slot2中。如果该函数被意外地被调用,通常的操作是结束掉这个程序。
class Point2d继承自class Point,Point2d的virtual table在slot1中指出destructor,而在slot2中指出mult()(取代pure virtual function)。它自己的y()函数实体地址放在slot3,继承自Point的z()函数实体地址则放在slot4。
现在,如果我有这样的式子:
ptr->z();
那么如何有足够的知识在编译时期设定virtual function的调用呢?
1>一般而言,我并不知道ptr所指对象的真正类型。然而我知道,经由ptr可以存取到该对象的virtual table;
2>虽然我不知道哪一个z()函数实体会被调用,但我知道每一个z()函数地址都被放在slot4。
这些信息使得编译器可以将该调用转化为:
(*ptr->vptr[4])(ptr);
在这个转化中,vptr表示编译器所安插的指针,指向virtual table;4表示z()被赋值的slot编号(关联到Point体系的virtual table)。唯一一个在执行期才能知道的东西是:slot4所指的到底是哪一个z()函数实体?
2.2 多重继承下的Virtual Functions
在多重继承中支持virtual functions,其复杂度围绕在第二个及后继的base classes身上,以及“必须在执行期调整this指针”这一点。
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;
};
有三种情况,第二或后继的base class会影响对virtual functions的支持:
1>通过一个“指向第二个base class”的指针,调用derived class virtual function。
Base2* pbase2 = new Derived;
新的Derived对象的地址必须调整,以指向其Base2 subobject。编译时期会产生以下的代码:
// 转移以支持第二个base class
Derived* temp = new Derived;
Base2* pbase2 = temp ? temp + sizeof(Base1) : 0;
如果没有这样的调整,指针的任何“非多态运用”都将失败:
// 即使pbase2被指定一个Derived对象,这也应该没有问题
pbase2->data_Base2;
当要删除pbase2所指的对象时:
// 必须首先调用正确的virtual destructor函数实体
// 然后施行delete运算符
// pbase2 可能需要调整,以指出完整对象的起始点
delete pbase2;
指针必须被再一次调整,以求再一次指向Derived对象的起始处(推测它还指向Derived对象)。然而上述的offset加法却不能够在编译时期直接设定,因为pbase2所指的真正对象只有在执行期才能确定。
一般规则是,经由指向“第二或后继的base class”的指针(或reference)来调用derived class virtual function,该调用操作所连带的“必要的this指针调整”操作,必须在执行期完成。
在多重继承之下,一个derived class内含n-1个额外的virtual tables,n表示其上一层base classes的数目(因此,单一继承将不会有额外的virtual tables)。对于本例的Derived而言,会有两个virtual tables被编译器产生出来:
(1)一个主要实体,与Base1(最左端base class)共享;
(2)一个次要实体,与Base2(第二个base class)有关。
针对每一个virtual tables,Derived对象中有对应的vptr。vptrs将在constructors中被设立初值。
用以支持“一个class拥有多个virtual tables”的传统方法是,将每一个tables以外部对象的形式产生出来,并给与独一无二的名称。例如,Derived所关联的两个tables可能有这样的名称:
vtbl__Derived; //主要表格
vtbl__Base2__Derived; //次要表格
于是当你将一个Derived对象地址指定给一个Base1指针或Derived指针时,被处理的virtual table是主要表格vtbl__Derived。而当你将一个Derived对象地址指定给一个Base2指针时,被处理的virtual table是次要表格vtbl__Base2__Derived。
2>通过一个“指向derived class”的指针,调用第二个base class中一个继承而来的virtual function。在此情况下,derived class指针必须再次调整,以指向第二个base subobject。
Derived* pder = new Derived;
// 调用Base2::mumble()
// pder 必须被向前调整sizeof(Base1)个bytes
pder->mumber();
3>允许一个virtual function的返回值类型有所变化,可能是base type,也可能是publicly derived type。
本例的Derived::clone()传回一个Derived class指针,默默地改写了它的两个base class函数实体。
Base* pb1 = new Derived;
//调用Derived* Derived::clone()
//返回值必须被调整,以指向Base2 subobject
Base2* pb2 = pb1->clone();
当进行pb1->clone()时,pb1会被调整指向Derived对象的起始地址,于是clone()的Derived版会被调用;它会传回一个指针,指向一个新的Derived对象;该对象的地址在被指定给pb2之前,必须先经过调整,以指向Base2 subobject。
2.3 虚拟继承下的Virtual Functions
class Point2d
{
public:
Point2d(float = 0.0, float = 0.0);
virtual ~Point2d();
virtual void mumble();
virtual float z();
protected:
float _x, _y;
};
class Point3d : public virtual Point2d
{
public:
Point3d(float = 0.0, float = 0.0, float = 0.0);
~Point3d();
float z();
protected:
float _z;
};
虽然Point3d有唯一一个(同时也是最左边的)base class,也就是Point2d,但Point3d和Point2d的起始部分并不像“非虚拟的单一继承”情况那样一致。由于Point2d和Point3d的对象不再相符,两者之间的转换也就需要调整this指针。
当一个virtual base class从另一个virtual base class派生而来,并且两者都支持virtual functions和nonstatic data members时,编译器对于virtual base class的支持简直就像进了迷宫一样。因此,最好不要在一个virtual base class中声明nonstatic data members。
3. 其他
取一个nonstatic member function的地址,如果该函数是nonvirtual,则得到的结果是它在内存中真正的地址。然而这个值也是不完全的,它也需要被绑定于某个class object的地址上,才能够通过它调用该函数。所有的nonstatic member functions都需要对象的地址(以参数this指出)。
如果取一个static member function的地址,获得的将是其在内存中的位置,也就是其地址。由于static member function没有this指针,所以其地址的类型并不是一个“指向class member function的指针”,而是一个“nonmember函数指针”。