前一段时间被问到过一个问题,当时模模糊糊,就是说不清楚,问题问到说:什么情况下会将基类的析构函数定义成虚函数?

  当时想到 如果子类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++多态和实现机理的更多相关文章

  1. 探索VS中C++多态实现原理

    引言 最近把<深度探索c++对象模型>读了几遍,收获甚大.明白了很多以前知其然却不知其所以然的姿势.比如构造函数与拷贝构造函数什么时候被编译器合成,虚函数.实例函数.类函数的区别等等.在此 ...

  2. c#中的多态 c#中的委托

    C#中的多态性          相信大家都对面向对象的三个特征封装.继承.多态很熟悉,每个人都能说上一两句,但是大多数都仅仅是知道这些是什么,不知道CLR内部是如何实现的,所以本篇文章主要说说多态性 ...

  3. 【转】iOS开发者账号和证书

    原文网址:http://www.jianshu.com/p/8e967c1d95c2 从Xcode7之后,苹果支持了免证书调试,但是若是需要调试推送功能,或者需要发布App,则需要使用付费的开发者账户 ...

  4. Java探索之旅(8)——继承与多态

    1父类和子类: ❶父类又称基类和超类(super class)子类又称次类和扩展类.同一个package的子类可以直接(不通过对象)访问父类中的(public,缺省,protected)数据和方法. ...

  5. 探索Python的多态是怎么实现的

    多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为. 对于其他如C++的语言,多态是通过在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来 ...

  6. php内核探索 [转]

    PHP内核探索:从SAPI接口开始 PHP内核探索:一次请求的开始与结束 PHP内核探索:一次请求生命周期 PHP内核探索:单进程SAPI生命周期 PHP内核探索:多进程/线程的SAPI生命周期 PH ...

  7. C++和java多态的区别

    C++和java多态的区别 分类: Java2015-06-04 21:38 2人阅读 评论(0) 收藏 举报  转载自:http://www.cnblogs.com/plmnko/archive ...

  8. 关于c语言模拟c++的多态

    关于c++多态,个人认为就是父类调用子类的方法,c++多态的实现主要通过虚函数实现,如果类中含有虚函数,就会出现虚函数表,具体c++多态可以参考<深度探索c++对象模型> c语言模拟多态主 ...

  9. PHP内核探索:哈希碰撞攻击是什么?

    最近哈希表碰撞攻击(Hashtable collisions as DOS attack)的话题不断被提起,各种语言纷纷中招.本文结合PHP内核源码,聊一聊这种攻击的原理及实现. 哈希表碰撞攻击的基本 ...

随机推荐

  1. Flask 学习 八 用户角色

    角色在数据库中表示 app/models.py class Role(db.Model): __tablename__='roles' id = db.Column(db.Integer,primar ...

  2. vue初尝试--新建项目

    这是一篇技术贴--如何新建一个基于vue的项目 1.下载对应版本的nodejs安装,下载的nodejs都集成了npm,所以nodejs安装完成之后npm也对应安装完成了. 安装完成之后可以在cmd命令 ...

  3. nodejs 全局变量

    1.全局对象 所有模块都可以调用 1)global:表示Node所在的全局环境,类似于浏览器中的window对象. 2)process:指向Node内置的process模块,允许开发者与当前进程互动. ...

  4. Mego(06) - 关系数据库建模

    框架中提供了多种数据注释以便可以全面的描述数据库结构特性. 自增列 可以使用注释声明指定列是数据库自增列,同时能指定自增的起始及步长. public class Blog { [Identity(, ...

  5. Linq 延迟加载

    IList<Student> ssList = new List<Student>() { , StudentName = "John", } , , St ...

  6. Web框架之Django基础篇

    Web框架之Django基础篇   本节介绍Django 简介,安装 基本配置及学习  路由(Urls).视图(Views).模板(Template).Model(ORM). 简介 Django 是一 ...

  7. Linux之Shell命令

    开始接触Linux命令行,学习Linux文件系统导航以及创建.删除.处理文件所需的命令.  注:文末有福利! 几个快捷键: Linux发行版通常使用Ctrl+Alt组合键配合F1~F7进入要使用的控制 ...

  8. 对于错误“Refused to execute script from '...' because its MIME type ('') is not executable, and strict MIME type checking is enabled.”的处理。

    今天在是用公司的报表插件Stimulsoft时发现的问题.之前可以正常使用,突然不能加载了.查看发现得到这个错误. 查看请求头 可以看到,请求正常响应,但是发现 Content-Type是空的,但是引 ...

  9. hive:数据库“行专列”操作---使用collect_set/collect_list/collect_all & row_number()over(partition by 分组字段 [order by 排序字段])

    方案一:请参考<数据库“行专列”操作---使用row_number()over(partition by 分组字段 [order by 排序字段])>,该方案是sqlserver,orac ...

  10. UVA-10037 Bridge---过河问题进阶版(贪心)

    题目链接: https://vjudge.net/problem/UVA-10037 题目大意: N个人夜里过河,总共只有一盏灯,每次最多过两个人,然后需要有人将灯送回 才能继续过人,每个人过桥都需要 ...