您的位置:首页 > 编程语言 > C语言/C++

【C++对象模型】之虚函数详解

2017-11-13 22:50 295 查看

Function语义学

Member function的各种调用方式

1. Nonstatic Member Functions

实际上member function被转换为nonmember function。C++设计准则就是:nonstatic member function至少必须和一般的nonmember function有相同的效率。

class Point3d
{
public:
void normalize() { x = x / 2;}
private:
float x;
}


两种调用方法

Point3d object;
object.normalize();

Point3d *ptr = &object;
ptr->normalize();


转换之后为了防止重名,每个编译器会产生不同的name mangling技术来生成函数名;然后增加一个参数;转换后的函数如下:

void normalize_7Point3dFv(register Point3d *const this)
{
this->x = this->x / 2;
}
// 注意这里的this前面一定是有const的,this作为一个指针是永远不希望被你修改的。
// 如果函数normalize是一个const的只读函数,那么转换后的参数为: register const Point3d *const this
// 表示不能修改Point3d的内容


调用就转换为

normalize_7Point3dFv(&object);
normalize_7Point3dFv(ptr);


是的,就是这么简单。

2.Virtual Member Function

简单来说,虚函数的调用是通过 vptr来访问虚函数表得到函数地址后再调用的。

class Point3d
{
public:
virtual void normalize() { x = x / 2;}
private:
float x;
}

Point3d object; object.normalize(); Point3d *ptr = &object; ptr->normalize();


对于
ptr->normalize()
被编译器转换成如下形式:

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


对于
object.normalize()
被编译器转换成如下形式:

(*object.vptr[1])(&obj); // 这当然可以,但是效率不高,对于具体对象来说,是不可能有多态的
// 所以编译器会像对待一般的nonstatic member function一样来加以决议(resolved):
normalize_7Point3dFv(&obj); //这才是真正的转换形式


3.Static Member Function

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

Point3d object;
object.normalize();

Point3d ptr;
ptr->normalize();


将被转换成一般的nonmember函数调用,像这样:

//object.normalize()
normalize_7Point3dSFv();
//ptr->normalize()
normalize_7Point3dSFv();


Virtual Member Function

先聊聊多态

定义:多态是指,通过public base class的指针或reference,寻址出一个derived class object的意思

比如:

Point *ptr;

ptr = new Point2d; //指定ptr以寻址出一个Point2d对象
ptr = new Point3d; //指定ptr以寻址出一个Point3d对象


什么样的class需要多态

欲鉴定哪些class展现多态性,我们需要额外的执行期信息。识别一个class是否支持多态,唯一适当的方法就是看看它是否有virtual function。只要class拥有virtual Function,它就需要这份额外的执行期信息。

额外保存信息有哪些

对于调用
ptr->z()
加入z()是一个Virtual function,我们需要两方面的信息才能调用正确的z()实体:(1)ptr所指对象的真实类型,这可使我们选择正确的z()实体。(2)z()实体位置,以便我们能够调用它。

1.单继承下的Virtual Member Functions

编译器为每一个class object增加一个vptr的指针,指针指向virtual table。virtual table中存储着函数实体的地址。每个class只会有一个virtual table。这一组地址是固定不变的,执行期不可能新增或者替换。所以其构建完全是在编译期完成的,执行期唯一要做的就是在特定的Virtual table slot中激活virtual function。

对于单一继承体系中,继承到底干了什么?也就是说编译器是如何组织virtual table的,如下:

class Point

class Point
{
public:
virtual ~Point();
virtual Point& mult(float) = 0; // pure virtual function
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;
}
// 一共四个虚函数,依次放到1-4slot中。对于纯虚函数,则放入pure_virtual_called(),运行期调用一般都是结束程序




Class Point2d: public Point

这里有三点需要说哈:(1)Point2d可以继承Point的virtual function的函数实例。这样该函数的地址会被拷贝到derived class的virtual table**对应**slot中。(2)Point2d可以使用自己的函数实例。新的函数地址会被填到对应slot中。(3)Point2d可以加入一个新的virtual function。这时候virtual table的尺寸会增大一个slot,对应填入函数实例的地址。

注意一点:析构函数会填入自己声明的,覆盖(override)父类的。

总体来说,如果derived class没有override base class的virtual function,就使用base class的;如果override使用自己的。

class Point2d : public Point
{
public:
Point2d(float x=0.0, float y=0.0):Point(x), _y(y){}
~Point2d();

// override base class virtual function
Point2d& mult(float);
float y() const {return _y;}
protected:
float _y;
}




