static member functions不可能作到的两点:(1)直接存取nonstatic数据,(2)被声明为const的。程序员
C++的设计准则之一就是:nonstatic member function至少必须和通常的nonmember function有相同的效率。好比,要在下面两个函数之间做选择:express
float magnitude3d(const Point3d *_this){ ... } float Point3d::magnitude3d() const { ... }
选择member function不该该带来什么额外负担。这是由于编译器内部已将“member函数实例”转换为对等的“nonmember函数实例”安全
下面是magnitude()的一个nonmember定义:ide
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的形式。 转化步骤以下:函数
通常而言,member的名称前面会加上class的名称,造成独一无二的命名。例以下面的声明:布局
class Bar{ public: int ival; ... };
其中的ival
有可能变成这样:this
//member通过name-mangling以后的可能结果之一 ival_3Bar
为何编译器要这么作?请考虑以下派生操做:编码
class Foo : public Bar{ public: int ival; ... }
记住,Foo对象内部结合了base class和derived class二者:lua
//Foo的内部描述 class Foo{ public: int ival_3Bar; int ival_3Foo; ... };
无论你要处理哪个ival
,经过"name mangling",均可以绝对清楚地指出来。因为member function能够被重载化(overload),因此须要更普遍的mangling手法,以提供绝对独一无二的名称。设计
把参数和函数名称编码在一块儿,编译器是在不一样的编译模块之间达成了一种有限形式的类型检验。举例以下,若是一个print函数被这样定义:
void print(const Point3d& ){ ... }
但意外地被这样声明和调用:
//觉得是const Point3d& void print(const Point3d );
两个实体若是拥有独一无二的name mangling,那么任何不正确的调用操做在连接时期就因没法决议(resolved)而失败。有时候咱们能够乐观地称此为“确保类型安全的连接行为”(type-safe linkage)。我说“乐观地”是由于它能够捕捉函数标记(signature,亦即函数名称+参数数目 + 参数类型)错误;若是“返回类型”声明错误,就没有办法检查出来。
若是normalize()是一个virtual member function,那么如下调用:
ptr->normalize();
将会被内部转化为:
( *ptr->vptr[1])(ptr);
其中:
若是Point3d::normalize()是一个static member function,如下两个调用操做:
obj.normalize(); ptr->normalize();
将被转换为通常的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++ 语言到当前为止并不能识别这种状况。
static member functions的主要特性是它没有this指针。如下的次要特性通通根源于其主要特性:
若是取一个static member function的地址,得到的将是其在内存中的位置,也就是其地址。因为static member function没有this指针,因此其地址的类型并非一个“指向class member of function的指针”,而是一个“nonmember函数指针”。
virtual function的通常实现模型:每个class有一个virtual table,内含该class之中有做用的virtual function的地址,而后每一个object有一个vptr,指向virtual table的所在。
为了支持virtual function机制,必须首先可以对于多态对象有某种形式的“执行期类型判断法(runtime type resolution)”。也就是说,如下的调用操做将须要ptr在执行期的某些相关信息,
ptr->z();
如此一来才可以找到并调用z()的适当实体。
在C++中,多态(polymorphism)表示“以一个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)了。以下:
//积极多态的常见例子 ptr->z();
在runtime type identification(RTTI)性质于1993年被引入C++ 语言以前,C++ 对“积极多态”的惟一支持,就是对virtual function call的决议(resolution)操做。有了RTTI,就可以在执行期查询一个多态的指针或多态的reference了
//积极多态的第二个例子 if(Point3d *p3d = dynamic_cast<Point3d*>(ptr)) return p3d->_z;
在实现上,能够在每个多态对象的class object身上增长两个members:
表格中的virtual functions地址如何被构建起来?在C++ 中,virtual function(可经由其class object被调用)能够在编译时期获知。此外,这一组地址是固定不变的。执行期不可能新增或替换之。因为程序执行时,表格的大小和内容都不会改变,因此其建构和存取皆能够由编译器彻底掌控,不须要执行期的任何介入。
一个class只会有一个virtual table,每个table内含其对应的class object中全部active virtual functions函数实例的地址。这些active virtual functions包括:
每个virtual function都被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的virtual function的关系。以下的Point class体系中:
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; };
vitual destructo被赋值slot 1。而mult()被赋值slot 2。mult()并无函数定义(由于它是一个pure virtual function),因此pure_virtual_calssed()的函数地址会被放在slot 2中,若是该函数意外地被调用,一般的操做是结束掉这个程序。y()被赋值slot 3而z()被赋值slot 4。下图为Point的内存布局和virtual table。
当一个class派生自Point时,会发生什么事情?
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; };
一共有三种可能性:
相似的状况以下,Point3d派生自Point2d:
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; };
其virtual table中的slot 1 放置Point3d destructor,slot 2放置Point3d::mult()。slot 3放置继承自Point2d的y()函数地址,slot 4放置本身的z() 函数地址。Point2d,Point3d的对象布局和virtual table以下:
在一个单一继承体系中,virtual function机制的行为十分良好,不但有效率并且很容易塑造出模型来。可是在多重继承和虚拟继承中,对virtual function的支持就没有那么美好了。
在多重继承中支持virtual functions,其复杂读围绕在第二个及后继的base classes身上,以及“必须在执行期调整this指针”这一点。如下面的class体系为例:
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; };
Derived 支持virtual functions的困难度,通通落在Base2 subobject身上。有三个问题须要解决,以此而言分别是(1)virtual destructor,(2)被继承下来的Base2::mumble(),(3)一组clone()函数实体。
首先,把一个从heap中配置而得的Derived对象的地址,指定给一个Base2指针:
Base2 *pbase2 = new Derived;
新的Derived对象的地址必须调整,以指向其Base2 subobject。编译时期会产生如下的码:
//转移以支持第二个base class Derived *tmp = new Derived; Base2 *pbase2 = tmp ? tmp + 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。如:
Base2 *pbase2 = new Derived; ... delete pbase2; //invoke derived class's destructor(virtual)
该调用操做所连带的“必要的this指针调整”操做,必须在执行期完成。也就是说,offset的大小,以及把offset加到this指针上头的那一小段程序代码,必须经由编译器在某个地方插入。
比较有效率的解决办法是利用所谓的thunk。所谓thunk是以小段assembly代码,用来(1)以适当的offset值调整this指针,(2)调到virtual function去。例如,经由一个Base2指针用Derived destructor,其相关的thunk可能看起来是这个样子的:
pbase2_dtor_thunk: this += sizeof(base1); Derived::~Derived(this);
Thunk技术容许virtual table slot继续内含一个简单的指针,所以多重继承不须要任何空间上的额外负担。Slots中的地址能够直接指向virtual function,也能够指向一个相关的thunk(若是须要调整this指针的话)。因而,对于那些不须要调整this指针的virtual function而言,也就不需承载效率上的额外负担。
调整this指针的第二个额外负担就是,因为两个不一样的可能:(1)经由derived class(或第一个base class)调用,(2)经由第二个(或其后继)base class调用,同一函数在virtual table中可能须要多笔对应的slots。如:
Base1 *pbase1 = new Derived; Base2 *pbase2 = new Derived; delete pbase1; delete pabse2;
虽然两个delete操做致使相同的Derived destructor,但它们须要两个不一样的virtual table slots:
在多重继承之下,一个derived class内含n-1个额外的virtual tables,n表示其上一层base classes的个数(所以,单一继承将不会有额外的virtual tables)。
针对每个virtual tables,Derived对象中有对应的vptr。下图说明了这点,vptrs将在constructor(s)中被设定初值(经由编译器所产生的码)
用以支持“一个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。
因为执行期连接器(runtime linkers)的降临(能够支持动态共享函数库),符号名称的连接变得很是缓慢。为了调节执行期连接器的效率,Sun编译器将多个virtual tables连锁为一个;指向次要表格的指针,可由主要表格名称加上一个offset得到。在这样的策略下,每个class只有一个具名的virtual table。
有如下三种状况,第二或后继的base class会影响对virtual functions的支持。
Base2 *ptr = new Derived; //调用Derived::~Derived //ptr必须向后调整sizeof(Base1)个bytes delete ptr;
从上面那个图能够看到这个调用操做的重点:ptr指向Derived对象中的Base2 subobject;为了可以正确执行,ptr必须调整指向Derived对象的起始处。
Derived *pder = new Derived; //调用Base2::mumble() //pder必须被向前调整sizeof(Base1)个bytes pder->mumble();
Base2 *pb1 = new Derived; //调用Derived * Derived::clone() //返回值必须被调整,指向Base2 subobject Base2 *pb2 = pb1->clone();
当进行pb1->clone()时,pb1会被调整指向Derived对象的起始地址,因而clone()的Derived版会被调用;它会传回一个指针,指向一个新的Derived对象,该对象的地址在被指定给pb2以前,必须先通过调整,以指向Base2 subobject。
考虑下面的virtual base class派生体系,从Point2d派生出Point3d:
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; };
其内存布局以下图:
当一个virtual base class从另外一个virtual base class派生而来,而且二者都支持virtual functions和nonstatic data members时,编译器对于virtual base class的支持简直就像进了迷宫同样。建议不要在一个virtual base class中声明nonstatic data members。
取一个nonstatic data member的地址,获得的结果是该member在class布局中bytes位置(再加1)。能够想象,它是一个不完整的值,它须要被绑定于某个class object的地址上,才可以被存取
取一个nonstatic data member的地址,若是该函数是nonvirtual,获得的结果是它在内存中真正的地址。然而这个值也是不彻底的。它也须要被绑定于某个class object的地址上,才可以经过它调用该函数。全部的nonstatic member functions都须要对象的地址(以参数this指出)
一个指向member fucntion的指针,其声明语法以下:
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->*corrd)();
这些操做会被编译器转化为:
(coord)(&origin); 和 (coord)(ptr);
指向member function的指针的声明语法,以及指向“member selection运算符”的指针,其做用是做为this指针的空间保留着。这也就是为何static member functions(没有this指针)的类型是“函数指针”,而不是“指向member function的指针”之故。
使用一个“member function指针”,若是并不用于virtual function、多重继承、virtual base class等状况的话,并不会比使用一个“nonmember function指针”的成本高
注意下面的程序片断:
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)();
也就是说,虚拟机制仍然可以在使用"指向memeber 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
获得的结果是 1, 取x()或y()的地址:
&Point::x(); &Point::y();
获得的则是函数在内存中的地址,由于它们不是virtual。取z()的地址:
&Point::z();
获得的结果是2。经过pmf来调用z(),会被内部转化为一个编译时期的式子,通常形式以下:
(*ptr->vptr[(int)pmf])(ptr);
对一个“指向member function的指针”评估求值,会由于改制有两种意义而复杂化;其调用操做也将有别于常规的调用操做。集编译器必须定义函数指针使它可以(1)含有两种数值,(2)更重要的是其数值能够被区别表明内存地址仍是virtual table中的索引值。
为了让指向member functions的指针也可以支持多重继承和虚拟继承,Stroustrup设计了下面一个结构体:
//通常结构,用以支持在多重继承之下指向member functions的指针 struct _mptr{ int delta; int index; union{ protofunc 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。
delta字段表示this指针的offset值,而v_offset字段放的是一个virtual(或多重继承中的第二或后继的)base class的vptr位置。若是vptr被编译器放在class对象的起头处。这个字段就没有必要了,代价则是C对象兼容性下降。这些字段只在多重继承或虚拟继承的状况下才有其必要性,有许多编译器在自身内部根据不一样的classes特性提供多种指向member functions的指针形式,例如Microsoft就供应了三种风味:
通常而言,处理一个inline函数,有两个阶段:
若是函数因其复杂度,或因其建构问题,被判断不可成为inline,它会被转为一个static函数,并在“被编译模块”内产生对应的函数语义。
一样在扩展点上,编译器将决定这个调用是否“不可为inline”。
通常而言,面对“会带来反作用的实际参数”,一般都须要引入临时性对象。换句话说,若是实际参数时一个常量表达式(constant expression),咱们能够在替换以前先完成其求值操做(evaluations);后继的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)的那一行会被扩展为:
minval = val1 < val2 ? val1 : val2;
标识为(2)的那一行直接拥抱常量:
minval = 1024;
标识为(3)的那一行则引起参数的反作用,它须要导入一个临时对象,以免重复求值(multiple evaluations)
int t1; int t2; minval = (t1 = foo()), (t2 = bar() + 1), t1 < t2 ? t1 : t2;
通常而言,inline函数中的每个局部变量都必须被放在函数调用的一个封闭区段中,拥有一个独一无二的名称。若是inline函数以单一表达式(expression)扩展屡次,则每次扩展都须要本身的一组局部变量。若是inline函数以分离的多个式子(discrete statements)被扩展屡次,那么只需一组局部变量,就能够重复使用(译注:由于它们被放在一个封闭区段中,有本身的scope)