若是有一个Point3d的指针和对象:ios
Point3d obj; Point3d *ptr = &obj;
当这样作:程序员
obj.normalize(); ptr->normalize();
时,会发生什么事?其中的Point3d::normalize()定义以下:express
Point3d Point3d::normalize() const { register float mag = magnitude(); Point3d normal; normal._x = _x / mag; normal._y = _y / mag; normal._z = _z / mag; return normal; }
而其中的Point3d::magnitude()又定义以下:安全
float Point3d::magnitude() const { return sqrt( _x * _x + _y * _y + _z * _z ); }
答案是不明确的。C++支持三种类型的member functions:static、nonstatic和virtual,每一ide
种类型被调用的方式都不一样。不过咱们虽不能肯定normalize()和magnitude()两函数是否为函数
virtual或nonvirtual,但能够肯定它必定不是static,缘由有二:(1)它直接存取nonstatic数据,(2)工具
它被声明为const。而static member functions不可能作到这两点。布局
C++的设计准则之一就是:nonstatic member function至少必须和通常的nonmember测试
function有相同的效率。也就是说,若是咱们要在如下两个函数之间作选择:优化
float magnitude3d( const Point3d *_this ){ ... } float Point3d::magnitude3d() const { ... }
选择member function不该该带来什么额外负担。这是由于编译器内部已将”member 函数实
例“转换为对等的”nonmember函数实例“。
好比下面是magnitude()的一个nonmember定义:
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被内化为nonmember的
形式。下面是转化步骤:
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 );
而normalize()函数会被转化为下面的形式,其中假设已经声明有一个Point3d copy
constructor,而named returned value(NRV)的优化也已经实施:
// 如下描述”named return value函数“的内部转化 // 使用C++代码 void normalize_7Point3dFv( register const Point3d *const this, Point3d &_result ) { register float mag = this->magnitude(); // default constructor _result.Point3d::Point3d(); _result._x = this->_x / mag; _result._y = this->_y / mag; _result._z = this->_z / mag; return; }
一个有效率的作法是直接构建”normal“值,像这样:
Point3d Point3d::normalize() const { register float mag = magnitude(); return Point3d( _x / mag, _y / mag, _z / mag ); }
它会转化为如下的代码(再一次假设Point3d的copy constructor已经声明好了,而NRV
的优化也已实施):
// 如下描述内部转化 // 使用C++伪码 void normalize_7Point3dFv( register const Point3d *const this, Point3d & _result ) { register float mag = this->magnitude(); // _result用以取代返回值(return value) _result.Point3d::Point3d( this->_x / mag, this->_y / mag, this->_z / mag ); return; }
这能够节省default constructor初始化所引发的额外负担。
通常而言,member的名称前面会被加上class名称,造成独一无二的命令。例以下面的声
明:
class Bar { public: int ival; ... };
其中的ival有可能变成这样:
// member通过name-mangling以后的可能结果之一 ival_3Bar
为何编译器要这么作?清考虑这样的派生操做(derivation):
class Foo : public Bar { public: int val;... };
Foo对象内部结合了base class和derived class二者:
// C++伪码 // Foo的内部描述 class Foo { public: int ival_3Bar; int ival_3Foo; };
无论处理哪一个ival,经过”name mangling“,均可以绝对清楚地指出来。因为member
functions能够被重载化(overload),因此须要更普遍的mangling手法,以提供对独一无二的
名称。若是把:
class Point { public: void x( float newX ); float x(); ... };
转换为:
class Point { public: void x_5Point( float newX ); float x_5Point(); ... };
会致使两个被重载化(overloaded)的函数实例拥有相同的名称。为了让它们独一无二,惟
有再加上它们的参数链表(能够从函数原型中参考获得)。若是把参数类型也编码进取,就一
定能够制造出独一无二的结果,使咱们的两个x() 函数有良好的转换(若是声明extern ”C“,就会
压抑nonmember functions的”mangling“效果):
class Point { public: void x_5PointFf( float newX ); float x_5PointFv(); ... };
把参数和函数名称编码在一块儿,编译器因而在不一样的编译模块之间达成了一种优先形式的
类型检验。以下print函数被这样定义:
void print( const Point3d& ) { ... }
但意外地被这样声明和调用:
// 觉得是const Point3d& void print( const Point3d );
两个实例若是拥有独一无二的name mangling,那么任何不正确的调用操做在连接时期就
因没法决议(resolved)而失败。但若是是”返回类型“声明错误就没办法检查出来。
若是normalize()是一个virtual member function,那么如下的调用:
ptr->normalize();
将会被内部转化为:
( *ptr->vptr[ 1 ] )( ptr );
其中:
1)vptr表示由编译器产生的指针,指向virtual table。它被安插在每个“声明有(或继承
自),一个或多个virtual functions”的class object中。其名称也会被“mangled”,由于在一个复
杂的class派生体系中,可能存在多个vptrs。
2)1是virtual table slot的索引值,关联到normalize()函数。
3)第二个ptr表示this指针。
一样,若是magnitude()也是一个virtual function,它在normalize()之中的调用操做将被转
换以下:
// register float mag = magnitude(); register float mag = ( *this->vptr[ 2 ] )( this );
因为Point3d::magnitude()是在Point3d::normalize()中被调用的,然后者已经由虚拟机制而
决议稳当,因此显示地调用“Point3d实例”会比较有效率,并所以压制因为虚拟机制而产生的不
必要重复调用操做:
// 显示的调用操做(explicitly invocation)会压制虚拟机制 register float mag = Point3d::magnitude();
若是magnitude()声明为inline函数,会更有效率。使用class scope operator显示调用一
个virtual function,其决议方式会和nonstatic member function同样:
register float mag = magnitude_7Point3dFv( this );
对于如下调用:
// Point3d obj; obj.normalize();
若是编译器把它转换为:
( *obj.vptr[ 1 ] )( &obj );
虽然语意正确,却没有必要。”经由一个class object 调用一个virtual function“,这种操做
应该老是被编译器像对待通常nonstatic member function同样地加以决议:
normalize_7Point3dFv( &obj );
这项优化的一利益是,virtual function的一个inline函数实例能够被扩展(expanded)开
来,于是提供极大的效率利益。
若是Point3d::normalize()是一个static member function,如下两个调用操做:
obj.normalize(); ptr->normalize();
将被转换为通常的nonmember函数调用,以下:
// obj.normalize(); normalize_7Point3dSFv(); // ptr->normalize(); normalize_7Point3dSFv();
在C++引入static member functions以前,不多会看到以下怪异写法:
( ( Point3d* )0 )->object_count();
其中的object_count只是简单传回_object_count这个static data member。
在引入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。
这么一来就存取static data members时产生了一些不规则性。若是class的设计者把static
data member声明为nonpublic(这一直被视为一种好的习惯),那么他就必须提供一个或多个
member functions来存取该member。所以,虽然你能够不靠class object来存取一个static
member,但其存取函数却得绑定于一个class object之上。
独立于class object以外的存取操做,在某个时候特别重要:当class设计者但愿支持”没
有class object存在“的状况时。程序方法上的解决之道是很奇特意把0强制转换为一个class指
针,于是提供出一个this指针实例:
// 函数调用的内部转换 object_count( ( Point3d* )0 );
Static member functions的主要特性就是它没有this指针。如下次要特性通通根源于其主
要特性:
1)它不能直接存取其class中的nonstatic members。
2)它不可以被声明为const、volatile或virtual。
3)它不须要经由class object才被调用——虽然大部分时候它是这样被调用的!
“member selection”语法的使用是一种符号上的便利,它会被转化为一个直接调用操做:
if( Point3d::object_count() > 1 ) ...
若是class object是由于某个表达式而得到的,会如何?例如:
if( foo().object_count() > 1 ) ...
这个表达式仍然须要被评估求值:
// 转化,以保存反作用 ( void ) foo(); if( Point3d::object_count() > 1 ) ...
一个static member function,固然会被提出于class声明以外,并给予一个通过
“mangled”的适当名字。例如:
unsigned int Point3d::object_count() { return _object_count; }
会被cfront转化为:
// 在cfront之下的内部转化结果 unsigned int object_count_5Point3dSFv() { return _object_count_5Point3d; }
其中SFv表示它是一个static member function,拥有一个空白(void)的参数链表
(argument list)。
因为static member function没有this指针,因此其地址的类型并非一个“指向class
member function的指针”,而是一个“nonmember函数指针”。也就是说:
&Point3d::object_count();
会获得一个数值,类型是:
unsigned int (*)();
而不是:
unsigned int ( Point3d::* )( );
Static member function因为缺少this指针,所以差很少等同于nonmember function。它
提供了一个意想不到的好处:成为一个callback函数,使咱们得以将C++和C-base X Window系
统结合。它们也能够成功地应用在线程(threads)函数身上。
virtual function的通常实现模型:,每个class 有一个virtual table,内含该
class之中有做用的virtual function的地址,而后每一个object有一个vptr,指向virtual
table的所在。
为了支持virtual function机制,必须首先可以对于多态对象有某种形式的“执行期类型判断
(runtime type resolution)”。也就是说如下的调用操做将须要ptr在执行期的某些相关信息:
ptr->z();
如此一来才可以找到并调用z()的适当实例。
或许直截了当可是成本最高的解决方法就是把必要信息加载ptr身上。在这样的策略之
下,一个指针(或是一个reference)持有两项信息:
1)它所参考到的对象的地址(也就是目前它所持有的东西);
2)对象类型的某种编码,或是某个结构(内含某些信息,用以正确决议出z()函数实例)
的地址。
这个方法带来两个问题:第一,它明显增长了空间负担,即便程序并不使用多态
(polymorphism);第二,它打断了与C程序间的连接兼容性。
若是这份额外信息不可以和指针放在一块儿,下一个能够考虑的地方就是把它放在对象本
身。可是哪个对象真正须要这些信息呢?咱们应该把这些信息放进可能被继承的每个集合
体身上呢?也许。但请考虑一下这样的C struct声明:
struct date { int m, d, y; };
这符合上述规则。然而事实上它并不须要那些信息。加上那些信息将使C struct膨胀而且
打破连接兼容性,却没有带来任何明显的补偿利益。
而面对那些显示使用了class关键词的声明,才应该加上额外的执行期信息。这样作能够
保持语言的兼容性,不过仍然不是一个够聪明的政策。例如,下面这个class符合新规则:
class data { public: int m, d, y; };
但实际上它并不须要那份信息。下面的class声明虽然不符合新规范,却须要那份信息:
struct geom { public: virtual ~geom(); ... };
咱们须要一个以class的使用为基础,而不在意关键词是class或struct的规范。若是class
真正须要那份信息,它就会存在;若是不须要,它就不存在。很明显在必须支持某种形式之“执行
期多态(runtime polymorphism)”的时候须要这份信息。
在C++中,多态(ploymorphism)表示“一个public base class的指针(或reference),
寻找出一个derived class object”的意思。例以下面的声明:
Point *ptr;
咱们能够指定ptr以寻址出一个Point2d对象:
ptr = new Point2d;
或是一个Point3d对象:
ptr = new Point3d;
ptr的多态技能主要扮演一个输送机制(transport mechanism)的角色,经由它,咱们
能够在程序的任何地方采用一组public derived类型。这种多态形式被称为是消极的(passive)
,能够在编译时期完成——virtual base class的状况除外。
当被指出的对象真正被使用时,多态也就变成积极的(active)了。下面对于virtual
function的调用,就是一例:
// "积极多态(active ploymorphism)"的常见例子 ptr->z();
在funtime type identification(RTTI)性质于1993年被引入C++语言以前,C++对“积极
多态(active polymorphism)”的惟一支持,就是对于virtual function call 的决议操做。有了
RTTI,就可以在执行期查询一个多态的pointer或多态的reference了:
// "积极多态(active polymorphism)"的第二个例子 if( Point3d *p3d = dynamic_cast<Point3d*>( ptr ) ) return p3d->_Z;
因此欲鉴定哪些classes展示多态特性,咱们须要额外的执行期信息。关键词class和
struct并不能帮助咱们。因为没有导入像是polymorphic之类的新关键词,所以识别一个class是
否支持多态,惟一适当的方法就是看看它是否有任何virtual function。只要class拥有一个virtual
function,它就须要这份额外的执行期信息。
下一个明显的问题是,什么样的额外信息是咱们须要存储起来的?也就是说,若是有这
样的调用:
ptr->z();
其中z()是一个virtual function,那么什么信息才能让咱们在执行期调用正确的z()实例?
须要知道:
1)ptr所指对象的真实类型。这可以使咱们选择正确的z()实例。
2)z()实例的位置,以便可以调用它。
在实际上,首先能够在每个多态的class object身上增长两个members:
1)一个字符串或数字,表示class的类型。
2)一个指针,指向某表格,表格中持有程序的virtual functions的执行期地址。
关于表格中的virtual functions地址如何被构建起来。在C++中,virtual functions(可经由
其class object被调用)能够在编译时期获知。此外,这一组地址是固定不变的,执行期不可能
新增或替换之。因为程序执行时,表格大小和内容不会改变,因此其建构和存取皆能够由编译
器彻底掌控,不须要执行期的任何介入。
然而,执行期备妥那些函数地址,只是解答的一半而已。另外一半解答是找到那些地址。
两个步骤能够完成这项任务:
1)为了找到表格,每个class object被安插了一个由编译器内部产生的指针,指向该
表格。
2)为了找到函数地址,每个virtual function被指派一个表格索引值。
这些工做都是由编译器完成。执行期要作的,只是在特定的virtual table slot中激活virtual
function。
一个class只会有一个virtual table。每个table内含其对应之class object中全部active
virtual function函数实例的地址。包括:
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的关系。例如咱们的Point class体系中:
#include <iostream> class Point { public: Point( float x = 0.0 ) : _x( x ) { } virtual ~Point() { } virtual int mult( float ) = 0; // ...其余操做 float x() const { return _x; } virtual float y() const { return 0; } virtual float z() const { return 0; } // ... protected: 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 int mult( float y ) { return 1; } 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 int mult( float z ) { return 2; } float z() const { return _z; } // ...其余操做 protected: float _z; }; int main() { Point2d point2d; Point3d point3d; std::cout << "sizeof( Point ) = " << sizeof( Point ) << std::endl; std::cout << "sizeof( point2d ) = " << sizeof( point2d ) << std::endl; std::cout << "sizeof( point3d ) = " << sizeof( point3d ) << std::endl; return 0; }
下面是class Point的虚表:
.section .rodata._ZTV5Point,"aG",@progbits,_ZTV5Point,comdat .align 8 .type _ZTV5Point, @object .size _ZTV5Point, 28 _ZTV5Point: # vtable for Point .long 0 .long _ZTI5Point # typeinfo for Point .long _ZN5PointD1Ev # Point::~Point() .long _ZN5PointD0Ev # Point::~Point() .long __cxa_pure_virtual .long _ZNK5Point1yEv # Point::y() const .long _ZNK5Point1zEv # Point::z() const
virtual destructor被指派slot 2,3,而mult()被指派slot 4。此例并无mult()的函数定义(因
为它是一个pure virtual function),因此pure_virtual_called()的函数地址会被放在slot 4。若是
该函数意外地被调用,一般操做是结束这个程序。y()被指派slot 5,z被指派slot 6。x()不是
virtual function因此不存在虚表中。
下面是class Point2d的虚表:
.section .rodata._ZTV7Point2d,"aG",@progbits,_ZTV7Point2d,comdat .align 8 .type _ZTV7Point2d, @object .size _ZTV7Point2d, 28 _ZTV7Point2d: # vtable for Point2d .long 0 .long _ZTI7Point2d # typeinfo for Point2d .long _ZN7Point2dD1Ev # Point2d::~Point2d() .long _ZN7Point2dD0Ev # Point2d::~Point2d() .long _ZN7Point2d4multEf # Point2d::mult(float) .long _ZNK7Point2d1yEv # Point2d::y() const .long _ZNK5Point1zEv # Point::z() const
当一个class派生自Point时,会发生什么事?
一共有三种可能性:
1)它能够继承base class所声明的virtual function的函数实例。正确地说是,该函数实例
的地址会被拷贝到derived class的virtual table的相对应slot之中。
2)它可使用本身的函数实例。这表示它本身的函数实例地址必须放在对应的slot之中。
3)它能够加入一个新的virtual function。这时候virtual table的尺寸会增大一个slot,而新
的函数实例地址会被放进该slot之中。
Point2d的virtual table在slot 2,3中指出destructor,而slot 4中指出mult()(取代pure
virtual function)。它本身的y()函数实例地址放在slot 5中,继承自Point的z()函数实例地址则放
在slot 6中。
下面是class Point3d的虚表:
.section .rodata._ZTV7Point3d,"aG",@progbits,_ZTV7Point3d,comdat .align 8 .type _ZTV7Point3d, @object .size _ZTV7Point3d, 28 _ZTV7Point3d: # vtable for Point3d .long 0 .long _ZTI7Point3d # typeinfo for Point3d .long _ZN7Point3dD1Ev # Point3d::~Point3d() .long _ZN7Point3dD0Ev # Point3d::~Point3d() .long _ZN7Point3d4multEf # Point2d::mult(float) .long _ZNK7Point2d1yEv # Point2d::y() const .long _ZNK7Point3d1zEv # Point3d::z() const
一样对于派生自Point2d的Point3d,其virtual table中的slot 2,3放置Point3d的
destructor,slot 4放置Point3d::mult()函数地址,slot 5放置继承自Point2d的y()函数地址,slot 6
放置本身的z()函数地址。
如今,若是有这样的式子:
ptr->z();
如何有足够的知识在编译时期设定virtual function的调用呢?
1)通常而言,在每次调用z()时,并不知道ptr所指对象的真正类型。然而知道经由ptr能够
存取到该对象的virtual table。
2)虽然不知道哪个z()函数实例会被调用,但知道每个z()函数地址都被放在slot 6中。
这些信息使得编译器能够将该调用转化为:
( *ptr->vptr[ 6 ] )( ptr );
这一转化中,vptr表示编译器所安插的指针,指向virtual table;6表示z()被指派的slot编号
(关系到Point体系的virtual table)。惟一一个在执行期才可以知道的东西是:slot 6所指的到
底是哪个z()函数实例。
在一个单一继承体系中,virtual function 机制的行为十分良好,不但有效率并且很容易塑
造出模型来。可是在多重继承和虚拟继承之中,对virtual functions的支持就没那么美好了。
在多重继承中支持virtual functions,其复杂度围绕在第二个及后继的base classes身上,
以及”必须在执行期调整this指针“这一点。如下面的class体系为例:
#include <iostream> class Base1 { public: Base1( float base1 ) : data_Base1( base1 ) { } virtual ~Base1() { } virtual void speakClearly() { data_Base1 += 1000.0; } virtual Base1 *clone() const { return new Base1( this->data_Base1 ); } protected: float data_Base1; }; class Base2 { public: Base2( float base2 ) : data_Base2( base2 ) { } virtual ~Base2() { } virtual void mumble() { data_Base2 -= 1.0; } virtual Base2 *clone() const { return new Base2( this->data_Base2 ); } protected: float data_Base2; }; class Derived : public Base1, public Base2 { public: Derived( float data1, float data2, float derived ) : Base1( data1 ), Base2( data2 ), data_Derived( derived ) { } virtual ~Derived() { } virtual Derived *clone() const { return new Derived( this->data_Base1, this->data_Base2, this->data_Derived ); } protected: float data_Derived; }; int main() { Base1 base1( 1.0 ); Base2 base2( 2.0 ); Derived derived( 1.0, 2.0, 3.0 ); std::cout << "sizeof( base1 ) = " << sizeof( base1 ) << std::endl; std::cout << "sizeof( base2 ) = " << sizeof( base2 ) << std::endl; std::cout << "sizeof( derived ) = " << sizeof( derived ) << std::endl; return 0; }
"Derived支持virtual functions"的困难度,通通落在Base2 subobject身上。有三个问题需
要解决,以此而言分别是(1)virtual destructor,(2)被继承下来的Base2::mumble(), (3)一组
clone()函数实例。
首先,把一个从heap中配置而得的Derived对象的地址,指定给一个Base2指针:
Base2 *pbase2 = new Drived;
新的derived对象的地址必须调整以指向其Base2 subobject。编译时期会产生如下的代
码:
// 转移以支持第二个base class Derived *temp = new Drived; Base2 *pbase2 = temp ? temp + sizeof( Base1 ) : 0;
若是没有这样的调整,指针的任何”非多态运用“都将失败:
// 即便pbase2被指定一个Derived对象,这也应该没有问题 pbase2->data_Base2;
当程序员要删除pbase2所指的对象时:
// 必须首先调用正确的virtual destructor函数实例 // 而后施行delete运算符 // pbase2 可能须要调整,以指出完整对象的起始点 delete pbase2;
指针必须被再一次调整,以求再一次指向Drived对象的起始处。然而上述的offset加法
却不可以在编译时期直接设定,由于pbase2所指的真正对象只有在执行期才能肯定。
通常规则是,经由指向”第二或后继之base class“的指针(或reference)来调用derived
class virtual function。
Base2 *pbase2 = new Derived; ... delete pbase2; // invoke derived class's destructor( virtual )
其所连带的必要的”this指针调整”操做,必须在执行期完成。也就是说,offset的大小,
以及把offset加到this指针上头的那一小段程序代码,必须由编译器在某个地方插入。问题是,
在哪插入?
Bjarne原先实施于cfront编译器中的方法是将virtual table 加大,使它容纳此处所须要的
this指针,调整相关事物。每个virtual table slot,再也不只是一个指针,而是一个集合体,内含
可能的offset以及地址。因而virtual function的调用操做由:
( *pbase2->vptr[ 1 ] )( pbase2 );
改变为:
( *pbase2->vptr[ 1 ].faddr ) ( pbase2 + pbase2->vptr[ 1 ].offset );
其中faddr内含virtual function地址,offset内含this指针调整值。
这个作法的缺点是,它至关于连坐“处罚”了全部的virtual function调用操做,无论它们是
否须要offset的调整。
比较有效率的解决方法是利用所谓的thunk。所谓thunk是一小段assembly代码,用来(1)
以适当的offset值调整this指针,(2)跳到virtual function去。例如,经由一个Base2指针调用
Derived destructor,其相关的thunk可能看起来是这个样子:
// 虚拟C++代码 pbase2_dtor_thunk: this += sizeof( base1 ); Derived::~Derived( this );
Thunk技术容许virtual table slot继续内含一个简单的指针,所以多重继承不要任何空间上
的额外负担。Slots中的地址能够直接指向virtual function,也能够指向一个相关的thunk(如需
调整this指针的话)。
调整this指针的第二个额外负担就是,因为两种不一样的可能:(1)经由derived class(或
第一个base class)调用,(2)经由第二个(或后继)base class调用,同一函数在virtual table
中可能须要多笔对应的slots。例如:
Base1 *pbase1 = new Derived; Base2 *pbase2 = new Derived; delete pbase1; delete pbase2;
虽然两个delete操做致使相同的Derived destructor,可是它们须要两个不一样的virtual
table slots:
1)pbase1不须要调整this指针(由于Base1是最左端base class之故,它已经指向
Derived对象的起始处)。其virtual table slot需放置真正的destructor地址。
2)pbase2须要调整this指针。其virtual table slot须要相关的thunk地址。
在多重继承之下,一个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。
class Base1的虚表:
.weak _ZTV5Base1 .section .rodata._ZTV5Base1,"aG",@progbits,_ZTV5Base1,comdat .align 8 .type _ZTV5Base1, @object .size _ZTV5Base1, 24 _ZTV5Base1: # vtable for Base1 .long 0 .long _ZTI5Base1 # typeinfo for Base1 .long _ZN5Base1D1Ev # Base1::~Base1() .long _ZN5Base1D0Ev # Base1::~Base1() .long _ZN5Base112speakClearlyEv # Base1::speakClearly() .long _ZNK5Base15cloneEv # Base1::clone() const
class Base2的虚表:
.weak _ZTV5Base2 .section .rodata._ZTV5Base2,"aG",@progbits,_ZTV5Base2,comdat .align 8 .type _ZTV5Base2, @object .size _ZTV5Base2, 24 _ZTV5Base2: # vtable for Base2 .long 0 .long _ZTI5Base2 # typeinfo for Base2 .long _ZN5Base2D1Ev # Base2::~Base2() .long _ZN5Base2D0Ev # Base2::~Base2() .long _ZN5Base26mumbleEv # Base2::mumble() .long _ZNK5Base25cloneEv # Base2::clone() const
class Derived的虚表:
.weak _ZTV7Derived .section .rodata._ZTV7Derived,"aG",@progbits,_ZTV7Derived,comdat .align 32 .type _ZTV7Derived, @object .size _ZTV7Derived, 48 _ZTV7Derived: # vtable for Derived .long 0 .long _ZTI7Derived # typeinfo for Derived .long _ZN7DerivedD1Ev # Derived::~Derived() .long _ZN7DerivedD0Ev # Derived::~Derived() .long _ZN5Base112speakClearlyEv # Base1::speakClearly() .long _ZNK7Derived5cloneEv # Derived::clone() const .long -8 .long _ZTI7Derived # typeinfo for Derived .long _ZThn8_N7DerivedD1Ev # non-virtual thunk to Derived::~Derived() .long _ZThn8_N7DerivedD0Ev # non-virtual thunk to Derived::~Derived() .long _ZN5Base26mumbleEv # Base2::mumble() .long _ZTchn8_h8_NK7Derived5cloneEv # covariant return thunk to Derived::clone() const
因而当你将一个Derived对象地址指定给一个Base1指针或Derived指针时,被处理的
virtual table是主要表格。而当你将一个Derived对象地址指定给一个Base2指针时,被处理的
virtual table是次要表格。
因为执行期连接器(runtime linkers)的降临(能够支持动态共享函数库),符号名称的
连接可能变得很是缓慢。为了调节执行期链接器的效率,Sun编译器将多个virtual tables连锁为
一个:指向次要表格的指针,可由主要表格名称加上一个offset得到。这样的策略下,每个
class只有一个具名的virtual table。
有三种状况,第二或后继的base class会影响对virtual functions的支持。第一种状况
是,经过一个“指向第二个base class”的指针,调用derived class virtual function。例如:
Base2 *ptr = new Derived; // 调整Derived::~Derived // ptr必须被向后调整sizeof( Base1 )个bytes delete ptr;
这个操做的重点:ptr指向Derived对象中的Base2 subobject;为了可以正确执行,ptr必须
调整指向Derived对象的起始处。
第二种状况是第一种状况的变化,经过一个“指向derived class”的指针,调用第二个
base class中一个继承而来的virtual function。在此状况下,derived class指针必须再次调整,
以指向第二个base subobject。例如:
Derived *pder = new Derived; // 调用Base2::mumble() // pder必须被向前调整sizeof( Base1 )个bytes pder->mumble();
第三种状况发生于一个语言扩充性质之下:容许一个virtual function的返回值类型有所变
化,多是base type,也多是publicly derived type。这一点能够经由Derived::clone()函数实
例来讲明。clone函数的Derived版本回传一个Derived class指针,默默地改写了它的两个base
class函数实例。当咱们经过“指向第二个base class”的指针来调用clone()时,this指针的offset问
题因而诞生了:
Base2 *pb1 = new Derived; // 调用Derived* Derived::clone() // 返回值必须被调整,以指向Base2 subobject Base2 *pb2 = pb1->clone();
当进行pb1->clone()时,pb1会被调整指向Derived对象的起始地址,因而clone()的
Derived版会被调用,它会传回一个指针,指向一个新的Derived对象;该对象的地址在被指定给
pb2以前,必须先通过调整,以指向Base2 subobject。
Microsoft以所谓的“address points“来代替thunk策略。即将用来改写别人的那个函数
(overriding function)期待得到的是”引入该virtual function之class“(而非derived class)的地
址。这就是该函数的“address point”。
考虑下面的virtual base class派生体系,从Point2d派生出Point3d:
#include <iostream> class Point2d { public: Point2d( float x = 0.0, float y = 0.0 ) : _x( x ), _y( y ) { } virtual ~Point2d() { } virtual void mumble( ) { _y += _x; } virtual float z() { return _x + _y; } protected: float _x, _y; }; class Point3d : public virtual Point2d { public: Point3d( float x = 0.0, float y = 0.0, float z = 0.0 ) : Point2d( x, y ), _z( z ) { } ~Point3d() { } float z() { return _z; } protected: float _z; }; int main() { Point2d point2d; Point3d point3d; std::cout << "sizeof( point2d ) = " << sizeof( point2d ) << std::endl; std::cout << "sizeof( point3d ) = " << sizeof( point3d ) << std::endl; return 0; }
class Point2d的虚表:
.weak _ZTV7Point2d .section .rodata._ZTV7Point2d,"aG",@progbits,_ZTV7Point2d,comdat .align 8 .type _ZTV7Point2d, @object .size _ZTV7Point2d, 24 _ZTV7Point2d: # vtable for Point2d .long 0 .long _ZTI7Point2d # typeinfo for Point2d .long _ZN7Point2dD1Ev # Point2d::~Point2d() .long _ZN7Point2dD0Ev # Point2d::~Point2d() .long _ZN7Point2d6mumbleEv # Point2d::mumble() .long _ZN7Point2d1zEv # Point2d::z()
class Point3d的虚表:
.weak _ZTV7Point3d .section .rodata._ZTV7Point3d,"aG",@progbits,_ZTV7Point3d,comdat .align 32 .type _ZTV7Point3d, @object .size _ZTV7Point3d, 60 _ZTV7Point3d: # vtable for Point3d .long 8 .long 0 .long _ZTI7Point3d # typeinfo for Point3d .long _ZN7Point3dD1Ev # Point3d::~Point3d() .long _ZN7Point3dD0Ev # Point3d::~Point3d() .long _ZN7Point3d1zEv # Point3d::z() .long -8 .long 0 .long -8 .long -8 .long _ZTI7Point3d # typeinfo for Point3d .long _ZTv0_n12_N7Point3dD1Ev # virtual thunk to Point3d::~Point3d() .long _ZTv0_n12_N7Point3dD0Ev # virtual thunk to Point3d::~Point3d() .long _ZN7Point2d6mumbleEv # Point2d::mumble() .long _ZTv0_n20_N7Point3d1zEv # virtual thunk to Point3d::z()
虽然Point3d有惟一一个(同时也是最左边的)base class,也就是Point2d,单Point3d和
Point2d的起始部分并不像”非虚拟的单一继承“状况那样一致。因为Point2d和Point3d的对象不
再相符,二者之间的转换也就须要调整this指针。至于在虚拟继承的状况下要消除thunks,通常
而言已经被证实是一项该难度技术。
建议是不要在一个virtual base class中声明nonstatic data members。要否则会愈来愈复
杂。
在下面这组测试中,计算两个3D点,其中用到一个nonmember friend function,
一个member function,以及一个virtual member function,而且Virtual member
function分别在单1、虚拟、多重继承三种状况下执行。
对于nonmember function:
未优化:
优化:
对于inline member:
未优化:
优化:
对于static Member:
未优化:
优化:
对于nonstatic Member:
未优化:
优化:
对于Virtual Member:
未优化:
优化:
对于Virtual Member(多重继承):
未优化:
优化:
对于Virtual Member(虚拟继承):
未优化:
优化:
nonmember 、static member或nonstatic member函数都被转化为彻底相同的形式。因此三
者效率彻底相同。
virtual member的效率相比前三项下降了4%到11%不等。
多重继承中的virtual function的调用利用thunk技术用掉了较多成本。
而虚拟继承花掉了最多的成本。
下面使用两种方法优化:
1)在函数参数中加上一个对象,用以存放加法的结果:
void Point3d::cross_product( Point3d &pC, const Point3d &pA, const Point3d &pB ) { pC.x = pA.y * pB.z - pA.z * pB.y; pC.y = pA.z * pB.x - pA.x * pB.z; pC.z = pA.x * pB.y - pA.y * pB.x; }
能够看到在未优化状况下,效率优化了50%。
2)直接在this对象中计算结果:
void Point3d::cross_product( const Point3d &pB ) { x = y * pB.z - z * pB.y; y = z * pB.x - x * pB.z; z = x * pB.y - y * pB.x; }
取一个nonstatic data member的地址,获得的结果是该member在class布局中的
bytes位置(再加1)。能够想象它是一个不完整的值,它须要被绑定于某个class
object的地址上,才可以被存取。
取一个nonstatic member function的地址,若是该函数是nonvirtual,获得的结果是它在内存
中真正的地址。然而这个值也是不彻底的。它也须要被绑定于某个class object的地址上,才能
够经过它调用该函数。全部的nonstatic member functions都须要对象的地址(以this指出)。
一个指向member function的指针,其声明语法以下:
double // return type { Point::* // class the function is member pmf } // name of pointer to member (); // argument list
而后咱们能够这样定义并初始化该指针:
double( Point::*coord )() = &Point::x;
也能够这样指定其值:
coord = &Point::y;
欲调用它,能够这么作:
( origin.*coord )();
或
( ptr->*coord )();
这些操做会被编译器转化为:
// 虚拟C++码 ( coord )( &origin );
和
// 虚拟C++码 ( coord )( ptr );
指向member function的指针的声明语法,以及指向”member selection运算符“的指针,其
做用是做为this指针的空间保存者。这也就是为何static member functions(没有this指针)的
类型是”函数指针”,而不是“指向member function的指针”之故。
使用一个“member function指针”,若是并不用于virtual function、多重继承、virtual base
class等状况的话,并不会比使用一个“nonmember function指针”的成本高。上述三种状况对于
“member function指针”的类型以及调用都太过复杂。事实上,对于那些没有virtual functions、
virtual base class或multiple base classes的classes而言,编译器能够为它们提供相同的效率。
考虑下面的程序片断:
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 function,起地址在编译时期是未知的,所能知道的仅是virtual function在其相关之
virtual table中的索引值。也就是是说,对一个virtual member function取其地址,所能得到的只
是一个索引值。
例如,假设咱们有如下的Point声明:
class Point { public: virtual ~Point(); float x(); float y(); virtual float z(); // ... };
而后取destructor的地址:
&Point::~Point;
取x()或y()的地址:
&Point::x(); &Point::y();
获得的则是函数在内存中的地址,由于它们不是virtual。取z()的地址:
&Point::z();
获得的结果是2。经过pmf来调用z(),会被内部转化为一个编译时期的式子,通常形式如
下:
( *ptr->vptr[ ( int )pmf ] )( ptr );
对一个“指向member function的指针”评估求值,会由于该值有两种意义而复杂化:其调
用操做也将有别于常规调用操做。pmf的内部定义,也就是:
float ( Point::*pmf )();
必须容许此函数可以寻址出nonvirtual x()和virtual z()两个member functions,而那两个
函数有着相同的原型:
// 二者均可以被指定给pmf float Point::x() { return _x; } float Point::z() { return 0; }
只不过其中一个表明内存地址,另外一个表明virtual table中的索引值。所以,编译器必
须定义pmf。使它可以(1)持有两种数值,(2)更重要的是其数值能够被区别表明内存地址还
是Virtual table中的索引值。
在cfront2.0非正式版中,这两个值被内含在一个普通的指针内。cfront如何识别该值是
内存地址仍是virtual table索引呢?它使用了如下技巧:
( ( ( int )pmf ) & ~127 ) ? // non-virtual invocation ( *pmf )( ptr ) : // virtual invocation ( *ptr->vptr[ ( int )pmf ]( ptr ) );
为了让指向member functions的指针也能支持多重继承和虚拟继承,Stroustrup设计了下面
一个结构体:
// 通常结构,用以支持 // 在多重继承之下指向member functions的指针 struct _mptr { int delta; int index; union { ptrtofunc faddr; int v_offset; }; };
index和faddr分别(不一样时)持有virtual table索引和nonvirtual member function地址(为
了方便,当index不指向virtual table时,会被设为-1)。在此模型之下,像这样的调用操做:
( ptr->*pmf )();
会变成:
( pmf.index < 0 ) ? // non-virtual invocation ( *pmf.faddr )( ptr ) : // virtual invocation ( *ptr->vptr[ pmf.index ]( ptr ) );
此法所受到的批评是,每个调用操做都得付出上述成本,检查其是否为virtual或
nonvirtual。Microsoft把这项检查拿掉,导入一个它所谓的vcall thunk。在此策略执之下,faddr
被指定的要不就是真正的member function地址(若是函数是nonvirtual的话),要不就是vcall
thunk的地址。因而virtual或nonvirtual函数的调用操做透明化,vcall thunk会选出并调用相关
virtual table中的适当slot。
这个结构体的另外一个反作用就是,当传递一个不变值的指针给member function时,它需
要产生一个临时性对象。以下:
extern Point3d foo( const Point3d&, Point3d ( Point3d::* )() ); void bar( const Point3d& p ) { Point3d pt = foo( p, &Point3d::normal ); // ... }
其中&Point3d::normal的值相似这样:
{ 0, -1, 10727417 }
将须要产生一个临时性对象,有明确的初值:
// 虚拟C++码 _mpter temp = { 0, -1, 10727417 } foo( p, temp );
delta字段表示this指针的offset值,而v_offset字段放的是一个virtual(或多重继承中的第
二或后继的)base class的vptr位置。若是ptr被编译器放在class对象的起头处,这个字段就没
有必要了,代价则是C对象兼容性下降。这些字段只在多重继承或虚拟继承的状况下才有其必要
性,有许多编译器在自身内部根据不一样的classes特性提供多种指向member functions的指针形
式,例如Microsoft就提供了三种风味:
1)一个单一继承实例(其中持有vcall thunk地址或是函数地址)
2)一个多重继承实例(其中持有faddr和delta两个members)
3)一个虚拟继承实例(其中持有4个members)
下面一组测试中,cross_product()函数经由如下方式调用:
1)一个指向nonmember function的指针;
2)一个指向class member function的指针;
3)一个指向virtual member function的指针;
4)多重继承下的nonvirtual及virtual member function call;
5)虚拟继承下的nonvirtual及virtual member function call;
下面是一个加法运算符的可能实现内容:
class Point { friend Point operator+( const Point&, const Point& ); } Point operator+( const Point &lhs, const Point &rhs ) { Point new_pt; new_pt._x = lhs._x + rhs._x; new_pt._y = lhs._y + rhs._y; return new_pt; }
理论上,一个比较“干净”的作法是使用inline函数来完成set和get函数:
// void Point::x( float new_ ) { _x = new_x; } // float Point::x() { return _x; } new_pt.x( lhs.x() + rhs.x() );
因为咱们受限只能在上述两个函数中对_x直接存取,所以也就将稍后可能发生的data
members的改变所带来的冲击最小化了。若是把这些存取函数声明为inline,咱们就能够继续保
持直接存取members的那种高效率——虽然咱们亦兼顾了函数的封装性。此外,加法运算符不
再须要被声明为Point的一个friend。
然而,实际上咱们并不可以强迫将任何函数都变为inline。关键词inline只是一项请求。如
果这项请求被接受,编译器就必须认为它能够用一个表达式(expression)合理地将这个函数
扩展开来。
通常而言,处理一个inline函数,有两个阶段:
1)分析函数定义,以决定函数的“intrinsic inline ability”(本质的inline能力)。“instrinsic”
一词在这里指“与编译器相关”。
若是函数因其复杂度,或因其建构问题,被判断不可成为inline,它会被转为一个static
函数,并在编译模块内产生对应的函数定义。
2)真正的inline函数扩展操做是在调用的那一点上。这会带来参数的求值操做以及临时对象
的管理。
在inline扩展期间,每个形式参数都会被对应的实际参数取代。通常而言,面对“会带来副
做用的实际参数”,一般都须要引入临时性对象。换句话说,若是实际参数是一个常量表达式
(constant expression),咱们能够在替换以前先完成其求值操做;后继的inline替换,就能够把
常量直接“绑”上去。若是既不是常量表达式,也不是个带有反作用的表达式,那么就直接带换
之。
假设有如下的简单inline函数:
inline int min( int i, int j ) { return i < j ? i : j; }
下面是三个调用操做:
inline int bar() { int minval; int val1 = 1024; int val2 = 2048; /* (1) */ minval = min( val1, val2 ); /* (2) */ minval = min( 1024, 2048 ); /* (3) */ minval = min( foo(), bar()+1 ); return minval; }
标示为(1)的那一行会被扩展为:
// (1)参数直接替换 minval = val1 < val2 ? val1 : val2;
标示为(2)的那一行直接拥抱常量:
// (2) 代换以后,直接拥抱常量 minval = 1024;
表示为(3)的那一行则引起参数的反作用。它须要导入一个临时性对象,以免重复求
值:
// (3) 有反作用,因此导入临时性对象 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; }
这个局部变量须要什么额外的支持或处理吗?若是咱们有如下的调用操做:
{ int local_var; int minval; // ... minval = min( val1, val2 ); }
inline被扩展来后,为了维护其局部变量,可能会成为这个样子
{ int local_var; int minval; // 将inline函数的局部变量处以“mangling”操做 int _min_lv_minval; minval = ( _min_lv_minval = val1 < val2 ? val1 : val2 ), _min_lv_minval; }
通常而言,inline函数中的每个局部变量都必须被放在函数调用的一个封闭区段中,拥有
一个独一无二的名称。若是inline函数以单一表达式扩展屡次,则每次扩展都须要本身的一组局
部变量。若是inline函数以分离的多个式子(duscrete statements)被扩展屡次,那么只须要一
组局部变量,就能够重复使用。
inline函数中的局部变量,再加上有反作用的参数,可能会致使大量临时性对象的产生。特
别是若是它以单一表达式被扩展屡次的话。例如:
minval = min( val1, val2 ) + min( foo(), foo() + 1 );
可能被扩展为:
// 为局部变量产生临时变量 int _min_lv_minval_00; int _min_lv_minval_01; // 为放置反作用值而产生临时变量 int t1; int t2; minval = ( ( _min_lv_minval_00 = val1 < val2 ? val1 : val2 ), _min_lv_minval_00 ) + ( ( _min_lv_minval_01 = ( t1 = foo() ), ( t2 = foo() + 1 ), t1 < t2 ? t1 : t2 ), _min_lv_minval_01 );
Inline函数对于封装提供了一种必要的支持,能够有效存取封装于class中的nonpublic数
据。它同时也是C程序中大量使用的#define(前置处理宏)的一个安全代替品——特别是若是
宏中的参数有反作用的话。然而一个inline函数若是被调用太屡次的话,会产生大量的扩展码,
使程序大小暴涨。
对于既要安全又要效率的程序码,inline函数提供了一个强而有力的工具。然而,与non-
inline函数比起来,它们须要更加当心地处理。