深度探索c++对象模型第四章笔记上

Function 语意学 The Semantics of Function

c++支持三种类型的成员函数(member functions):static、nonstatic和virtual,每一种类型被调用的方式都不相同。

4.1 Member的各种调用方式

Nonstatic Member Function(非静态成员函数)

假设有如下程序:

Point3d obj;
Point3d *ptr=&obj;

当调用  obj.normalize();
ptr->normalize();

Point3d
Point3d::normalize() const
{
	register float mag=magnitude();
	Point3d normal;
	normal._x=_x/mag;
	normal._y=_y/mag;
	normai._z=_z/mag;
	return normal;
}


float
Point3d::magnitude()  const
{
return sqrt(_x*_x+_y*_y+_z*_z);
}

c++的设计准则之一就是:nonstatic member function至少必须和一般nonmember function有相同的效率。也就说,如果我们要在一下两个函数之间做出选择:

float magnitude3d(const Point3d *_this){......}
float Point3d::magnitude3d() const{......}

那么选择member function 不应该带来什么额外的负担。这是因为编译器内部已将“member 函数实体”转换为对等的“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)的优化也会施行:

//使用c++伪代码
void 
normalize_7Point3dFv(register const Point3d *const this, Point &__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);
}

同样,这个函数也会被转化为:
void 
normalize_7Point3dFv(register const Point3d* const this,Point3d &__result)
{
	register float mag=this->magnitude();

	__result.Point3d::Point3d(this->_x/mag,this->_y/mag,this->_z/mag);
	return ;
}

因为这样可以节省default constructor初始化所引起的额外负担。所以会比较有效率。

名称的特殊处理方式(Name Mangling)

一般而言,member的名称前面会加上class名称,形成独一无二的命名。
这样的好处就是当出现派生操作时,就可以知道调用哪个类中的函数:

class Bar{ public: int ival;...}
其中 ival可能会变成: ival_3Bar;

当有派生操作时:
class Foo:public Bar{public : int ival;....};

所以有:
//c++ 伪代码  Foo 内部描述
class Foo
{
public:
	int ival_3Bar;
	int ival_3Foo;
	...
};

所以不管我们要处理哪一个ival,通过"name mangling",都可以绝对清楚的指出来。由于member functions 可以被重载化,所以需要更广泛的mangling手法,以提供独一无二的名称。
为了使被重载化的函数实体拥有不同的名称,为了让他们独一无二,我们可以在函数的名称中加上它们的参数链表。如果把参数类型也编码进去,就一定可以得出一个独一无二的结果,使得重载函数有良好的转换(但如果声明“extern” C,那么就会压抑nonmember functions的“magnling”结果):

class Point
{
	void x__5PointFf(float newX);
	float x__5PointFv();  //重载函数因为参数类型不同而名字不同
	...
};

把参数和函数名称编码在一起,编译器就在不同的被编译模块之间达成了一种有限形式的类型检验。

Virtual Member Functions(虚拟成员函数)

如果normalize()是一个virtual member function,那么一下的调用:

ptr->normalize();

将会被内部转化为:

(*ptr->vptr[1])(ptr);

其中:

  • 1、vptr表示由编译器产生的指针,指向virtual table。它被安插在每一个“声明由(或继承自)一个或多个virtual functions”的class objects中。事实上其名称也会被“mangled”,因为在一个复杂的class派生体系中,可能存在多个vptrs。
  • 2、1是virtual table slot的索引值,关联到normalize()函数。
  • 3、第二个ptr是表示this指针。
    同样的是,如果magnitude()是一个virtual function,它在normalize()之中的调用操作将会转换为:
register float mag=(*this->vptr[2])(this);

此时,由于Point3d::magnitude()是在Point3d::normalize()中被调用,而后者已经有虚拟机制而决议妥当,所以明确调用Point3d实体会比较有效率,并因此压制了由于虚拟机制而产生的不必要的调用操作。

//明确的调用操作(explicitly invocation)会压制虚拟机制
register float mag=Point3d::magnitude();

如果将magnitude()声明为inline函数会更有效率。

virtual function 的一个inline函数实体可以被扩展开来。

Static Member Functions(静态成员函数)

如果Point3d::normalize()是一个static member function,以下两个调用操作:

obj.normalize();
ptr->normalize();

将被转换为一般的nonmember函数调用

4.2 Virtual Member Functions(虚拟成员函数)

virtual functions的一般实现模型:每一个class有一个virtual table,内含该class之中有作用的vitual function的地址,然后每个object有一个vptr,指向virtual table的所在
为了支持virtual function机制,必须首先能够对于多态对象有某种形式的“执行期类型判断法(runtime type resolution)”。也就说,以下的调用操作将需要ptr在执行期的某些相关信息,ptr->z(); 如此一来才能找到并调用z()的适当实体,或者最直接的方法就是把必要的信息加载ptr身上。在这样的策略下,一个指针(或是一个引用)含有两项信息:

  • 1、它所参考的对象的地址(也就是当前它含有的东西);
  • 2、对象类型的某种编码,或是某个结构(内含某些信息,用以正确决议出z()函数实例)的地址。
    这样的解决方案会带来两个问题,第一,它明显增加了空间负担,即使程序并不使用多态;第二,它打断了与C程序间的链接兼容性。

