C++的多态性实现机制剖析

1. 多态性和虚函数

#include <iostream.h>
class animal
{
public:
void sleep()
{
cout<<"animal sleep"<<endl;
}
void breathe()
{
cout<<"animal breathe"<<endl;
}
};
class fish:public animal
{
public:
void breathe()
{
cout<<"fish bubble"<<endl;
}
};
void main()
{
fish fh;
animal *pAn=&fh;
pAn->breathe();
}

注意。程序中未定义虚函数。

程序执行的结果是什么?答案是输出:animal breathe

我们在main()函数中首先定义了一个fish类的对象fh。接着定义了一个指向animal类的指针变量pAn,将fh的地址赋给了指针变量pAn。然后利用该变量调用pAn->breathe()。很多学员往往将这种情况和C++的多态性搞混淆,觉得fh实际上是fish类的对象。应该是调用fish类的breathe(),输出“fish bubble”,然后结果却不是这样。以下我们从两个方面来讲述原因。

1、 编译的角度。C++编译器在编译的时候,要确定每一个对象调用的函数的地址。这称为早期绑定(early binding),当我们将fish类的对象fh的地址赋给pAn时。C++编译器进行了类型转换。此时C++编译器觉得变量pAn保存的就是animal对象的地址。

当在main()函数中执行pAn->breathe()时,调用的当然就是animal对象的breathe函数。

2、 内存模型的角度。我们给出了fish对象内存模型,例如以下图所看到的



我们构造fish类的对象时。首先要调用animal类的构造函数去构造animal类的对象,然后才调用fish类的构造函数完毕自身部分的构造,从而拼接出一个完整的fish对象。当我们将fish类的对象转换为animal类型时,该对象就被觉得是原对象整个内存模型的上半部分。也就是图1-1中的“animal的对象所占内存”。

那么当我们利用类型转换后的对象指针去调用它的方法时,当然也就是调用它所在的内存中的方法。因此。输出animal breathe,也就顺理成章了。

前面输出的结果是因为编译器在编译的时候,就已经确定了对象调用的函数的地址。要解决问题就要使用迟绑定(late binding)技术。当编译器使用迟绑定时。就会在执行时再去确定对象的类型以及正确的调用函数。而要让编译器採用迟绑定。就要在基类中声明函数时使用virtual关键字,这种函数我们称为虚函数。一旦某个函数在基类中声明为virtual,那么在全部的派生类中该函数都是virtual,而不须要再显式地声明为virtual。

#include <iostream.h>
class animal
{
public:
void sleep() { cout<<"animal sleep"<<endl;}
virtual void breathe() { cout<<"animal breathe"<<endl; }
};
class fish:public animal
{
public:
void breathe() { cout<<"fish bubble"<<endl;}
};
void main()
{
fish fh;
animal *pAn=&fh;
pAn->breathe();
}

大家可以再次执行这个程序,你会发现结果是“fish bubble”,也就是依据对象的类型调用了正确的函数。

那么当我们将breathe()声明为virtual时。在背后发生了什么呢?

编译器在编译的时候。发现animal类中有虚函数。此时编译器会为每一个包括虚函数的类创建一个虚表(即vtable),该表是一个一维数组,在这个数组中存放每一个虚函数的地址。对于例1-2的程序,animal和fish类都包括了一个虚函数breathe(),因此编译器会为这两个类都建立一个虚表,例如以下图所看到的:



那么怎样定位虚表呢?编译器另外还为每一个类的对象提供了一个虚表指针(即vptr),这个指针指向了对象所属类的虚表。在程序执行时。依据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,从而在调用虚函数时,就行找到正确的函数。对于例1-2的程序,因为pAn实际指向的对象类型是fish,因此vptr指向的fish类的vtable。当调用pAn->breathe()时,依据虚表中的函数地址找到的就是fish类的breathe()函数。

正是因为每一个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是很重要的。换句话说,在虚表指针没有正确初始化之前。我们不可以去调用虚函数。那么虚表指针在什么时候。或者说在什么地方初始化呢?

答案是在构造函数中进行虚表的创建和虚表指针的初始化

还记得构造函数的调用顺序吗。在构造子类对象时,要先调用父类的构造函数,此时编译器仅仅“看到了”父类,并不知道后面是否后还有继承者,它初始化父类对象的虚表指针。该虚表指针指向父类的虚表。

当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表

对于例2-2的程序来说,当fish类的fh对象构造完毕后,其内部的虚表指针也就被初始化为指向fish类的虚表。在类型转换后,调用pAn->breathe(),因为pAn实际指向的是fish类的对象。该对象内部的虚表指针指向的是fish类的虚表,因此终于调用的是fish类的breathe()函数。

