《深度摸索C++工具模子》念书条记(4)
副标题#e#
***非静态成员函数(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正是 在这种景象下应运而生的。
#p#副标题#e#
编译器的开拓者针对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的函数实体
#p#分页标题#e#
这是在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地点
详细的办理要领是:
#p#分页标题#e#
在多重担任下,一个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***
#p#分页标题#e#
在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.