【C++对象模型】之虚函数详解
来源:互联网 发布:数据质量提升方案 编辑:程序博客网 时间:2024/06/11 01:12
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; //主要tablevtbl_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 tabledelete 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 caseObject* 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++对象模型之:理解虚函数机制
- C++对象模型之虚函数实现原理
- 【C++】深度探索C++对象模型之虚拟成员函数(virtual member function)
- c++对象模型笔记之构造函数
- C++对象模型之构造函数
- C++ - 对象模型之 成员函数调用
- C++对象模型之拷贝构造函数
- thinkphp5 数据库和模型详解 之3 模型和对象
- C之system函数详解
- 深入探索C++对象象模型--拷贝构造函数、对象模型
- C++对象模型详解
- C++对象模型详解
- 虚函数详解(C++)
- C++虚函数对象模型剖析
- C++ 虚函数的对象模型
- Javaweb-xml编程-sax解析与实例
- 2017-11-13 每周小结(编码,注解,反射)
- poj 1364 King(hud 1531)
- js/jQuery 跨iframe操作
- 自定义View的自定义属性,TypedArray的使用和命名空间
- 【C++对象模型】之虚函数详解
- VINS_Mono,OpenCV Error: Bad argument (Invalid pointer to file storage) in cvGetFileNodeByName问题终于解决了
- 缓存架构设计细节二三事
- 小结
- IntelliJ IDEA编译环境编写JSP文件报错且没有代码提示,还能正常运行
- Java进阶代码
- 生产者消费者模式实现
- Java学习——Character 类
- Haar Adaboost 视频车辆检测代码和样本