探索C++多态和实现机理
前一段时间被问到过一个问题,当时模模糊糊,就是说不清楚,问题问到说:什么情况下会将基类的析构函数定义成虚函数?
当时想到 如果子类B继承了父类A,那么定义出一个子类对象b,析构时,调用完子类析构函数,不是自动调用父类的析构函数吗!干嘛还要把定义为虚函数。将基类析构函用到了数定义成虚函数,难道是也是为了实现多态?。。 额,现在想想,其实自己都想到多态了,可惜还是没加点劲想到点上。这个问题用到了多态的原理。首先鉴于父子类的析构函数底层其实是同名(编译器做了特殊处理,都叫destructor),然后它们又是虚函数的话,便构成重写,达到了多态的条件;而如果基类析构不是虚函数,而恰好又要delete一个指向子类的基类指针时,此时函数对象按类型调用,于是便会只调用基类析构,未调用子类析构函数而产生内存泄漏。如
- #include<iostream>
- using namespace std;
- class A
- {
- public:
- ~A()
- {
- cout<<"~A()"<<endl;
- }
- protected:
- int _a;
- };
- class B:public A
- {
- public:
- ~B()
- {
- cout<<"~B()"<<endl;
- }
- private:
- int _b;
- };
- int main(void)
- {
- A* p = new B;
- delete p;
- return ;
- }
例
按《Effective C++》中的观点其实是:只要一个类有可能会被其它类所继承, 析构函数就应该声明是虚析构函数。
那为什么定义成虚析构函数就能解决这个问题呢?
因为实现了多态。此时子类对象模型里父类析构函数被覆盖,(编译器依旧能知晓父类析构)当父类指针/引用指向父类对象时,调用的是父类的虚函数,指向子类对象时调用的是子类的虚函数;所以析构函数被定义为虚函数就不难理解了。
那多态底层又是怎么实现的呢?来探索一下。
多态底层实现
多态实现利用到了一个叫虚函数表(虚表V-table)的东西。它是一块虚函数的地址表,通过一块连续内存来存储虚函数的地址。这张表解决了继承、虚函数(重写)的问题。在有虚函数的对象实例中都存在一张虚函数表,虚函数表就像一张地图,指明了实际应该调用的虚函数函数。
Vs2008下,虚表(v-table)大致是这样,
简化后就像这样
注意 :
①每个虚表后面都有一个‘0’,它类似字符串的‘\0’,用来标识虚函数表的结尾。结束标识在不同的编译器下可能会有所不同。
②不难发现虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
多态是如何利用虚表
假使实现这样一个单继承,Derive继承Base,
- class Base
- {
- public :
- virtual void func1()
- {
- cout<<"Base::func1" <<endl;
- }
- virtual void func2()
- {
- cout<<"Base::func2" <<endl;
- }
- private :
- int a ;
- };
- class Derive : public Base
- {
- public :
- virtual void func1()
- {
- cout<<"Derive::func1" <<endl;
- }
- virtual void func3()
- {
- cout<<"Derive::func3" <<endl;
- }
- virtual void func4()
- {
- cout<<"Derive::func4" <<endl;
- }
- private :
- int b ;
- };
例2
不难发现子类对象模型中继承的基类部分存了一个虚表指针,它又指向了一个虚表,这个虚表里面的值(虚函数地址)也从父类继承过来。
但是注意几个地方
1.子类重写了的虚函数会覆盖它虚表中原来存放基类虚函数地址的值(而且我们通常也需要构成覆盖,因为没有覆盖,不实现多态,那定义出虚函数又创建出虚表就没有意义了)
2.没有被覆盖的虚函数,在虚表中保持原有状态(这个地方 Vs下监视窗口没有显示f3,f4是vs的bug)
3.同类对象的虚表指针指向同一张虚表(同类的对象,大小一样,指向同一张虚表便减少开销)
大致可以这样判断
多继承情况
像这种继承关系
- class A
- {
- public :
- virtual void f1()
- {
- cout<<"A::f1" <<endl;
- }
- virtual void func2()
- {
- cout<<"A::f2" <<endl;
- }
- protected :
- int _a ;
- };
- class B
- {
- public :
- virtual void func1()
- {
- cout<<"B::f1" <<endl;
- }
- virtual void func2()
- {
- cout<<"B::f2" <<endl;
- }
- protected :
- int _b ;
- };
- class C : public A, public B
- {
- public :
- virtual void func1()
- {
- cout<<"C::f1" <<endl;
- }
- virtual void func3()
- {
- cout<<"C::f3" <<endl;
- }
- private:
- int _c ;
- };
- void Test1 ()
- {
- C c;
- }
例3
按照前面的情况,不难得到上面的对象模型,从中注意到
①子类的虚函数c::f1()同时覆盖虚表1和虚表2中被重写的虚函数
②子类里不构成重写的虚函数的地址会按继承顺序放到第一个父类的虚表中。
这样做就为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
提到多继承就不得不想到棱形继承,那如果是棱形继承的方式,对象模型又是怎么样的呢?继续探索探索
菱形继承情况
先看看普通菱形继承(通常这种继承因冗余和二义性没有意义,但可以用它来作对比,方便理解对象模型)
- void Test1()
- {
- D d;
- d.B::_a = ;
- d.C::_a =
- d._b = ;
- d._c = ;
- d._d = ;
- }
和上面多继承有着相同特点。最终子类D::f1()覆盖了两个虚表中被重写的B::f1()和C::f1(); 并且虚表指针都在相应对象模型的前面。
现在有了普通的菱形继承对象模型。我们又知道解决菱形继承的两大缺陷会用到虚继承,于是当以虚继承方式的棱形继承的对象模型又是怎么呢,怎么完成的多态?再来探索探索。。
- class A
- {
- public:
- virtual void f1()
- {}
- virtual void f2()
- {}
- int _a;
- };
- class B : virtual public A
- {
- public:
- virtual void f1()
- {}
- //virtual void f3()
- //{}
- int _b;
- };
- class C : virtual public A
- {
- public:
- virtual void f1()
- {}
- //virtual void f3()
- //{}
- int _c;
- };
- class D : public B, public C
- {
- public:
- virtual void f1()
- {
- cout<<"D:f1()"<<endl;
- }
- virtual void f4()
- {
- cout<<"D:f4()"<<endl;
- }
- int _d;
- };
- void Test1()
- {
- D d;
- d._a = ;
- d._b = ;
- d._c = ;
- d._d = ;
- }
例5
进入调试
从图可以大致推敲出子类对象模型。现在有一个问题是,因为是虚继承,那么模型里面就会有一个虚基表指针,指向虚基表(里面存放偏移量),那模型里面虚表指针以及虚基表指针又是怎么布局呢?按前面经验,编译器为了高性能,通常把虚表指针放最前面,大小相近的应该是同一种指针,大胆猜测一下它布局
调试查看内存图,基本可以确认此种对象模型。但这是类B和类C的虚函数f1() 都被D重写的情况,当它们存在没有被子类重写的虚函数时,这些虚函数又会存在哪个虚表?
比如此时加上B::f3() C::f3()
- class A
- {
- public:
- virtual void f1()
- {}
- virtual void f2()
- {}
- int _a;
- };
- class B : virtual public A
- {
- public:
- virtual void f1()
- {}
- virtual void f3() //有一个不被重写虚函数
- {}
- int _b;
- };
- class C : virtual public A
- {
- public:
- virtual void f1()
- {}
- virtual void f3() //不被重写的虚函数
- {}
- int _c;
- };
- class D : public B, public C
- {
- public:
- virtual void f1()
- {
- cout<<"D:f1()"<<endl;
- }
- virtual void f4()
- {
- cout<<"D:f4()"<<endl;
- }
- int _d;
- };
- void Test1()
- {
- D d;
- d._a = ;
- d._b = ;
- d._c = ;
- d._d = ;
- }
例6
再查看内存图,最后大致画得如下模型图
试着从这些图中总结菱形继承时的规律:
1.有几个“含有虚表的父类”,子类就有几个虚表
2. 遵循“先继承那个父类,就把子类虚函数地址放在那个父类对应的虚表中”
3.利用虚继承方式时:把父类的虚函数指针放在一个公共区,并把公共区地址放在子类对象里的最后面。
①有几个“虚继承同一个类的”父类(如B,C),子类就有几个虚基表指针
②原始基类里虚函数会被放到公共区(最高位虚表)当中,若当中某个虚函数被重写,就将其覆盖。其子类通过虚基表里 面的偏移量来找到虚表指针(公共区),进而实现多态
③类里未参与重写的虚函数不会放到公共区,而是存在对象模型中自己相对应的那个虚表里面
探索C++多态和实现机理的更多相关文章
- 探索VS中C++多态实现原理
引言 最近把<深度探索c++对象模型>读了几遍,收获甚大.明白了很多以前知其然却不知其所以然的姿势.比如构造函数与拷贝构造函数什么时候被编译器合成,虚函数.实例函数.类函数的区别等等.在此 ...
- c#中的多态 c#中的委托
C#中的多态性 相信大家都对面向对象的三个特征封装.继承.多态很熟悉,每个人都能说上一两句,但是大多数都仅仅是知道这些是什么,不知道CLR内部是如何实现的,所以本篇文章主要说说多态性 ...
- 【转】iOS开发者账号和证书
原文网址:http://www.jianshu.com/p/8e967c1d95c2 从Xcode7之后,苹果支持了免证书调试,但是若是需要调试推送功能,或者需要发布App,则需要使用付费的开发者账户 ...
- Java探索之旅(8)——继承与多态
1父类和子类: ❶父类又称基类和超类(super class)子类又称次类和扩展类.同一个package的子类可以直接(不通过对象)访问父类中的(public,缺省,protected)数据和方法. ...
- 探索Python的多态是怎么实现的
多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为. 对于其他如C++的语言,多态是通过在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来 ...
- php内核探索 [转]
PHP内核探索:从SAPI接口开始 PHP内核探索:一次请求的开始与结束 PHP内核探索:一次请求生命周期 PHP内核探索:单进程SAPI生命周期 PHP内核探索:多进程/线程的SAPI生命周期 PH ...
- C++和java多态的区别
C++和java多态的区别 分类: Java2015-06-04 21:38 2人阅读 评论(0) 收藏 举报 转载自:http://www.cnblogs.com/plmnko/archive ...
- 关于c语言模拟c++的多态
关于c++多态,个人认为就是父类调用子类的方法,c++多态的实现主要通过虚函数实现,如果类中含有虚函数,就会出现虚函数表,具体c++多态可以参考<深度探索c++对象模型> c语言模拟多态主 ...
- PHP内核探索:哈希碰撞攻击是什么?
最近哈希表碰撞攻击(Hashtable collisions as DOS attack)的话题不断被提起,各种语言纷纷中招.本文结合PHP内核源码,聊一聊这种攻击的原理及实现. 哈希表碰撞攻击的基本 ...
随机推荐
- HTTP协议中PUT和POST使用区别
有的观点认为,应该用POST来创建一个资源,用PUT来更新一个资源:有的观点认为,应该用PUT来创建一个资源,用POST来更新一个资源:还有的观点认为可以用PUT和POST中任何一个来做创 ...
- PostgreSQL 客户端乱码问题
关于客户端和服务器端的乱码问题, POSTGRESQL字符集问题总结 总结的很详细, 特别棒. 这里让我头痛了很久的问题在于 终端 上字符编码的问题, 由于我的mbp上的 iterm2 的默认编码为 ...
- vue 手机端开发 小商铺 添加购物车 以及结算 功能
这个功能绕了我一天!!! 对 就是这个功能 一系列相关联的 四处相关联 现在加班 没时间更 过两天在更
- csrf学习笔记
CSRF全称Cross Site Request Forgery,即跨站点请求伪造.我们知道,攻击时常常伴随着各种各样的请求,而攻击的发生也是由各种请求造成的. CSRF攻击能够达到的目的是使受害者发 ...
- 使用JavaScript实现一个俄罗斯方块
清明假期期间,闲的无聊,就做了一个小游戏玩玩,目前游戏逻辑上暂未发现bug,只不过样子稍微丑了一些-.-项目地址:https://github.com/Jiasm/tetris在线Demo:http: ...
- 第一章 IDEA的使用
第一章 IDEA的使用 1.为什么要使用idea 最智能的IDE IDEA相对于eclipse来说最大的优点就是它比eclipse聪明.聪明到什么程度呢?我们先来看几个简单的例子. A.智能提示重 ...
- Java KeyTool command
Create a new key: keytool -genkey -alias keyAlias -keyalg RSA -validity 1000 -keystore d:\keyPath\k ...
- 深度爬取之rules
深度爬取之rules CrawlSpider使用rules来决定爬虫的爬取规则,并将匹配后的url请求提交给引擎.所以在正常情况下,CrawlSpider不需要单独手动返回请求了. 在rules中包含 ...
- WebApi 的三种寄宿方式 (一)
最近逛博客园,看到了Owin,学习了一下,做个笔记,说不定将来哪天就用上了 关于 Owin 的介绍,百度解释的很清楚了: https://baike.baidu.com/item/owin/28607 ...
- Linux搭建Apache+Tomcat实现负载均衡
一.首先需要安装java,详见http://www.cnblogs.com/fun0623/p/4350004.html 二.编译安装Apache,详见http://www.cnblogs.com/f ...