***非静态成员函数(Nonstatic Member Functions)***
C++的设计准则之一就是:nonstatic member function至少必须和一般的nonmember function有相同的效率。也就是说,如果我们要在以下两个函数之间作选择:
float
magnitude3d(
const
Point3d
*
this
)
...
{ ... }
float
Point3d::magnitude3d()
const
...
{ ... }
那么选择member function不应该带来什么额外负担。因为编译器内部已将“member函数实体”转化为对等的“nonmember函数实体”。下面是magnitude()的一个nonmember定义:
float Pointer3d::magnitude()
const
...
{
return sqrt(_x*_x + _y*_y + _z*_z);
}
//
内部转化为
float
magnitude_7Point3dFv(
const
Point3d
*
this
)
//
已对函数名称进行“mangling”处理
...
{
return sqrt(this->_x*this->_x + this->_y*this->_y + this->_z*this->_z);
}
现在,对该函数的每一个调用操作也都必须转换:
obj.magnitude();
// 转换为
magnitude_7Point3dFv( &
obj);
对于class中的memeber,只需在member的名称中加上class名称,即可形成独一无二的命名。但由于member function可以被重载化,所以需要更广泛的mangling手法,以提供绝对独一无二的名称。其中一种做法就是将它们的参数链表中各参数的类型也编码进去。
class Point ... {
public:
void x(float newX);
float x();
...
} ;
// 内部转化为
class Point ...
{
void x_5PointFf(float newX); // F表示function,f表示其第一个参数类型是float
float x_5PointFv(); // v表示其没有参数
} ;
上述的mangling手法可在链接时期检查出任何不正确的调用操作,但由于编码时未考虑返回类型,故如果返回类型声明错误,就无法检查出来。
***虚拟成员函数(Virtual Member Functions)***
对于那些不支持多态的对象,经由一个class object调用一个virtual function,这种操作应该总是被编译器像对待一般的nonstatic member function一样地加以决议:
// Point3d obj
obj.normalize();
// 不会转化为
( * obj.vptr[ 1
])(
&
obj);
// 而会被转化未
normalize_7Point3dFv( & obj);
***静态成员函数(Static Member Functions)***
在引入static member functions之前,C++要求所有的member functions都必须经由该class的object来调用。而实际上,如果没有任何一个nonstatic data members被直接存取,事实上就没有必要通过一个class object来调用一个member function。
这样一来便产生了一个矛盾:一方面,将static data member声明为nonpublic是一种好的习惯,但这也要求其必须提供一个或多个member functions来存取该member;另一方面,虽然你可以不靠class object来存取一个static member,但其存取函数却得绑定于class object之上。static member functions正是在这种情形下应运而生的。
编译器的开发者针对static member functions,分别从编译层面和语言层面对其进行了支持:
(1)编译层面:当class设计者希望支持“没有class object存在”的情况时,可把0强制转型为一个class指针,因而提供出一个this指针实体:
// 函数调用的内部转换
object_count((Point3d * ) 0 );
(2)语言层面:static member function的最大特点是没有this指针,如果取一个static member function的地址,获得的将是其在内存中的位置,其地址类型并不是一个“指向class member function的指针”,而是一个“nonmember函数指针”:
unsigned int Point3d::object_count() ... { return _object_count; }
& Point3d::object_count();
// 会得到一个地址,其类型不是
unsigned int (Point3d:: * )();
// 而是
unsigned int ( * )();
static member function经常被用作回调(callback)函数。
***虚拟成员函数(Virtual Member Functions)***
对于像ptr->z()的调用操作将需要ptr在执行期的某些相关信息,为了使得其能在执行期顺利高效地找到并调用z()的适当实体,我们考虑往对象中添加一些额外信息。
(1)一个字符串或数字,表示class的类型;
(2)一个指针,指向某表格,表格中带有程序的virtual functions的执行期地址;
在C++中,virtual functions可在编译时期获知,由于程序执行时,表格的大小和内容都不会改变,所以该表格的建构和存取皆可由编译器完全掌握,不需要执行期的任何介入。
(3)为了找到表格,每一个class object被安插上一个由编译器内部产生的指针,指向该表格;
(4)为了找到函数地址,每一个virtual function被指派一个表格索引值。
一个class只会有一个virtual table,其中内含其对应的class object中所有active virtual functions函数实体的地址,具体包括:(a)这个class所定义的函数实体
它会改写一个可能存在的base class virtual function函数实体。若base class中不存在相应的函数,则会在derived class的virtual table增加相应的slot。
(b)继承自base class的函数实体
这是在derived class决定不改写virtual function时才会出现的情况。具体来说,base class中的函数实体的地址会被拷贝到derived class的virtual table相对应的slot之中。
(c)pure_virtual_called函数实体
对于这样的式子:
ptr -> z();
运用了上述手法后,虽然我不知道哪一个z()函数实体会被调用,但却知道每一个z()函数都被放在slot 4(这里假设base class中z()是第四个声明的virtual function)。
// 内部转化为
( * ptr -> vptr[ 4 ])(ptr);
***多重继承下的Virtual Functions***
在多重继承中支持virtual functions,其复杂度围绕在第二个及后继的base classes身上,以及“必须在执行期调整this指针”这一点。
多重继承到来的问题:
(1)经由指向“第二或后继之base class”的指针(或reference)来调用derived class virtual function,该调用操作连带的“必要的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;
} ; 对于下面一行:
Base2 * pbase2 = new Derived;
会被内部转化为:
// 转移以支持第二个base class
Derived * temp = new Derived;
Base2 * pbase2 = temp ? temp + sizeof (Base1) :
0
;
如果没有这样的调整,指针的任何“非多态运用”都将失败:
pbase2->data_Base2;
当程序员要删除pbase2所指的对象时:
// 必须调用正确的virtual destructor函数实体
// pbase2需要调整,以指出完整对象的起始点
delete pbase2;
指针必须被再一次调整,以求再一次指向Derived对象的起始处。然而上述的offset加法却不能够在编译时期直接设定,因为pbase2所指的真正对象只有在执行期才能确定。
自此,我们明白了在多重继承下所面临的独特问题:经由指向“第二或后继之base class”的指针(或reference)来调用derived class virtual function,该调用操作所连带的“必要的this指针调整”操作,必须在执行期完成。有两种方法来解决这个问题:
(a)将virtual table加大,每一个virtual table slot不再只是一个指针,而是一个聚合体,内含可能的offset以及地址。这样一来,virtual function的调用操作发生改变:
( * pbase2 -> vptr[ 1 ])(pbase2);
// 改变为
( * pbase2 -> vptr[ 1 ].faddr)(pbase2 + pbase2 -> vptr[ 1 ].offset);
这个做法的缺点是,它相当于连带处罚了所有的virtual function调用操作,不管它们是否需要offset的调整。
(b)利用所谓的thunk(一小段assembly码),其做了以下两方面工作:(1)以适当的offset值调整this指针;(2)跳到virtual function去。
pbase2_dtor_thunk:
this += sizeof (base1);
Derived:: ~ Derived( this );
Thunk技术允许virtual table slot继续内含一个简单的指针,slot中的地址可以直接指向virtual function,也可以指向一个相关的thunk。于是,对于那些不需要调整this指针的virtual function而言,也就不需要承载效率上的额外负担。
(2)由于两种不同的可能:(a)经由derived class(或第一个base class)调用;(b)经由第二个(或其后继)base class调用,同一函数在virtual table中可能需要多笔对应的slot;
Base1 * pbase1 = new Derived;
Base2 * pbase2 = new Derived;
delete pbase1;
delete pbase2;
虽然两个delete操作导致相同的Derived destructor,但它们需要两个不同的virtual table slots:
(a)pbase1不需要调整this指针,其virtual table slot需放置真正的destructor地址
(b)pbase2需要调整this指针,其virtual table slot需要相关的thunk地址
具体的解决方法是:
在多重继承下,一个derived class内含n-1个额外的virtual tables,n表示其上一层base classes的数目。按此手法,Derived将内含以下两个tables:vtbl_Derived和vtbl_Base2_Derived。(3)允许一个virtual function的返回值类型有所变化,可能是base type,可能是publicly derived type,这一点可以通过Derived::clone()函数实体来说明。
Base2 * pb1 = new Derived;
// 调用Derived::clone()
// 返回值必须被调整,以指向Base2 subobject
Base2 * pb2 = pb1 -> clone();
当运行pb1->clone()时,pb1会被调整指向Derived对象的起始地址,于是clone()的Derived版会被调用:它会传回一个指针,指向一个新的Derived对象;该对象的地址在被指定给pb2之前,必须先经过调整,以指向Base2 subobject。
当函数被认为“足够小”的时候,Sun编译器会提供一个所谓的“split functions”技术:以相同算法产生出两个函数,其中第二个在返回之前,为指针加上必要的offset,于是无论通过Base1指针或Derived指针调用函数,都不需要调整返回值;而通过Base2指针所调用的,是另一个函数。
***虚拟继承下的Virtual Functions***
其内部机制实在太过诡异迷离,故在此略过。唯一的建议是:不要在一个virtual base class中声明nonstatic data members。
***函数的效能***
由于nonmember、static member和nonstatic member函数都被转化为完全相同的形式,故三者的效率安全相同。virtual member的效率明显低于前三者,其原因有两个方面:(a)构造函数中对vptr的设定操作;(b)偏移差值模型。
***指向Member Function的指针***
取一个nonstatic member function的地址,如果该函数是nonvirtual,则得到的结果是它在内存中真正的地址。
我们可以这样定义并初始化该指针:
double (Point:: * coord)() = & Point::x;
想调用它,可以这么做:
(origin.*coord)();
(ptr->*coord)();
“指向Virtual Member Functions”之指针将会带来新的问题,请注意下面的程序片段:
float (Point:: * pmf)() = & Point::z;
Point * ptr = new Point3d;
其中,pmf是一个指向member function的指针,被设值为Point::z()(一个virtual function)的地址,ptr则被指向一个Point3d对象。
如果我们直接经由ptr调用z():
ptr -> z(); // 调用的是Point3d::z()
但如果我们经由pmf间接调用z():
(ptr ->* pmf)(); // 仍然调用的是Point3d::z()
也就是说,虚拟机制仍然能够在使用“指向member function之指针”的情况下运行,但问题是如何实现呢?
对一个nonstatic member function取其地址,将获得该函数在内存中的地址;而对一个virtual member function取其地址,所能获得的只是virtual function在其相关之virtual table中的索引值。因此通过pmf来调用z(),会被内部转化为以下形式:
(*ptr->vptr[(int)pmf])(ptr); 但是我们如何来判断传给pmf的函数指针指向的是内存地址还是virtual table中的索引值呢?例如以下两个函数都可指定给pmf:
// 二者都可以指定给pmf
float Point::x() ... { return _x; } // nonvirtual函数,代表内存地址
float Point::z() ... { return 0; } // virtual函数,代表virtual table中的索引值
cfront 2.0是通过判断该值的大小进行判断的(这种实现技巧必须假设继承体系中最多只有128个virtual functions)。
为了让指向member functions的指针也能够支持多重继承和虚拟继承,Stroustrup设计了下面一个结构体:
// 用以支持在多重继承之下指向member functions的指针
struct _mptr ... {
int delta;
int index;
union ...{
ptrtofunc faddr;
int v_offset;
};
} ;
其中,index表示virtual table索引,faddr表示nonvirtual member function地址(当index不指向virtual table时,被设为-1)。
在该模型之下,以下调用操作会被转化为:
(ptr ->* pmf)();
// 内部转化为
(pmf.index < 0 )
? ( * pmf.faddr)(ptr) // nonvirtual invocation
: ( * ptr -> vptr[pmf.index](ptr) // virtual invocation
对于如下的函数调用:
(pA. * pmf)(pB); // pA、pB均是Point3d对象
会被转化成:
pmf.iindex < 0
? ( * pmf.faddr)( & pA + pmf.delta, pB)
: ( * pA._vptr_Point3d[pmf.index].faddr)( & pA + pA._vptr_Point3d[pmf.index] + delta, pB);
***Inline Functions***
在inline扩展期间,每一个形式参数都会被对应的实际参数取代。但是需要注意的是,这种取代并不是简单的一一取代(因为这将导致对于实际参数的多次求值操作),而通常都需要引入临时性对象。换句话说,如果实际参数是一个常量表达式,我们可以在替换之前先完成其求值操作;后继的inline替换,就可以把常量直接绑上去。
举个例子,假设我们有以下简单的inline函数:
inline int min( int i, int j)
... {
return i < j ? i : j;
}
对于以下三个inline函数调用:
minval = min(val1,val2);
minval = min( 1024 , 2048 );
minval = min(foo(),bar() + 1 );
会分别被扩展为:
minval = val1 < val2 ? val1 : val2; // 参数直接代换
minval = 1024 ; // 代换之后,直接使用常量
int t1;
int t2;
minval = (t1 = foo()), (t2 = bar() + 1 ),t1 < t2 ? t1 : t2; // 有副作用,所以导入临时对象
inline函数中的局部变量,也会导致大量临时性对象的产生。
inline int min( int i, int j)
... {
int minval = i < j ? i : j;
return minval;
}
则以下表达式:
minval = min(val1, val2);
将被转化为:
int _min_lv_minval;
minval = (_min_lv_minval = val1 < val2 ? val1 : val2),_min_lv_minval; 总而言之,inline函数中的局部变量,再加上有副作用的参数,可能会导致大量临时性对象的产生。特别是如果它以单一表达式被扩展多次的话。新的Derived对象的地址必须调整,以指向其Base2 subobject。