c++ 多态

多态的概念

通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运行时多态(动态多态),这里我们重点讲运行时多态,编译时多态(静态多态)运行时多态(动态多态)。编译时多态(静态多态)主要就是函数重载函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态运行时归为动态

例如我们去买票,会有学生类(student)群体和普通人(person)群体,同时传给一个函数会有不同的结果,给学生类打折,给普通群体就不打折

运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。

那么下面我就讲运行时的多态,下面的多态默认就是运行时的多态:

多态的定义及实现

多态的构成条件

多态是一个继承关系的下的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象优惠买票。

实现多态还有两个必须重要条件:
1 必须指针或者引用调用虚函数(父类指针或者引用可以指向它本身或者它的任意子类形成多态的前提)
2 被调用的函数必须是虚函数。

说明:要实现多态效果,

第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向派生类对象;

第二派生类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派生类才能有不同的函数,多态的不同形态效果才能达到。

虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修
饰。

虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同,但是我们不对缺省参数做要求),称派生类的虚函数重写了基类的虚函数。

注意

1、在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。

2、重写不重写函数声明,如果调用子类的函数,那么只会看子类的函数体,不会看子类的函数声明。因此,子类的缺省变量是没用的。

请做一下下面这个题目:(这个是一个公司笔试题,大家认真对待)

以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

还有这题运行结果又是选什么呢?

第一题选的是B->1,第二题选的是B->1 B->0。

第一题和第二题有不同点在于第二题没有virtual和B多了test函数。那为什么答案就变了呢?

首先我们要知道一个重要的点,子类继承下来的父类那块使用的是父类指针。这个点我在继承的博客里面也说到过。子类里面继承父类的那一块。为了方便大家理解,我把相应的内容截图给大家:

如下:

看完之后大家应该就知道了。

那么我继续将这个题,方便大家看题,我先把第一题拿下来

那么我们先看test函数,图中B继承了A的test函数,但是这个test函数是在A域里面的。那么指针类型就是父类指针,并且这里我们可以看到func指针构成了重写。那么就形成了多态,多态重写不会重写子类的函数声明,所以两个函数的缺省参数都是val=1。我们用B*类的指针去调用test,test就会传子类被切片的A*指针,指向了B里面的func函数,那么就会有B—>val=1。B->1。

第二题:

我们再把题目拿下来看看

这里我们把virtual去掉,其实没啥变化,virtual不会影响test的调用。我们增加一个test,但是A里面还是有test,那我们只要用A里面的test,就可以用A*去调用重写的func实现多态。那么这里的p_->test()还是上面的答案B->1。这里的p->test()就是指向了B里面非A域的test函数,是无法使用到A*类的指针了,无法形成多态,那么就是简单的调用B本身的func()。就是B->0。

谐变

上面提到了构成重写要返回值相同,函数名相同,参数相同,但是这里有一个例外可以让返回类型不同而形成重写。

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

析构函数的重写

基类的析构函数要设为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写。

原因是:

上面这个就是一个多态的场景,我们用A*指针来指向父类和子类对象,那么析构的时候理应析构函数也要在传指向父类的指针时调用父类析构,传子类指针调用子类析构,但是这里是没有的。原因就是我们的析构函数没有构成多态,导致delete b是运行的A里面的切片--调用a里面的析构。

所以我们的基类析构要加上virtual,子类加上virtual更好。

这里我们析构B的时候先调用B析构再调用A析构的原因是因为我们在创建b的时候是先完成B中继承的A部分的创建,再完成B中原属于B的部分的创建,那么我们析构的时候就应该反着来。所以默认写了B的析构时,在最后面加上了~A()。

override 和 final关键字

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。

override

final

重载/重写/隐藏的对比

纯虚函数和抽象类        

在虚函数的后面写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。

多态的原理

虚函数表指针

下面编译为64位程序的运行结果是什么()
A.编译报错 B.运行报错 C. 8 D. 12 E 16  F 5


可能大家会选8字节的C,但是这里并不是。答案是E。如果选的F建议去看一看作者类的一篇博客。

原因是类里面还存放着虚函数表,除了_b和_ch成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表

多态是如何实现的        

从底层的角度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调Person::BuyTicket,ptr指向Student对象调用Student::BuyTicket的呢?通过下图我们可以看到,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。第一张图,ptr指向的Person对象,调用的是Person的虚函数;第二张图,ptr指向的Student对象,调用的是Student的虚函数。

所以原理就是通过不同的指针类型调用来指向不同的虚函数表里面的地址。运行不同的函数。

虚函数表的构成

那么虚函数表是如何构成的呢?

编译器是怎么将基类和派生类的函数统一到一个虚函数表的:

1、基类对象的虚函数表中存放基类所有虚函数的地址。


2、派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。

     如图:用A实例化a,B实例化b

我们可以看到父子类虚函数表指针存的地址是相同的。


3、派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。

      这里就是第二步,第一部就是父子类兼用地址相同的函数,然后在将子类重写的函数地址覆盖到原函数地址上:

如图:我们重写func,发现test函数地址没变,但是func地址变了。

4、派生类的虚函数表中包含,基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址三个部分。
5、虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000标记,g++系列编译不会放)


6、虚函数存在哪的?虚函数和普通函数一样的,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。

7、虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下面的代码可以对比验证一下。vs下是存在代码段(常量区)

更靠近代码段(常量区)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值