首先需要知道的是,这份额外的信息是在必须支持某种形式之“执行期多态”的时候才需要的。
在c++中,多态表示“以一个public base class 的指针,寻址出一个derived class object ”的意思。指针的多态机能主要扮演一个输送机制的角色,经由它,我们可以在程序的任何地方采用一组public derived类型。这种多态形式被称为是消极的(negative),可以在编译期完成—virtual base class的情况除外。当被指出的对象真正被使用时,多态也就变成积极的了。
所以当我们想要知道哪些class会展现多态特性时,我们就需要额外的执行期信息。
但关键词class和struct并不能帮助我们了解这些信息,而c++中又没有polymorphic(多态)之类的关键词,所以识别一个class是否支持多态,唯一的方法就是看它是否有任何virtual function。只要class拥有一个virtual function,他就需要这份额外的执行期信息。
那么什么样的额外信息是需要我们存储起来的呢?假如有一个virtual function,那么什么信息才能让我们在执行期调用正确呢function呢?
假设有一个函数:

//ptr  为base 类型的指针
 ptr->z();//z()是一个virtual function
  • 1、ptr所指对象的真实类型。这可使我们选择正确地z()实体;
  • 2、z()实体位置,以便我们调用它。

所以在实现上,我们首先在每一个多态的class object身上增加两个members:

  • 1、一个字符串或数字,表示class的类型
  • 2、一个指针,指向表格,表格中带有程序的virtual functions的执行期地址
    那么表格中的virtual functions地址是如何被构建起来的呢?在c++中,virtual functions可以在编译器获知,此外,这一组地址是固定不变的,执行期不可能新增或替换之。由于程序执行时,表格的大小和内容都不会改变,所以其建构和存取都是有编译器掌握的,不需要执行期的任何介入。
    然而执行期备妥这些函数地址,只是解答一半,另一半是要找到那些地址,那么如何寻找呢?
  • 1、为了找到表格,每一个class object被安插上一个由编译器内部产生的指针,指向该表格。
  • 2、为了找到函数地址,每一个virtual function被指派一个表格索引值。
    上述的两步工作都是在编译器完成的,执行期要做的就是在特定的virtual table slot中激活virtual function。

一个class 只会有一个virtual table,每一个table内含其对应的class object中所有的active virtual functions 函数实体的地址。这些函数实体的地址包括:

  • 这个class所定义的函数实体。它会改写一个可能存在的base class virtual functions函数实体
  • 继承自base class 的函数实体,这是在derived class 决定不改写的virtual function时才会出现的情况
  • 一个pure_virtual_called()函数实体,它既可以扮演pure virtual function的空间保卫者角色,也可以当做执行期异常处理函数。
    每一个virtual function都被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的virtual function 的关联
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;
};

在这个类中,virtual desturctor 被赋值为slot1 ,而mult被赋值为slot2,此例并没有mult()的函数定义,所以pure_virtual_called()的函数地址会放在slot2中。如果该函数被意外调用,通常的操作时结束掉这个程序。y()被赋值slot3,而z()被赋值为slot4()。x()的slot是多少呢?答案是没有,这是因为x()不是virtual function。所以virtual table中没有x()的地址。

当一个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;
};

这里一共有三种可能:

  • 1、它可以继承base class所声明的virtual function的函数实体,正确地说,是该函数实体的地址会被拷贝到derived class 的virtual table相对应的slot之中
  • 2、它可以使用自己的函数实体。这表示它自己的函数实体地址必须放在对应的slot之中
  • 3、它可以加入一个新的virtual function。这时候virtual table的尺寸会增大一个slot,而新的函数实体地址会被放进该slot之中、
    所以 Point2d的virtual table在slot 1中指出destructor(因为Point 中析构函数声明为virtual),而在slot2中指出mult()(取代pure virtual function)。它自己的y()函数实体地址放在slot3,继承自Point的z()函数实体地址放在slot4中。
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,slot2 放置Point3d::mult()函数地址。slot3放置自己的z()函数地址。
所以有下图:
在这里插入图片描述

ptr->z();

如果有这样的式子,那么如何才能在编译时期设定virtual function的调用呢?

  • 一般而言,我们并不知道ptr所指对象的真正类型。然而我们知道,经由ptr可以存取到该对象的virtual table.
  • 虽然我们不知道哪一个z()函数实体会被调用,但是我们知道每一个z()函数地址都在slot4;

