c++编译器对多态的实现原理总结
问题:定义一个空的类型,里面没有任何的成员变量或者成员函数,对这个类型进行 sizeof 运算,结果是?
结果是1,因为空类型的实例不包含任何信息,按道理 sizeof 计算之后结果是0,但是在声明任何类型的实例的时候,必须在内存占有一定的空间,否则无法使用这些实例,至于占据多少内存大小,由编译器决定。
继续问:如果在这个类型里添加一个构造函数和析构函数,那么结果又是多少?
还是1,因为我们调用构造函数和析构函数,只需要知道函数的地址即可,而这些函数的地址只和类型相关,和类型的实例无关,编译器不会为这两个函数在实例内添加任何额外的信息。
继续问:如果把析构函数变为虚函数呢?结果是多少?
c++编译器发现了类型里有虚函数,,就会为这个类型生成一个虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针,在32位机器,指针类型大小是4字节,结果是4,64位机器中,指针大小是8字节,结果是8。
面向对象的多态的实现效果
多态:同样的调用语句有多种不同的表现形态
看下面的代码例子:
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;
}
}; int main(void)
{
fish fh;
animal *pAn=&fh;
pAn->breathe();
return ;
}
父类指针指向了子类对象,调用了 breathe 方法,那么结果是animal breathe,也就是说调用的是父类的breathe方法。 这没有实现多态性。因为C++编译器在编译的时候,要确定每个对象调用的函数的地址,这称为早期绑定(early binding),当fish类的对象fh的地址赋给父类的pAn指针时,C++编译器进行了类型转换,它认为父类的指针变量pAn保存的就是animal对象的地址。当在main()函数中执行pAn->breathe()时,调用的就是animal对象的breathe函数。
进一步说:
在我们构造fish类的对象时,首先要调用父类:animal类的构造函数去构造animal类的对象,然后才调用fish类的构造函数完成自身部分的构造,从而拼接出一个完整的fish对象。当将fish类的对象转换为animal类型时,该对象就被认为是原对象整个内存模型的上半部分,也就是图中的“animal的对象所占内存”。
那么当利用类型转换后的对象指针去调用它的方法时,当然也就是调用它所在的内存中的方法。因此,输出animal breathe。这不是多态的表现形式。
多态实现的三个条件
必要的前提是必须有继承关系、然后我们需要父类指针(引用)去调用子类的对象,且关键是:子类有对父类的虚函数的重写。virtual关键字,告诉编译器这个函数要支持多态,我们不要根据指针类型判断如何调用方法,而是要根据指针所指向的实际对象类型来判断如何调用。
多态的理论基础
前面的例子,输出的结果是因为编译器在编译的时候,就已经确定了对象调用的函数的地址,要解决这个问题就要使用迟绑定(late binding)技术。当编译器使用迟绑定时,就会在运行时再去确定对象的类型以及正确的调用函数。而要让编译器采用迟绑定,就要在基类中声明函数时使用virtual关键字,这样的函数我们称为虚函数。一旦某个函数在基类中声明为virtual,那么在所有的派生类中该函数都是virtual,而不需要再显式地声明为virtual。
所谓的动态联编:根据实际的对象类型来判断重写函数的调用。
C++中多态的实现原理
当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储类成员函数指针的数据结构,虚函数表是由编译器自动生成与维护的,virtual成员函数会被编译器放入虚函数表中,存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)
如图,编译器为每个类的对象提供一个虚表指针vptr,这个指针指向对象所属类的虚函数表。在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,从而在调用虚函数时,就能够找到正确的函数。
在上例子中:
fish fh;
animal *pAn=&fh;
pAn->breathe();
由于父类的指针pAn实际指向的对象类型是子类的对象,因此vptr指向的子类fish 类的vtable,当调用pAn->breathe()时,根据虚表中的函数地址找到的就是fish类的breathe()函数。正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数。
那么虚表指针在什么时候,或者说在什么地方初始化呢?
c++是在构造函数中进行虚表的创建和虚表指针的初始化。
构造函数的调用顺序:在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否后还有继承者,它初始化父类对象的虚表指针vptr,该虚表指针指向父类的虚表。当执行子类的构造函数时,子类对象的虚表指针vptr被初始化, 此时 vptr指向自身的虚表。当fish类的fh对象构造完毕后,其内部的虚表指针也就被初始化为指向fish类的虚表。
在类型转换后,调用pAn->breathe(),由于pAn实际指向的是fish类的对象,该对象内部的虚表指针指向的是fish类的虚表,因此最终调用的是fish类的breathe()函数。
说明:
通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了调用的函数。在效率上,虚函数的效率要低很多。出于效率考虑,没有必要将所有成员函数都声明为虚函数
对象在创建的时,由编译器对VPTR指针进行初始化,只有当对象的构造完全结束后VPTR的指向才最终确定,到底是父类对象的VPTR指向父类虚函数表还是子类对象的VPTR指向子类虚函数表。
回到开始的问题:
class A
{
void g(){.....}
};
则sizeof(A)=;如果改为如下:
class A
{
public:
virtual void f()
{
......
}
void g(){.....}
}
则 sizeof(A)=4,这是因为在类A中存在virtual function,为了实现多态,每个含有virtual function的类中都隐式包含着一个静态虚指针vptr指向该类的静态虚表vtable, vtable中的表项指向类中的每个virtual function的入口地址。
多态是在程序进行动态绑定得以实现的,而不是编译时就确定对象的调用方法的静态绑定。
程序运行到动态绑定时,通过基类的指针所指向的对象类型,通过vptr找到其所指向的vtable,然后调用其相应的方法,即可实现多态。这就是动态绑定(dynamic binding)或者叫做迟后联编(lazy compile)。
class base; base *pbase; class base
{
public:
base()
{
pbase=this;
} virtual void fn()
{
cout<<"base"<<endl;
}
}; class derived:public base
{
void fn()
{
cout<<"derived"<<endl;
}
}; derived aa; int main(void)
{
pbase->fn();
return ;
}
在base类的构造函数中将this指针保存到pbase全局变量中。在定义全局对象aa,即调用derived aa;时,要调用基类的构造函数,先构造基类的部分,然后是子类的部分,由这两部分拼接出完整的对象aa。
这个this指针指向的当然也就是aa对象,那么我们在main()函数中利用pbase调用fn(),因为pbase实际指向的是aa对象,而aa对象内部的虚表指针指向的是自身的虚表,最终调用的当然是derived类中的fn()函数。
在derived类中声明fn()函数时,忘了加public关键字,导致声明为了private(默认为private),但通过前面我们所讲述的虚函数调用机制,也就明白了这个地方并不影响它输出正确的结果。不知道这算不算C++的一个Bug,因为虚函数的调用是在运行时确定调用哪一个函数,所以编译器在编译时,并不知道pbase指向的是aa对象,所以导致这个奇怪现象的发生。如果直接用aa对象去调用,由于对象类型是确定的(注意aa是对象变量,不是指针变量),编译器往往会采用早期绑定,在编译时确定调用的函数,于是就会发现fn()是私有的,不能直接调用。
如果直接在基类的构造函数中调用虚函数,会怎样?
在调用基类的构造函数时,编译器只“看到了”父类,并不知道后面是否后还有继承者,它只是初始化父类对象的虚表指针,让该虚表指针指向父类的虚表,所以看到结果当然不正确。只有在子类的构造函数调用完毕后,整个虚表才构建完毕,此时才能真正应用C++的多态性。换句话说,不要在构造函数中去调用虚函数实现多态,当然如果只是想调用本类的函数,也无所谓。
得到一个结论:
构造函数中调用多态函数,不能实现多态。
虚函数和纯虚函数比较
虚函数
引入原因:为了方便使用多态特性,我们常常需要在基类中定义虚函数。
纯虚函数
引入原因:为了实现多态性,纯虚函数有点像java中的接口,自己不去实现过程,让继承他的子类去实现。在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 这时我们就将动物类定义成抽象类,也就是包含纯虚函数的类,纯虚函数就是基类只定义了函数体,没有实现过程:
virtual void Eat() = ; 直接= 不要 在cpp中定义就可以了
虚函数和纯虚函数的区别
虚函数中的函数是实现的哪怕是空实现,它的作用是这个函数在子类里面可以被重载,运行时动态绑定实现动态,而纯虚函数是个接口,是个函数声明,在基类中不实现,要等到子类中去实现
虚函数在子类里可以不重载,但是虚函数必须在子类里去实现。
总结:
对于虚函数调用来,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以才能实现动态的对象函数调用,这就是C++多态性实现的原理。
如果基类有虚函数:
1、每一个类都有虚表。
2、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
3、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
欢迎关注
dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!
c++编译器对多态的实现原理总结的更多相关文章
- 多态原理探究-从C++编译器角度理解多态的实现原理
理论知识: 当类中声明虚函数时,编译器会在类中生成一个虚函数表. 虚函数表是一个存储类成员函数指针的数据结构. 虚函数表是由编译器自动生成与维护的. virtual成员函数会被编译器放入虚函数表中. ...
- 从虚拟机指令执行的角度分析JAVA中多态的实现原理
从虚拟机指令执行的角度分析JAVA中多态的实现原理 前几天突然被一个"家伙"问了几个问题,其中一个是:JAVA中的多态的实现原理是什么? 我一想,这肯定不是从语法的角度来阐释多态吧 ...
- Java技术——多态的实现原理
.方法表与方法调用 如有类定义 Person, Girl, Boy class Person { public String toString(){ return "I'm a person ...
- c++的动态绑定和静态绑定及多态的实现原理(摘)
C++多态的实现原理 为了支持c++的多态性,才用了动态绑定和静态绑定.理解它们的区别有助于更好的理解多态性,以及在编程的过程中避免犯错误. 需要理解四个名词:对象的静态类型:对象在声明时采用的类型. ...
- C++学习笔记-多态的实现原理
深入了解多态的实现原理,有助于提高对于多态的认识 多态基础 多态的实现效果 多态:同样的调用语句有多种不同的表现形态 多态实现的三个条件 有继承.有virtual重写.有父类指针(引用)指向子类对象 ...
- Java 多态的实现原理
一个对象变量可以指示多种实际类型的现象称为多态 允许不同类的对象对同一消息做出响应.方法的重载.类的覆盖正体现了多态. 1.多态的机制 1.1 本质上多态分两种 .编译时多态(又称静态多态) .运行时 ...
- c/c++ 多态的实现原理分析
多态的实现原理分析 当类里有一个函数被声明成虚函数后,创建这个类的对象的时候,就会自动加入一个__vfptr的指针, __vfptr维护虚函数列表.如果有三个虚函数,则__vfptr指向的是第一个虚函 ...
- java多态的实现原理(JVM调用过程)(综合多篇文章,参考见文末)
一个对象变量可以指示多种实际类型的现象称为多态 允许不同类的对象对同一消息做出响应.方法的重载.类的覆盖正体现了多态. 1.多态的机制 1.1 本质上多态分两种 1.编译时多态(又称静态多态) 2.运 ...
- 浅谈C++编译原理 ------ C++编译器与链接器工作原理
原文:https://blog.csdn.net/zyh821351004/article/details/46425823 第一篇: 首先是预编译,这一步可以粗略的认为只做了一件事情,那就 ...
随机推荐
- 猥琐的wordpress后门分享
https://www.t00ls.net/thread-37312-1-1.html 一个可以自动调用管理员帐号登录wordpress后台的方法. <?php require('../../. ...
- JS中的柯里化与反柯里化
先占个位 看了一天折资料,感觉清楚多了
- java类加载相关
可参考一篇文章:http://www.tuicool.com/articles/QZnENv 下面题输出结果 package com.h3c.itac; public class Dervied ex ...
- JSP实现在项目在网页上查询
<table> <caption>user</caption> <tr> <td>编号</td> <td>姓名< ...
- jQuery弹出提示信息简洁版(自动消失)
之前看了有一些现成的blockUI.Boxy.tipswindow等的jQuery弹出层插件,可是我的要求并不高,只需要在保存后弹出提示信息即可,至于复杂点的弹出层-可以编辑的,我是直接用bootst ...
- PHP如何使用GeoIP数据库
1.首先下载GeoIP的IP库.参考<利用GeoIP数据库及API进行地理定位查询>.下载后解压,得到一个GeoIP.dat文件 2.新建一个文件geoip.inc.内容为 <?ph ...
- 拒绝了对对象 'base_config' (数据库 '****',架构 'dbo')的 SELECT 权限
在网上看了很多资料都是空说一谈,都只是说很简单,然后没有说遇到这样的情况具体该怎么做,看到这里都知道是权限问题,其实我们每一个人都知道,又是我觉得我还是要给以后遇到的朋友个解决方法: 这里用到的数据 ...
- Tempdb initial size和dbcc shrinkfile
在使用sql server时您可能遇到过下面的情况,tempdb的数据文件初始大小为3mb, 随着对tempdb的使用,tempdb文件逐渐变得很大(例如30GB),导致了磁盘空间不足. 此时您需要立 ...
- 在 Windows 上安装Rabbit MQ 指南
rabbitMQ是一个在AMQP协议标准基础上完整的,可服用的企业消息系统.他遵循Mozilla Public License开源协议.采用 Erlang 实现的工业级的消息队列(MQ)服务器. Ra ...
- 微冷的雨之Java中的多线程初理解(一)
在讲解多线程前,我们必须理解什么是多线程?而且很多人都会将进程和线程做对比. 进程和线程 进程:进程是操作系统结构的基础,是一次程序的执行,是一个程序及其数据在处理机上顺序执行时所发生的活动,是程序在 ...