要注意:对于虚函数调用来说,每一个对象内部都有一个虚表指针。该虚表指针被初始化为本类的虚表。所以在程序中,无论你的对象类型怎样转换,但该对象内部的虚表指针是固定的。所以呢,才干实现动态的对象函数调用,这就是C++多态性实现的原理。

总结(基类有虚函数):

1、 每一个类a都有虚表。

2、 虚表可以继承,假设子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,仅仅只是这个地址指向的是基类的虚函数实现。假设基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表。至少有三项,假设重写了对应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。假设派生类有自己的虚函数。那么虚表中就会加入该项。

3、 派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序同样。

C++——多态性实现机制的更多相关文章

  1. Java虚拟机 - 多态性实现机制

    [深入Java虚拟机]之五:多态性实现机制——静态分派与动态分派 方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际 ...

  2. JVM基础(3)-多态性实现机制

    一.方法解析 Class 文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在 Class 文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址. 因此,想要使用这些符号引用 ...

  3. 转:【深入Java虚拟机】之五:多态性实现机制——静态分派与动态分派

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/17965867   方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法 ...

  4. 深入Java虚拟机:多态性实现机制——静态分派与动态分派

    方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址.这个特性给Java带来了更强大的动态扩 ...

  5. 【深入Java虚拟机】之五:多态性实现机制——静态分派与动态分派

    方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址.这个特性给Java带来了更强大的动态扩 ...

  6. Java多态性详解 (父类引用子类对象)

    面向对象编程有三个特征,即封装.继承和多态. 封装隐藏了类的内部实现机制,从而可以在不影响使用者的前提下改变类的内部结构,同时保护了数据. 继承是为了重用父类代码,同时为实现多态性作准备.那么什么是多 ...

  7. Java多态性详解——父类引用子类对象

    来源:http://blog.csdn.net/hikvision_java_gyh/article/details/8957456 面向对象编程有三个特征,即封装.继承和多态. 封装隐藏了类的内部实 ...

  8. JAVA面向对象-多态的理解

    面向对象编程有三个特征,即封装.继承和多态. 封装隐藏了类的内部实现机制,从而可以在不影响使用者的前提下改变类的内部结构,同时保护了数据. 继承是为了重用父类代码,同时为实现多态性作准备.那么什么是多 ...

  9. Python面向对象编程(下)

    本文主要通过几个实例介绍Python面向对象编程中的封装.继承.多态三大特性. 封装性 我们还是继续来看下上文中的例子,使用Student类创建一个对象,并修改对象的属性.代码如下: #-*- cod ...

随机推荐

  1. Thinkphp的 is null 查询条件是什么,以及exp表达式如何使用

    Thinkphp的 is null 查询条件是什么,以及exp表达式如何使用 一.总结 一句话总结:$map['name'] = array('exp','is null'); 1.is null判断 ...

  2. SpringBoot与Dubbo整合-项目搭建

    本章节建立生产者和消费者来演示dubbo的demo 生产者:springboot-dubbo-provider 和 消费者:springboot-dubbo-consumer 工程配置详解 Apach ...

  3. 31.Intellij idea 的maven项目如何通过maven自动下载jar包

    转自:https://blog.csdn.net/u012851114/article/details/81872981 maven项目自动加载jar包 所需工具如下: Intellij IDEA 1 ...

  4. HDU 6153 A Secret

    A Secret Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 256000/256000 K (Java/Others)Total ...

  5. netty实现TCP长连接

    所用jar包 netty-all-4.1.30.Final.jar 密码:rzwe NettyConfig.java,存放连接的客户端 import io.netty.channel.group.Ch ...

  6. BZOJ5332: [Sdoi2018]旧试题(莫比乌斯反演)

    时光匆匆,转眼间又是一年寒暑…… 这是小 Q 同学第二次参加省队选拔赛. 今年,小 Q 痛定思痛,不再冒险偷取试题,而是通过练习旧 试题提升个人实力.可是旧试题太多了,小 Q 没日没夜地做题,却看不到 ...

  7. 【Python学习】爬虫报错处理bs4.FeatureNotFound

    [BUG回顾] 在学习Python爬虫时,运Pycharm中的文件出现了这样的报错: bs4.FeatureNotFound: Couldn’t find a tree builder with th ...

  8. Nim游戏算法实现

  9. 图片上传4-bug修复

    bug1:显示图片的时候,需要判断url是否为空 <#if photo.url != null> <img id="${photo.id}-img" src=&q ...

  10. visualSVN+花生壳实现外网访问局域网内SVN

    使用SubVersion+TortoiseSVN局域网内访问SVN成功后,想从外网访问SVN,使用花生壳绑定路由器动态DNS,但是折腾半天没搞定,突然发现一个帖子http://hi.baidu.com ...