所以这两条信息可以让我们调用编译器将其转化为:

(*ptr->vptr[4])(ptr);

在这个转化过程中,vprt表示编译器所安插的指针,指向virtual table;4表示z()被赋值的slot编号(关联到Point体系的virtual table)。唯一一个在执行期才能知道的东西是:slot4所指的到底是哪一个z()函数实体?

在单一继承体系中,virtual function机制的行为非常良好,不但有效率而且很容易被塑造出模型。但在多重继承和虚拟继承中,对virtual functions的支持就没有那么友好了。

多重继承下的Virtual Functions

在多重继承中支持virtual functions,其复杂度围绕在第二个及后继的base classes身上,以及“必须在执行期调整this指针”这一点。

//class 体系 ,用来描述多重继承情况下支持virtual function时的复杂度
class Base1
{
public:
	Base1();
	virtual ~Base1();
	virtual void speakClearly();
	virtual Base1 *clone() const;
protected:
	float data_Base1;
};

class Base2
{
public:
	Base2();
	virtual ~Base2();
	vitual void mumble();
	virtual Base2 *clone() const;
protected:
	float data_Base2;
};

class Derived: public Base1,public Base2
{
public:
	Derived();
	vitual ~Derived();
	vitual Derived *clone() const;
protected:
	float data_Derived;
};

在这里插入图片描述
以上多重继承(Derived 支持virtual functions)的困难度,全部都是因为Base2 subobject。其中在此例中三个问题需要解决:1)virtual destructor;2)被继承下来的Base2::mumble();3)一组clone()函数实体。

当我们将Base2的指针指向Derived的对象时会发生什么呢?

Base2 *pbase2=new Derived();

首先,我们要做的就是将新的Derived对象的地址调整,以指向Derived中的Base2 subobject部分。所以编译时期会产生类似如下的代码:

Derived *temp=new Derived();
Base2 *pbase2=temp ? temp+sizeof(Base1) :0;
//将Derived中的Base2 subobject部分赋值给Base2所声明的指针。

如果没有上述的调整,那么指针的任何“非多态调用”都将会失败(比如):

pbase2->data_Base2;// 因为调整,所以不会失败。

当我们需要删除pbase2所指的对象时:

//必须首先调用正确地 virtual destructor函数实体
//然后施行delete 运算符
//pbase2 可能需要调整,以指出完整对象的起始点
delete pbase2;

指针必须被再一次调整,以求再一次指向Derived对象的起始处。
但是上面的offset加法不能再编译时期直接设定,因为pbase2所指的真正对象只有在执行期才能确定。

一般的规则是,经由指向“第二或后继之base class”的指针来调用derived class virtual function。该操作所连带的“必要的this指针调整”操作,必须在执行期完成。也就是说,offset的大小,以及把offset加到this指针上头的那一小段程序代码,必须由编译器在某个地方插入。

所以采用一种叫做thunk的方法来解决上面的问题:
1以适当的offset值调整this指针;2跳到virtual function去。

Thunk技术允许virtual table slot继续内含一个简单的指针,因此多重继承不需要任何空间上的额外负担。slot中的地址(指针)可以直接指向virtual function,也可以指向一个相关的thunk。于是,对于那些不需要调整this指针的virtual function而言,也就不需要承载效率上的额外负担。
调用this指针的第二个额外的负担就是,由于两种不同的可能:1)经由derived 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指针。其virtual table slot需放置真正的destructor地址。
2)pbase2需要调整this指针,其virtual table slot 需要相关的thunk地址。

在多重继承中,一个derived class内含n-1个额外的virtual tables,n表示其上一层base classes的数目。针对每一个virtual tables,Derived object中都会有对象的vptr。同时,vptr将会在constructor(s)中被设立初值(由编译器所产生的码值)。
用以支持“一个class拥有多个virtual tables”的传统方式是,将每一个tables以外部对象的形式产生出来,并给予独一无二的名字。(可以类似于重载函数)
在这里插入图片描述

为了调节执行期链接器的效率,某些编译器将多个virtual tables 连锁为一个:指向次要表格的指针,可由主要表格名称加上一个offset获得。所以在这样的策略下,每一个class都只有一个具名的virtual table。

有三种情况,第二个或后继的base class会影响derived class对virtual functions的支持。

  • 1、通过一个“指向第二个base class”的指针,调用derived class virtual function。
  • 2、通过一个“指向derived class”的指针,调用第二个base class中一个继承而来的virtual function。
  • 3、允许一个virtual function的返回值类型有所变化,可能是base type,也可能是publicly derived type.

虚拟继承下的virtual Functions

当一个virtual base class 从另一个virtual base class派生而来,并且两者都支持virtual functions和nonstatic data memebers时,编译器对于virtual base class 的支持就会特别繁琐。

所以,关于虚拟继承的建议就是:不要在一个virtual base class 中声明nonstatic data members.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值