C++——多态性实现机制
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++——多态性实现机制的更多相关文章
- Java虚拟机 - 多态性实现机制
[深入Java虚拟机]之五:多态性实现机制——静态分派与动态分派 方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际 ...
- JVM基础(3)-多态性实现机制
一.方法解析 Class 文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在 Class 文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址. 因此,想要使用这些符号引用 ...
- 转:【深入Java虚拟机】之五:多态性实现机制——静态分派与动态分派
转载请注明出处:http://blog.csdn.net/ns_code/article/details/17965867 方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法 ...
- 深入Java虚拟机:多态性实现机制——静态分派与动态分派
方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址.这个特性给Java带来了更强大的动态扩 ...
- 【深入Java虚拟机】之五:多态性实现机制——静态分派与动态分派
方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址.这个特性给Java带来了更强大的动态扩 ...
- Java多态性详解 (父类引用子类对象)
面向对象编程有三个特征,即封装.继承和多态. 封装隐藏了类的内部实现机制,从而可以在不影响使用者的前提下改变类的内部结构,同时保护了数据. 继承是为了重用父类代码,同时为实现多态性作准备.那么什么是多 ...
- Java多态性详解——父类引用子类对象
来源:http://blog.csdn.net/hikvision_java_gyh/article/details/8957456 面向对象编程有三个特征,即封装.继承和多态. 封装隐藏了类的内部实 ...
- JAVA面向对象-多态的理解
面向对象编程有三个特征,即封装.继承和多态. 封装隐藏了类的内部实现机制,从而可以在不影响使用者的前提下改变类的内部结构,同时保护了数据. 继承是为了重用父类代码,同时为实现多态性作准备.那么什么是多 ...
- Python面向对象编程(下)
本文主要通过几个实例介绍Python面向对象编程中的封装.继承.多态三大特性. 封装性 我们还是继续来看下上文中的例子,使用Student类创建一个对象,并修改对象的属性.代码如下: #-*- cod ...
随机推荐
- Python命令行參数大全
-b : 当转换数组为字符串时提出警告.比方str(bytes_instance), str(bytearray_instance). -B : 当导入.py[co]文 ...
- jquery15 on() trigger() : 事件操作的相关方法
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content ...
- 获取session中存储的所有值的方法
记录一个获取系统中session存储的对象都有哪些的方法 HttpSession session = request.getSession(); for ( Enumeration e = sessi ...
- 2017国家集训队作业[agc004f]Namori
2017国家集训队作业[agc004f]Namori 题意: 给你一颗树或环套树,树上有\(N\)个点,有\(M\)条边.一开始,树上的点都是白色,一次操作可以选择一条端点颜色相同的边,使它的端点颜色 ...
- Linux 交换分区swap
Linux 交换分区swap 一.创建和启用swap交换区 如果你的服务器的总是报告内存不足,并且时常因为内存不足而引发服务被强制kill的话,在不增加物理内存的情况下,启用swap交换区作为虚拟内存 ...
- sleep---暂停指定的时间
sleep命令可以用来将目前动作延迟一段时间. 使用权限:所有使用者. 语法 sleep [--help] [--version] number[smhd] 参数说明: --help : 显示辅助讯息 ...
- Linux硬件信息查看
more /proc/cpuinfo more /proc/meminfo more /proc/*info lspci 查看主板信息等cat /proc/cpuinfo CPU信息cat /proc ...
- Java语言的Hook实现
引言:最近在玩完美时空的诛仙Online(不知道这里有没人有共同爱好的),这个游戏每晚七点会出现一个任务"新科试炼".这个任务简单地说就是做选择题,范围小到柴米油盐,大到世界大千, ...
- mac自己定义tree命令
编辑文件: vim ~/.bash_profile 在文件末尾追加: alias tree="find . -print | sed -e 's;[^/]*/;|____;g;s;____| ...
- 【原创】k8s源代码分析-----EndpointController
转自本人空间 http://user.qzone.qq.com/29185807/blog/1459325937 一.controller manager创建endpointController 代码 ...