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. Java程序猿的JavaScript学习笔记(6——面向对象模拟)

    计划按例如以下顺序完毕这篇笔记: Java程序猿的JavaScript学习笔记(1--理念) Java程序猿的JavaScript学习笔记(2--属性复制和继承) Java程序猿的JavaScript ...

  2. 46.Express框架 GET 方法和POST 方法

    转自:http://www.runoob.com/nodejs/nodejs-express-framework.html GET 方法 以下实例演示了在表单中通过 GET 方法提交两个参数,我们可以 ...

  3. Windows共享上网的详细设置

    作者:朱金灿 来源:http://blog.csdn.net/clever101 在Windows环境下在A和B在同一个局域网上,A机子可以上网,B机子可以通过A机子可以通过设置的网络共享来上网.其中 ...

  4. sim800c GPRS模块的透传模式

    一.透传模式 基于sim800c GPRS模块在建立TCP/IP连接情况下,可以设置进入透传模式,用来接收和发送数据,一旦进入即从串口收到的数据将被打包,然后发送.接收同理. 注意在透传模式下所有的A ...

  5. 洛谷 P1105 平台

    P1105 平台 题目描述 空间中有一些平台.给出每个平台的位置,请你计算从每一个平台的边缘落下之后会落到哪一个平台上.注意,如果某两个平台的某个两边缘横坐标相同,物体从上面那个平台落下之后将不会落在 ...

  6. JavaScript入门:003—JS中的变量

    编程语言都是同样的,JS中也是有变量的.首先JS的变量是区分大写和小写的,这个须要注意.比方number和Number是不同的变量.无论是经常使用类型的,还是对象类型,比方 Object obj和Ob ...

  7. js36---函数嵌套

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/stri ...

  8. launcher- 第三方应用图标替换

    有时候我们感觉第三方应用的icon不美观,或者跟我们主题风格不一致,这时候我们希望换成我们想要的icon,那我们可以这么做(以更换QQ应用icon为例): 1.首先我们当然要根据自己的需要做一张替换i ...

  9. Android开发经验之在图片上随意点击移动文字

    只要在图片范围之内,文字可随意点击移动. package xiaosi.GetTextImage; import android.content.Context; import android.con ...

  10. 团队作业-Beta冲刺(1)

    这个作业属于哪个课程 https://edu.cnblogs.com/campus/xnsy/SoftwareEngineeringClass2 这个作业要求在哪里 https://edu.cnblo ...