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. 关于cocos2dx之lua使用TableView

    在手机游戏的开发中,滚动是一项很重要的操作,而cocos2dx中使用的最广泛的就属于TableView了,只是由于cocos2dx的接口比較晦涩,所以须要一个熟悉的过程.本文主要解说怎样使用Table ...

  2. UICollectionView——整体总结

    前言 这几天有时间看了下UICollectionView的东西,才发觉它真的非常强大,很有必要好好学习学习.以前虽然用过几次,但没有系统的整理总结过.这两天我为UICollectionView做一个比 ...

  3. 使用Java语言开发微信公众平台(三)

            在上一节课程中,我们来学习了微信公众平台最基础的一个接口——access_token,并且能够从微信公众平台中取到access_token. 那么,在本节课程中,我们要以上节课获取到的 ...

  4. AIX lsof 命令

    1.查看某端口运行情况 如查看22端口运行情况 # lsof –i:22 # lsof –i:22 –r   ----每隔15秒显示22端口的监听情况.   2.查看活动的连接 如:查看ip地址为19 ...

  5. ES6--基础语法(一)

    一.支持环境:node.js完全支持,标准浏览器完全支持.二.测试环境: chrome下需要在script标签的最先开始的地方需要添加"use strict". firefox下需 ...

  6. 使用PHP中的curl发送请求

    使用CURL发送请求的基本流程 使用CURL的PHP扩展完成一个HTTP请求的发送一般有以下几个步骤: 初始化连接句柄: 设置CURL选项: 执行并获取结果: 释放VURL连接句柄. 下面的程序片段是 ...

  7. buffer--cache 详解

  8. 编程精粹--编写高质量C语言代码(4):为子系统设防(一)

    通常,子系统都要对事实上现细节进行隐藏,在进行细节隐藏的同一时候.子系统为用户提供了一些关键入口点. 程序猿通过调用这些关键的入口点来实现与子系统的通信.因此假设在程序中使用这种子系统而且在其调用点加 ...

  9. 如何使用Linux套接字?

          我们知道许多应用程序,例如E-mail.Web和即时通信都依靠网络才能实现.这些应用程序中的每一个都依赖一种特定的网络协议,但每个协议都使用相同的常规网络传输方法.许多人都没有意识到网络协 ...

  10. java匿名内部类使用场景列举

    示例一: package com;      interface Operation {       double operateTwoIntNum(int a, int b);   }      p ...