【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++对象模型之内联函数
- 图说C++对象模型:对象内存布局详解
- ★★★★★图说C++对象模型:对象内存布局详解
- 深度搜索C++对象模型 Function 语意学-虚拟成员函数
- [C++对象模型][5]堆栈与函数调用
- C++对象模型的那些事儿之六:成员函数调用方式
- 【C++对象模型】函数返回C++对象的问题
- C++对象模型之成员函数的指针
- 深入探索C++对象模型之指向成员函数的指针
- 深度探索C++对象模型之:成员函数语义学--静态成员函数
- c++继承详解之一——继承的三种方式、派生类的对象模型
- 【C++】深度探索C++对象模型之虚拟成员函数(virtual member function)
- 图说C++对象模型:对象内存布局详解
- C++对象模型——函数的效能(第四章)
- 【转】图说C++对象模型:对象内存布局详解
- 图说C++对象模型:对象内存布局详解
- 图说C++对象模型:对象内存布局详解
- 深入探索C++对象模型--详解深拷贝与浅拷贝以及拷贝构造的建构
- 【深度探索c++对象模型】Function语义学之成员函数调用方式
- 三十二、C++内存布局,对象大小计算、虚函数虚继承对类内存模型的影响