C++:多态

目录

多态的定义及实现:

       多态的构成条件:

         虚函数:

        虚函数重写:

        虚函数重写的两个例外:

        协变(基类与派生类虚函数返回值类型不同):

        析构函数的重写(基类与派生类析构函数的名字不同):

        C++11: override 和 final

重写,重载,重定义(隐藏)的区别:

多态的实现原理:

        虚函数表:

         虚函数表的存放位置(静态区?常量区?栈区?动态区?)

        多继承的虚函数表:

        


多态的定义及实现:

        先看代码:

C++:多态_第1张图片

        Student类继承Person类.其中都有成员函数BuyTicket(),并且成员函数都被virtual修饰,这种被virtual修饰的函数我们把它叫做虚函数.

        其中的Student我们使用了Person指针指向它,Person类同样用Person指针指向它,同样的调用BuyTicket()函数,但是确实执行各自的成员函数.

        像这样,存在继承关系的不同类,我们使用基类指针去调用他们相同的函数,产生不同的行为,我们把这种现象称为多态.

        多态也是现实世界的一种特性,就像不同的动物,面对同样的声音所作出的应激反应却不同.

       多态的构成条件:

        多态是发生在继承体系中,要构成多态,需要满足两个条件:

        1.必须通过基类的指针或者引用调用虚函数

        2.被调用的必须是虚函数,并且虚函数需要在派生类中被重写.

C++:多态_第2张图片

         虚函数:

        在类中被virtual关键字修饰的成员函数被叫做虚函数.

C++:多态_第3张图片

        虚函数重写:

        派生类中有一个与基类完全相同的虚函数,也就是三同(返回值,函数名,参数列表相同)函数,那么就称子类虚函数对基类的虚函数完成了重写.

C++:多态_第4张图片

         注意,派生类中虚函数的virtual关键字可以省略,派生类继承了基类的函数接口,让同名函数保持了虚函数的特性,但是这种写法不规范.

        虚函数重写的两个例外:

        协变(基类与派生类虚函数返回值类型不同):

        派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

C++:多态_第5张图片

        析构函数的重写(基类与派生类析构函数的名字不同):

        派生类与基类的析构函数如果被virtual修饰,那么析构函数就具有虚函数性质,二者之间构成重写

C++:多态_第6张图片

        在底层处理的时候,将析构函数名字的统一换成destructor,是为了满足虚函数的重写条件.

        如果析构函数不构成重写的话,析构函数就不会被放进虚表中,父类指针指向子类对象,系统自动析构函数调用时,只会调用父类的析构函数,就像下面,父类指针st维护的是一个子类对象,但是我们希望调用析构时,能够调用自己各自类的析构,但是确是下面的结果,Student对象也只是调用父类的析构:

C++:多态_第7张图片

        子类的成员包括父类成员和自己的成员,当我们通过重写和父类指针或引用实现多态时, 同时析构未重写,系统自动调用的将会是父类的析构,相应的系统也只会释放父类成员维护的空间,而子类成员维护的空间没有被释放,就会造成内存泄漏.

       另一个问题就是析构函数的调用顺序,还是上面的代码:

C++:多态_第8张图片

        这里可以看到,打印的第一个Person是父类对象调用析构,而子类调用析构时,不仅要调用自己的析构,而且还要调用父类的析构,遵循先子后父的顺序.

        两个方面的原因:

           在继承体系中,子类继承父类的非私有成员,子类中成员的定义顺序是根据继承顺序定义的,就像下面,Student类继承Person 和A,在其成员变量中,会将先继承的成员放到低地址处,先有Person,再有A,最后是自己的成员.C++:多态_第9张图片

         成员变量的顺序是从低地址向高地址存放,释放的时候就从高地址向低地址释放,有点像栈,为了提高效率.

        第二个原因是,考虑到子类的成员变量有可能在使用父类维护的空间,如果先把父类释放掉,那么子类成员使用的父类维护的空间就是非法使用,非法使用,高危操作.

        C++11: override 和 final

        被final修饰的虚函数不能再被重写:

C++:多态_第10张图片

         override关键字帮助检查重写:

        没有override关键字的话,那上面的例子说,你的本意是期望子类BuyTicket()函数完成重写,但是因为某些原因,导致或者函数名,或者参数列表,返回值与父类的虚函数不一致没有完成重写,那么编译器在编译期间或者把这个函数当成一个重载,或者当作一个新的函数,编译器不会报错,直到运行期间发现得到的结果不符合预期,你去寻找bug的时候才意识到这里的错误.

        override修饰以后,它告诉编译器,我是一个重写函数,你去我的父类找我爸爸吧,帮我确定一下我的返回值,函数名和参数列表跟我爸的一样,所以override关键字的作用就是帮助我们检查重写.

        同时还可以提高代码的可读性

重写,重载,重定义(隐藏)的区别:

C++:多态_第11张图片

   首先先说重写与重定义:

        重写与重定义是发生在继承体系中的,当子类继承了父类,子类成员与父类成员有着各自独立的作用域,你通过子类调用一个成员函数,编译器会首先在子类的类域中寻找该函数,如果没有,则到父类的类域中去寻找该函数,那么又会有这样一种情况发生,子类中的函数与父类中的函数存在同名函数,并且参数列表也相同,同时不是虚函数,这个时候,我们就说二者之间构成了重定义,重定义又叫隐藏.

        这个时候你该说了,那这不就是重载吗?二者最主要的一个区别就在于同名函数所在的作用域.就像是两个长的很像的人A,B,如果AB出生在同一个家庭,那么AB就是双胞胎(重载),如果AB不再同一个家庭,只能说明二者只是巧合,就像这里的重定义.

        我们在回来说重写,重写要求子类与父类的函数签名相同(返回值,参数,函数名),并且要被virtual关键字修饰.

        其实如果你最先了解到的是函数重载,你会感觉三者在形式上类似,但是在其背后有着区别,重定义与重载区别是两个同名函数所在作用域不同,并且重定义发生在继承体系中,重定义与重写的区别在于,父类中的同名函数多了virtual关键字.重载与重写,重写不仅同名函数发生的作用域不同,一个在父类域,一个在子类域,并且重写的函数要被virtual修饰.

多态的实现原理:

        先看一段代码:

C++:多态_第12张图片

         在x86平台下计算结果是8,根据结构体的存储规则,一个整型是4个字节,并且只有这一个成员变量,为什么是8呢? 我们创建一个Base对象b1来观察它的成员变量:

         这里除了自己定义的成员变量b以外,还有一个_vfptr成员,_vfptr就是能够实现多态的原理.它被叫做虚函数表.

        虚函数表:

        我们通过下面的代码验证:

C++:多态_第13张图片

         Base包括fun1,fun2两个虚函数,fun3普通成员函数和_b成员变量,Derive 继承Base,并且对Base类中的fun1进行了重写,在主函数内实例化了b和d对象.

        我们观察_vfptr的内容:

C++:多态_第14张图片

        _vfptr存放的内容其实就是地址,是函数地址,每个类都有各自的虚函数表,每个类会把自己的虚函数地址存放到自己的虚函数表中,当子类继承的时候,会将父类的虚函数表拷贝一份到自己的虚表中,如果子类重写了虚函数,就会把该函数的地址修改为重写后新函数的地址,所以重写还有一个名字叫做覆盖,他是从原理层面命名的.

        其次,当我们将一个子类对象赋值给父类对象时,这个时候会发生切片,例如将一个Derive对象赋值给Base对象,Base对象中会切割掉属于Derive不属于Base的成员变量,由于二者都有_vfptr,也就是虚表成员,所以Student的虚表不会被切割掉,但是Student中的虚表内容不会保留给父类.

        子类对象赋值给父类,子类的虚表不会保留给父类:

C++:多态_第15张图片        将一个Derive对象赋值给Base指针,Base指针中会保留Derive中的虚表:

        C++:多态_第16张图片

        我们这里直接对虚函数表中的地址进行验证,看是否真的是函数地址:        

        由于_vfptr是一个地址占四个字节,并且存放在对象空间的开始位置,我们取对象的地址,强转成int*类型,就得到了虚表的地址,但是内存会将其中的内容解读为一个整数,我们对这个地址解引用,找到虚表中的第一个元素,也就是Derive类中,Func1的地址:

C++:多态_第17张图片

           其次就是子类如果新增一个虚函数会不会放到虚函数表中?

C++:多态_第18张图片

        先查看Derive类中虚表的内容:

        C++:多态_第19张图片 

       然后观察虚表内存:

C++:多态_第20张图片

        发现这里多了第三行的一个内容,它是否是Fun3()函数的地址?我们还是取出这个地址然后调用.

        

C++:多态_第21张图片

         成功调用Derive类中新增的虚函数.

        所以,当子类新增一个虚函数时,同样会放入到虚表中,但是在监视窗口我们观察不到,同时要知道虚表是以nullptr作为结束标志.

         虚函数表的存放位置(静态区?常量区?栈区?动态区?)

        很多网上的答案是给的静态区,我们自己验证一下,具体思路就是我们分别在栈区,堆区,静态区,常量区定义一个变量,并且打印他们的地址,然后将他们的地址与虚表的地址进行比较,在同一个区域的地址相距不会太远:

        C++:多态_第22张图片

        肉眼可见,虚表存放的位置距离常量区近,所以虚表存放的位置是常量区.

        多继承的虚函数表:

        先看验证代码:

C++:多态_第23张图片

        首先Derive类继承了Base1,Base2,并且分别继承了Base1和Base2中的虚表,也就是说Derive中现在有两张虚表,Derive类中对Fun1()函数进行了重写,也就是说,重写后的Func1函数地址覆盖了两个虚表中Fun1()的地址.

        PrintfVtable()函数实现了对虚表的打印.

        并且Derive类新增了一个虚函数,在visual studio中,编译器将新增的虚函数放在了第一个虚表中的最后.

        但是需要注意的是,虚表1和虚表2的Fun1()函数地址不同,但是他们的调用结果却是同一个函数.

        下面解释一下,为什么多继承中,子类覆盖父类各自的虚表的结果不同:

C++:多态_第24张图片

         Base1和Base2都作为Derive 的父类,也就是说,不论是我通过Base1指针还是Base2指针,都能够实现多态,但是还有一个点,关于this指针的问题,

C++:多态_第25张图片

        不要忘记Func1()是一个成员函数,他还有一个隐藏的this形参,我们通过不同的父类指针调用Func1()传入的this指针是不一样的:1

C++:多态_第26张图片

         Func1()是Derive类的成员函数,this指针当然是需要指向Derive对象的首地址,Base1刚好指向了Derive类的首地址,如果我们通过Base2指针调用Func1()函数,传入的就不是在是Derive类的首地址,所以,在Base2的虚表中.Func1()的地址指向的指令处就是对Base2指针进行偏移的指令,指针偏移后,再正常调用Func1()函数,我们可以通过反汇编代码观察:

 C++:多态_第27张图片

       放大看吧 (T_T)

        在call eax之前的指令都是一样的,从虚表中读取Func1()函数的地址,到call eax时,我们进行跳转,注意每张图的黄色小箭头是当前所在的指令.

        可以看到,Base1调用func1()函数,直接jmp到对象的函数处.

        Base2则是jmp到一个sub ecx 8的指令处,这个指令就是对Base2的指针进行调整,调整完后直接调用的Func1()函数,也就是说,Base2调用虚函数相当于将该虚函数封装了一层.

        

        

               

        

        

        

        

        

            

              

你可能感兴趣的:(c++,开发语言)