4.1 Member的各种调用方式
4.1.1 非静态成员函数
C++设计准则之一就是非静态成员函数至少和非成员函数的效率是一样的。实质是编译器已将member函数实体转换为对等的nonmember函数实体
转换过程如下:
1.改写函数的signature(原型),安插一个额外的参数到member function中,用以提供一个存取管道,使class object得以调用该函数,该参数为this指针
//signature
Point3d Point3d::magnitude();
//改写
Point3d Point3d::magnitude(Point3d *const this);
2.对每一个“对nonstatic data member”的存取操作改为经由this指针来存取
{
return sqrt(this->_x *this->_x + ....;
}
3.将member function重新写成一个外部函数,对函数名称进行name mangling,使其在程序中独一无二
4.1.1.1 名称的特殊处理(Name Mangling)
通常的处理方式为member的名称前面加上类名。
class Bar{int val;}
//member经过name-mangling的可能结果
val_3Bar;
PS:可能还会加上参数链表,参数类型等
4.1.2 虚拟成员函数
//normalize是一个虚函数
ptr->normalize();
//内部转化
(*ptr->vptr[1])(ptr);
在上面的代码中,首先取ptr指针内容中的vptr(vptr的名称会被name-mangling,因为在复杂的class中可能会有多个vptr),[1]表示虚函数表的索引值,指向normalize这个虚函数,第二个ptr表示this指针
4.1.3 静态成员函数
该函数的主要特性:
1.不能够直接存取其class中的nonstatic members
2.没有this指针,因为this指针的用途主要是在类成员函数中存取nonstatic members,但静态成员函数中没有非静态成员,因此this指针也就没有了
3.不能被声明为const、virtual、volatile
4.不需要经由class调用
PS:若取一个静态成员函数的地址,获得的是其内存中的位置,地址类型是一个nonmember函数指针,而不是指向类成员函数的指针。
4.2 虚拟成员函数
ptr->z(); //z为虚函数
针对上面的这行代码,当我们在调用时,我们需要在执行期得到一些相关信息,比如ptr的类型等,这样才能确定到底是用基类还是派生类的函数z(),如果想解决这个问题,我们最先能想到的可能是将这些信息加到指针ptr身上。
所以,一个指针或引用要包含的信息有:
1.它所参考的对象的地址(也就是当前该指针或引用所含有的东西)
2.对象类型的某种编码,或是某个结构的地址
但是这样的解决方法存在两个问题:(1)明显增加了空间负担,即使程序不使用多态(2)不兼容C
那么接下来我们可以考虑下有没有更好的解决方法,比如不把这些额外信息放到指针里,还有就是如何判断哪些程序需要用到这些额外信息,针对第二个问题可以确定的是如果存在virtual function,那么就需要这些额外信息。而针对第一个问题,我们可以在使用多态的class object里增加两个members:(1)一个字符串或数字,表示class类型(type_info),(2)一个指针,指向某表格,表格中带有虚函数的执行期地址(这个地址是固定不变的,执行期不可能新增或替换)。
那么接下来要解决的就是如何找到虚函数的地址,我们可以这样:
1.为了找到那个表格,每一个class object安插一个由编译器内部产生的指针vptr,指向那个表格
2.为了找到函数地址,每个虚函数被指派一个表格索引值
这些工作都由编译器完成,执行期要做的只是在特定的virtual table slot(记录着虚函数的地址)中激活虚函数。
一个class只会有一个虚函数表,每个table内含其对应的class object中所有active virtual function函数实体的地址,这些active virtual function包括:
1.该class所定义的函数实体
2.继承自base class的函数实体
3.pure_virtual_called()函数实体,既可以当做纯虚函数的空间保卫者,也可以当做执行期异常处理函数。
下面我们来用一个实例具体说明虚函数的布局情况:
class Point
{
public:
virtual ~Point();
virtual Point& mult(float) = 0;
float x() const {return _x;}
virtual float y() const {return _y;}
virtual float z() const {return _z;}
protected:
Point(float x = 0.0);
float _x;
}
virtual destructor被赋值slot 1,而mult被赋值slot 2,而mult是纯虚函数,因此slot2处为pure_virtual_called,如果该函数被意外调用,通常操作是结束该程序,y()被赋值slot 3,z被赋值slot 4。具体见下图:
当Point2d继承自该类时:
class Point2d: public Point
{
public:
Point2d(float x = 0.0, float y = 0.0)
:Point(x),y(_y) {}
~Point2d();
Point2d& mult(float);
float y() const {return _y;}
protected:
float _y;
}
可能会出现三种情况
1.继承base class所声明的虚函数的函数实体,即基类的函数实体会被拷贝到派生类的虚函数表中相对应的slot内
2.可以使用自己的函数实体,放在对应的slot内
3.加入一个新的虚函数,虚函数表尺寸增大一个slot,新函数实体的地址放进该slot内
在Point2d中,slot 1赋予destructor,slot 2为mult,取代pure_virtual_called,slot 3为y,slot 4为z。具体见下图:
当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();
Point3d& mult(float);
float z() const {return _z;}
protected:
float _z;
}
slot 1放destructor,slot 2放Point3d::mult,slot 3放继承的y, slot 4放z,具体见下图:
现在让我们回到开头的例子
ptr->z(); //z为虚函数
//转化为
(*ptr->vptr[4])(ptr);
4.2.1 多重继承下的virtual functions
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;
在上面的代码中,新得到的Derived对象的地址必须调整,以指向其Base2 subobject,编译期会产生如下代码:
Derived *temp = new Derived;
Base2 *pbase2 = temp? temp + sizeof(Base1) : 0;
如果没有这样的调整,指针的任何“非多态应用”都将失败。
当要删除pbase2所指对象时:
//必须首先调用正确的virtual destructor实体
//然后施行delete运算符
//pbase2可能需要调整,以指出完整对象的起始点
delete pbase2;
此时指针必须被再一次调整,以求再一次指向Derived对象起始处,然而上述的offset加法却不能在编译期直接设定,因为pbase2所指的真正对象只有在执行期才能确定。一般来说,经由指向“第二或后继之基类”的指针来调用析构函数。该调用操作所连带的“this指针调整”操作必须在执行期完成,即offset大小和把offset加到this指针上这个操作必须由编译器在某处插入,那么问题是在哪里插入呢?
我们的回答是用thunk法,用来(1)以适当的offset值调整this指针,(2)跳到virtual function。
thunk技术允许虚函数表slots继续内含一个简单的指针,因此多重继承不需要任何空间上的额外负担。
另外,调整this指针的第二个外负担是,由于两种不同的可能,(1)经由derived class(或第一个base class)调用,(2)经由第二个(或其后继)base class调用,同一函数在virtual table中可能需要多笔对应的slots,例如
Base1 *pbase1 = new Derived;
Base2 *pbase2 = new Derived;
delete pbase1;
delete pbase2;
虽然两个操作导致相同的destructor,但需要两个不同的virtual table slots:
1.pbase1不需要调整base指针,因为Base1是最左端基类,已经指向派生来对象的起始处,其虚函数表slots放置在真正的destructor处。
2.pbase2需要调整,其虚函数表slots需要相关的thunk地址。
在多重继承下,一个派生类内含n-1个额外的虚函数表,n表示其上一层基类的数目。对于上面的例子来说,编译器会生成两个虚函数表:
1.一个主要实体,与Base1(最左端base class)共享
2.一个次要实体,与Base2(第二个base class)有关
具体分布如图所示:
4.2.2 虚拟继承下的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虚继承自Point2d,两个对象不相符,因此两者之间的转换也需要调整this指针。布局情况如下图:
PS:建议不要在虚基类中声明非静态成员变量
4.3 函数的效能
4.4 指向member function的指针
取一个nonstatic member function的地址,如果该函数是nonvirtual,则得到的是它在内存中的真正地址,然而这个值也是不完整的,还需要绑定到某个class object中,才能够通过它调用该函数,所有的nonstatic member function都需要对象的地址(以参数this指出)。
4.4.1 支持“指向Virtual Member Functions"之指针
float (Point::*pmf)() == &Point::z; //z()是一个虚函数
Point *ptr = new Point3d;
ptr->z(); //调用的z是Point3d::z()
(ptr->pmf)(); //这里调用的是哪个z()?
如上面的例子所示,通过pmf调用的z()仍然是Point3d::z(),即虚拟机制仍然能够在使用指向member function的指针的情况下运行。那么具体是如何实现的呢?
当我们对nonstatic member function取地址时,获得的时其在内存中的地址。而当我们对虚函数取地址时,其地址在编译期是未知的,所能知道的只是虚函数在其虚函数表中的slot索引值,比如:
class Point
{
public:
virtual ~Point();
float x();
float y();
virtual float z();
}
取&Point::~Point()地址,得到的是其在虚函数表中的索引值,即1.
取x(),y()的地址得到的是函数在内存中的地址,因为它们不是virtual。
当我们通过pmf来调用z()时,会被内部转化为一个编译期的式子,如
(*ptr->vptr[(int)pmf])(ptr);
因此对于是否是virtual函数,pmf可能会有两个含义,也应该包含两个数值并能区分出代表的是内存地址还是虚函数表中的索引值。
在cfront中的处理方法:
(((int) pmf ) & ~127) ?
(*pmf)(ptr) //非虚函数
:
(*ptr->vptr[(int)pmf])(ptr));//虚函数
4.4.2 在多重继承下,指向member functions的指针
4.4.3 ”指向member functions之指针“的效率
4.5 Inline Functions
一般情况下,处理内联函数有两个阶段:
1.分析函数定义,以决定函数的”intrinsic inline ability“
2.参数求值以及临时性对象的管理
4.5.1 形式参数
在inline处理过程中,每一个形参会被对应的实参所取代,但是带来的缺点就是过多的实参求值操作,因此我们需要引入临时对象,在替换之前就完成求值操作,后面替换时直接绑定即可。例如:
inline int min(int i,int j)
{
return i<j ? i:j;
}
inline int bar()
{
int minval;
int val1 = 1024;
int val2 = 2048;
minval = min(val1,val2); //(1)
minval = min(1024,2048); //(2)
minval = min(foo(),bar()+1); //(3)
return minval;
}
下面是(1)(2)(3)在编译器内的转换过程:
//(1)参数直接代换
minval = val1<val2?val1:val2;
//(2)代换之后直接使用常量
minval = 1024;
//(3)引入临时对象,避免重复求值
int t1;
int t2;
minval = (t1 = foo()),(t2 = bar()+1, t1 < t2 ? t1:t2;
4.5.2 局部变量
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;
//局部变量作mangling操作
int _min_lv_minval;
minval = (_min_lv_minval = val1<val2 ? val1 :val2), _min_lv_minval;