class Point3d: public Point2d

同样的道理,得到Point3d对象的内存布局如下:

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 function
Point3d& mult(float);
float z() const {return _z;}
protected:
float _z;
}




以上,当遇到这样的代码时:

Point *pp;
pp->mult(0.5);


就会被编译器转换成如下代码:

(*pp->vptr[2])(pp,0.5);


也就是说,虽然pp不知道具体调用那个mult函数实体,但是其对应virtual table的slot编号始终都是2.唯一需要在执行期才能知道的东西是:slot2所指的到底是哪一个mult()函数实例!!

在一个单一的继承体系中,virtual function机制行为十分良好,不但有效率而且内存模型比较清楚。

2.多重继承下的Virtual Member 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;
};


下面我们依次分析,并依此为基础,给出多重继承条件下的virtual function对象模型。

先给出多重继承条件下virtual member function的内存模型结论,再具体分析:

在多重继承的情况下,一个derived class和最左端的base class共享virtual table。所以会多出n-1个virtual table。其中n是继承的base class的个数。对于本例而言,会有两个virtual tables:

一个主要实体,与Base1(最左端base class)共享

一个次要实体,与Base2(第二个Base class)有关

针对每一个virtual table,Derived有对应的vptrs,在constructor中设立初值。为了支持,一个class拥有多个virtual table,每一个table的名称必须不同,比如Derived的两个可能如下:

vtbl_Derived; //主要table
vtbl_Base2_Derived; //次要table


于是,当一个Derived对象地址,指定给一个Base1指针或Derived指针时,被处理的virtual table是主要table vtbl_Derived.而当你讲一个Derived对象地址,指定给一个Base2指针时,被处理的是vtbl_Base2_Derived次要table。

在次要table中,需要调整this指针的时候,调整完this指针,然后再调用主要table中的函数实体。

以上,就是多重继承下的含有虚函数的内存对象模型。



上面结论说完了,让我们根据下面这几段程序来具体分析下:

考虑如下的这些程序段:

Base2 *pbase2 = new Derived;
pbase2->data_Base2;
delete pbase2; // 要调整pbase2指向Derived对象起始处,以调用正确析构函数


Base1 *pbase1 = new Derived;
Base2 *pbase2 = new Derived;

delete pbase1; // 不用调整pbase1,因为Derived和Base1共享virtual table
delete pbase2; // 要调整pbase2指向Derived 对象起始处


Derived *pder = new Derived;
pder->mumble();//要调整pder指向Base2 subobject


Base2 *pb = new Derived;
Base2 *pbase2 = pb->clone();//要调整返回指针


必要的this指针调整

Base2 *pbase2 = new Derived;

pbase2->data_Base2;

delete pbase2;

对于
Base2 *pbase2 = new Derived;
编译器会产生出如下的代码:

Derived* temp = new Derived;
Base2 *pbase2 = temp ? temp + sizeof(Base1) : 0;


为什么会这样?首先Derived继承顺序是先Base1,Base2,所以才会需要调整指针,让pbase2指向Derived中的Base2对象。其次,如果不这样修改指针,那么对于非多态对象的调用将会出现问题,比如:

pbase2->data_Base2;
//对于data_Base2的存取,是按照Base2中相对地址来取的。所以要求pbase2必须指向一个Base2结构的起始位置。否则会出错。


对于
delete pbase2;
为了调用正确的析构函数(也就是Derived的析构函数),指针必须再一次被调整,以求再一次指向Derive对象起始处。然而这样的offset不能再编译期直接设定,因为pbase2所指的真正对象只有在执行期才能确定。

总结:

​ 以上,也就是说,经由第二或后继的base class的指针或reference来调用derived class virtual function。该操作所连带的必要的this指针调整必须在执行期完成。

​ PS:对于例子
class D : public B1, public B2, public B3 ,,,  public Bn
中B2,B3…Bn就是第二或后继的base class。也就是说,只有使用这些指针调用virtual function的时候会导致指针调整,使用B1的指针调用的时候,是不用调整的,因为Derived和B1共享virtual table(后面会看到)。

this指针调整解决办法:thunk技术

Base1 *pbase1 = new Derived;

Base2 *pbase2 = new Derived;

delete pbase1;

delete pbase2;

virtual table slot中继续内含一个简单的指针。Slots中的地址可以直接指向virtual function,也可以指向一个相关的thunk(如果需要调整this指针的话)。thunk就是一小段assembly码,完成(1)以适当offset调整this指针;(2)跳转到virtual function中。比如:

