c++编译器对多态的实现原理总结

网友投稿 409 2022-10-26


c++编译器对多态的实现原理总结

问题:定义一个空的类型,里面没有任何的成员变量或者成员函数,对这个类型进行 sizeof 运算,结果是?

继续问:如果在这个类型里添加一个构造函数和析构函数,那么结果又是多少?

还是1,因为我们调用构造函数和析构函数,只需要知道函数的地址即可,而这些函数的地址只和类型相关,和类型的实例无关,编译器不会为这两个函数在实例内添加任何额外的信息。

继续问:如果把析构函数变为虚函数呢?结果是多少?

c++编译器发现了类型里有虚函数,,就会为这个类型生成一个虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针,在32位机器,指针类型大小是4字节,结果是4,64位机器中,指针大小是8字节,结果是8。

面向对象的多态的实现效果

多态:同样的调用语句有多种不同的表现形态

看下面的代码例子:

class animal {public:    void sleep()     {         cout<<"animal sleep"<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关键字,告诉编译器这个函数要支持多态,我们不要根据指针类型判断如何调用方法,而是要根据指针所指向的实际对象类型来判断如何调用。

多态的理论基础

所谓的动态联编:根据实际的对象类型来判断重写函数的调用。

C++中多态的实现原理

如图,编译器为每个类的对象提供一个虚表指针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类的虚表。

说明:

对象在创建的时,由编译器对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"<fn();    return 0; }

在base类的构造函数中将this指针保存到pbase全局变量中。在定义全局对象aa,即调用derived aa;时,要调用基类的构造函数,先构造基类的部分,然后是子类的部分,由这两部分拼接出完整的对象aa。

这个this指针指向的当然也就是aa对象,那么我们在main()函数中利用pbase调用fn(),因为pbase实际指向的是aa对象,而aa对象内部的虚表指针指向的是自身的虚表,最终调用的当然是derived类中的fn()函数。

如果直接在基类的构造函数中调用虚函数,会怎样?

在调用基类的构造函数时,编译器只“看到了”父类,并不知道后面是否后还有继承者,它只是初始化父类对象的虚表指针,让该虚表指针指向父类的虚表,所以看到结果当然不正确。只有在子类的构造函数调用完毕后,整个虚表才构建完毕,此时才能真正应用C++的多态性。换句话说,不要在构造函数中去调用虚函数实现多态,当然如果只是想调用本类的函数,也无所谓。

得到一个结论:

构造函数中调用多态函数,不能实现多态。

虚函数和纯虚函数比较

虚函数

引入原因:为了方便使用多态特性,我们常常需要在基类中定义虚函数。

纯虚函数

引入原因:为了实现多态性,纯虚函数有点像java中的接口,自己不去实现过程,让继承他的子类去实现。在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 这时我们就将动物类定义成抽象类,也就是包含纯虚函数的类,纯虚函数就是基类只定义了函数体,没有实现过程:

virtual void Eat() = 0; 直接=0 不要 在cpp中定义就可以了

总结:

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

如果基类有虚函数:

1、每一个类都有虚表。

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

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


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Java基础之Spring5的核心之一IOC容器
下一篇:设计鲁棒性的方法:输入一个链表的头结点,逆序遍历打印该链表出来
相关文章

 发表评论

暂时没有评论,来抢沙发吧~