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 0;
}

父类指针指向了子类对象,调用了 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)=1;如果改为如下: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 0;
}

在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() = 0; 直接=0 不要 在cpp中定义就可以了

虚函数和纯虚函数的区别
虚函数中的函数是实现的哪怕是空实现,它的作用是这个函数在子类里面可以被重载,运行时动态绑定实现动态,而纯虚函数是个接口,是个函数声明,在基类中不实现,要等到子类中去实现
虚函数在子类里可以不重载,但是虚函数必须在子类里去实现。

总结:

对于虚函数调用来,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以才能实现动态的对象函数调用,这就是C++多态性实现的原理。

如果基类有虚函数:

1、每一个类都有虚表。

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

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

时间: 2024-12-30 00:01:24

c++编译器对多态的实现原理总结的相关文章

多态原理探究-从C++编译器角度理解多态的实现原理

理论知识: 当类中声明虚函数时,编译器会在类中生成一个虚函数表. 虚函数表是一个存储类成员函数指针的数据结构. 虚函数表是由编译器自动生成与维护的. virtual成员函数会被编译器放入虚函数表中. 当存在虚函数时,每个对象中都有一个指向虚函数表的指针(C++编译器给父类对象.子类对象提前布局vptr指针:当进行howToPrint(Parent *base)函数是,C++编译器不需要区分子类对象或者父类对象,只需要再base指针中,找vptr指针即可.). VPTR一般作为类对象的第一个成员.

C++ 多态的实现原理与内存模型

多态在C++中是一个重要的概念,通过虚函数机制实现了在程序运行时根据调用对象来判断具体调用哪一个函数. 具体来说就是:父类类别的指针(或者引用)指向其子类的实例,然后通过父类的指针(或者引用)调用实际子类的成员函数.在每个包含有虚函数的类的对象的最前面(是指这个对象对象内存布局的最前面)都有一个称之为虚函数指针(vptr)的东西指向虚函数表(vtbl),这个虚函数表(这里仅讨论最简单的单一继承的情况,若果是多重继承,可能存在多个虚函数表)里面存放了这个类里面所有虚函数的指针,当我们要调用里面的函

从虚拟机指令执行的角度分析JAVA中多态的实现原理

从虚拟机指令执行的角度分析JAVA中多态的实现原理 前几天突然被一个"家伙"问了几个问题,其中一个是:JAVA中的多态的实现原理是什么? 我一想,这肯定不是从语法的角度来阐释多态吧,隐隐约约地记得是与Class文件格式中的方法表有关,但是不知道虚拟机在执行的时候,是如何选择正确的方法来执行的了.so,趁着周末,把压箱底的<深入理解Java虚拟机>拿出来,重新看了下第6.7.8章中的内容,梳理一下:从我们用开发工具(Intellij 或者Eclipse)写的 .java 源程

Java多态的底层原理

作为一门面向对象语言,Java 拥有封装.继承.多态三大特性.多态就是允许不同类的对象对同一消息做出响应.基于多态,可以消除一些类型耦合关系,实现可替换.可扩充.Java 中使用多态特性的方法主要有,实现一个接口,实现抽象类的一个方法,覆盖父类的一个方法. Java多态的底层原理 多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来.动态绑定涉及很多 JVM 的细节,全部写清楚需要很大篇幅,因此本文仅简单描述,后续会有其他文章就其中的细节一一分享. 静态绑定与动态绑定 JVM 的方

浅谈C++编译原理 ------ C++编译器与链接器工作原理

原文:https://blog.csdn.net/zyh821351004/article/details/46425823 第一篇:     首先是预编译,这一步可以粗略的认为只做了一件事情,那就是“宏展开”,也就是对那些#***的命令的一种展开. 例如define MAX 1000就是建立起MAX和1000之间的对等关系,好在编译阶段进行替换. 例如ifdef/ifndef就是从一个文件中有选择性的挑出一些符合条件的代码来交给下一步的编译阶段来处理.这里面最复杂的莫过于include了,其实

C++中多态的实现原理

1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数. 2. 存在虚函数的类都有一个一维的虚函数表叫做虚表.类的对象有一个指向虚表开始的虚指针.虚表是和类对应的,虚表指针是和对象对应的. 3. 多态性是一个接口多种实现,是面向对象的核心.分为类的多态性和函数的多态性. 4. 多态用虚函数来实现,结合动态绑定. 5. 纯虚函数是虚函数再加上= 0. 6. 抽象类是指包括至少一个纯虚函数的类. 纯虚函数:virtual void breathe()= 0:即抽象类!必须在子类实

C++学习研究之虚函数、多态的实现原理

1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数.2. 存在虚函数的类都有一个一维的虚函数表叫做虚表.类的对象有一个指向虚表开始的虚指针.虚表是和类对应的,虚表指针是和对象对应的.3. 多态性是一个接口多种实现,是面向对象的核心.分为类的多态性和函数的多态性.4. 多态用虚函数来实现,结合动态绑定.5. 纯虚函数是虚函数再加上= 0.6. 抽象类是指包括至少一个纯虚函数的类. 纯虚函数:virtual void breathe()=0:即抽象类!必须在子类实现这个函数!

C++编译器与链接器工作原理

http://blog.csdn.net/success041000/article/details/6714195 1. 几个概念 1)编译:把源文件中的源代码翻译成机器语言,保存到目标文件中.如果编译通过,就会把CPP转换成OBJ文件. 2)编译单元:根据C++标准,每一个CPP文件就是一个编译单元.每个编译单元之间是相互独立并且互相不可知. 3)目标文件:编译所生成的文件,以机器码的形式包含了编译单元里所有的代码和数据. 还有一些其他信息,如未解决符号表,导出符号表和地址重定向表等.目标文

C++之编译器与链接器工作原理

http://www.cnblogs.com/kunhu/p/3629636.html 原文来自:http://blog.sina.com.cn/s/blog_5f8817250100i3oz.html 这里并没不是讨论大学课程中所学的<编译原理>,只是写一些我自己对C++编译器及链接器的工作原理的理解和看法吧,以我的水平,还达不到讲解编译原理(这个很复杂,大学时几乎没学明白). 要明白的几个概念: 1.编译:编译器对源文件进行编译,就是把源文件中的文本形式存在的源代码翻译成机器语言形式的目标