pbase2_dtor_thunk:
this += sizeof(Base1);
Derived::~Derived(this);


​ 这会带来额外的负担:多占用一些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.(class Derived: public Base1, public Base2)。所以它已经指向了Derived对象的起始处。其Virtual table slot中防止真正的destructor地址。

(2) pbase2需要调整this指针。virtual table slot中存放相关的thunk地址。

第二或后继base class对virtual function的影响

影响有三种,分别是:

通过一个指向第二个base class的指针,调用derived class 的 virtual function。此时,需要调整base class指针指向Derived对象起始处。

比如:

Base2 *ptr = new Derived;

// 调用Derived::~Derived();ptr指向Derived对象中的Base2 subobject;为了能够正确执行, ptr必须调整到Derived对象的起始处。
delete ptr;


通过一个指向Derived class的指针,调用第二个base class中一个继承而来的virtual function。此时,需要调整Derived class指针,指向第二个 base subobject;

比如:

Derived *pder = new Derived;

// mumble()是Base2中的虚函数,Derived指针要想调用它,必须先调整指针指向 Base2 subobject;
pder->mumble();


允许一个virtual function的返回值类型有所变化,可能是base type,也可能是publicly derived type。

比如:

Base2 *pb = new Derived;

// 当进行pb->clone()时,pb会被调整指向Derived对象的起始地址,于是clone()的Derived版本被调用;它传回一个指针,指向一个新的Derived对象;该对象的地址再被指定给pb2之前,必须先经过调整,以指向Base2 subobject。
Base2 *pbase2 = pb->clone();


综上:

​ 谈一点个人的理解。关于
Base2 *p = new Derived;


​ 首先,C++为了实现多态,必须使用Base的指针或引用。

​ 其次,这里的Base2和Derived会影响哪些东西那?

​ (1)Derived是new的对象,所以他会影响heap中分配的内存空间,我们可以这样想象,编译器知道Derived和Base1 Base2的继承关系,所以在编译器就已经确定了Derived的内存布局,就是我们开头处给出的图。那么再new Derived的时候,就按照这个内存布局去申请、配置、使用空间。

​ (2)编译器根据Base2只能得到一个信息:p所指向的内存空间是一个Base2对象,通过p在读取内存时,按照Base2的内存布局依次进行读取。其他的编译器什么都不知道!!那问题来了,这样的一个Base2如何去实现多态,去调用到Derived中的函数实体那?答案就是:通过设计修改Virtual table。如何设计那?就是上面我们说的这些了。大体来说就是,Derived和Base1共享Virtual table;Base2 subobject的virtual table通过thunk间接调用Derived和Base1共享的虚表。

​ 另外多说一点,为什么C++多态必须在运行时绑定,既然已经知道了各个类的继承关系,为什么不能再编译期实现那?比如:

Base *p = new Derived;
p->func(); //既然已经知道了p是指向Derived的对象,那么直接在编译期实现不就行了吗?


答案是:这个你能看出来,但是很多情况下你根本就看不出来p到底指向的是什么对象类型。比如:

// if else 或 switch case
Object* obj;
std::cin >> type;
switch(type){
case PEOPLE:
obj = new People;
break;
case MUSLIM:
obj = new Sheep;
break;
}
obj->Fucked();

//对象指针作为参数. 编译器怎么可能知道调用哪个实体,在本文件中实体根本就没有出现!
class IFuck
{
public:
virtual void Fuck() = 0;
}
void Run(IFuck* fuck)
{
fuck->Fuck();
}


3.虚继承下的Virtual Member Functions

这个没搞太清楚,先放个图吧。

注意: 不要再virtual base class中声明nonstatic data members.

class Point2d
{
public:
Point2d(float x=0.0, float y = 0.0);
virtual ~Point2d();

virtual void mumble();
virtual float z();
protected:
float _x,_y;
};

class Point3d: public virtual Point2d
{
public:
Point3d(float x=0.0, float y=0.0, float z=0.0);
~Point3d();
float z();
protected:
float _z;
};




指向Member Function的指针

对一个nonstatic member function取地址,得到的是函数在内存中的地址

面对一个Virtual function。函数地址在编译期是不知道的,所能知道的仅是virtual function在其相关之virtual table中的索引值。所以对一个virtual member function取地址,所能得到的只是一个索引值。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  c++ 对象模型 虚函数