1. 多态的概念
多态通俗讲就是多种形态,就是指去完成某个行为,当不同对象去做时会产生不同的结果或状态。
比如买火车票这个行为,同样是买票的行为,普通成年人买到全价票,学生买到半价票,军人优先买票。这个买票就是一个多态行为,同样是买票的行为,不同人去买就会产生不同的状态。
2. 多态的定义与实现
多态是在继承了相同父类的子类对象同父类对象之间,去调用同一函数,产生了不同的行为。比如父类是普通成年人,子类是学生、军人,在同时调用买票这一函数时,参数是普通成年人就输出买全价票,参数是学生就输出买半价票,参数是军人就输出买优先票。
那么在继承中构成多态有两个必要条件:
1. 必须通过父类的指针或引用调用函数
2. 被调用的函数必须是虚函数,且子类必须有对父类的虚函数进行了重写
2.1 虚函数
虚函数就是用 virtual 修饰的函数。
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数 (及派生类虚函数与基类虚函数的返回值类型、函数名称、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
在继承中有一个类似的概念叫 隐藏 ,指子类的同名函数会隐藏父类的同名函数,但是隐藏的构成只需要函数同名即可,所以可以说重写是一种特殊的隐藏。
如果不是在两个不同但又有继承关系的类域中,函数重写的关系完全就是函数重载的关系。
事实上,构成函数重写的时候只需要在父类中声明 virtual 子类中无所谓是否声明虚函数,但为了代码的可读性建议在子类中也加上 virtual 关键字。
2.2 多态实现
可以看到通过func函数我们成功完成了多态行为的验证。
要注意必须符合多态形成的两个条件,及 虚函数重写 和 通过父类对象的引用或指针调用这个虚函数。
2.3 虚函数重写的两个例外
2.3.1 协变
基类与派生类返回值类型不同。
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时,成为协变。
这玩意儿没啥用知道有协变这么一说就好。
2.3.2 析构函数的重写
在正常情况下我们时用继承的析构函数没什么问题,但有一种特殊情况:
可以看到申请的是一块子类空间,但释放的时候只释放了父类空间。
这因为传参时的参数是父类类型的因此选择父类的析构函数,这肯定是不可取的,我们要想办法用子类的析构方式释放掉这块空间。
在C++设计的时候也意识到了这个问题,于是把这个地方设计称多态进行问题解决。
实际上析构函数在编译器中处理的时候用的不是我们现在看到的这个黄色的函数名,而是被同一成了 destructor() (这就是为了用多态的方案解决问题)。
也就是说p指针在 delete 的时候要调用一个 destruct() 函数,因为p指针类型是父类对象,同时这个要调用的 destruct() 函数又不是虚函数重写,所以自然就调用到了父类的析构函数。那么解决办法就是把析构函数进行虚函数重写。
问题解决。
说到这里就不得不提一下上节在说继承手时搓析构函数时的问题了,当时讲的是子类的析构函数中禁止调用父类的析构函数,原因不止那个要后调用父类析构,还因为,如果写了父类的析构的话,进了编译器析构函数名统一都会变成 destruct() 那在调用子类的析构函数时就进入了 destruct() 的无限递归了呀。
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名不同,但这只是表面上,在深层次中他们就是同名的,编译器对于析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成了destruct
2.4 C++11 override 和 final
从之前的学习我们已经见识到了,C++对于函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致类似函数名少写给字母的错误而无法构成重载,而这种错误在编译期间是不会爆出的,只有在程序运行时没有得到预期结果才能看出来,因此C++11提供了 override 和 final 两个关键字可以帮助用户检测是否重写。
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有就报错
final:修饰虚函数,表示该虚函数不能被重写
final还可以用来修饰类,表示这个类是最终类,无法被别的类继承
2.5 重载、重写(覆盖)、隐藏(重定义) 的对比
3. 抽象类
3.1 概念
在虚函数后面加上 =0 ,则这个函数属于纯虚函数。包含虚函数的类叫抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承了抽象类后也不能直接实例化出对象,必须进行重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
这里Car就是一个拥有纯虚函数的抽象类,无法生成对象。而Benz和BWM在继承了Car后需要重写纯虚函数才能生成对象。
在实际应用中,抽象类就是为这种情况出现的,用于描述有实际意义的抽象概念的接口。
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
我们看这段代码,结果是 A->1 还是 B->0 呢?事实上都不是,本题结果是 B->1
B类继承了A类并对其虚函数进行了重写,同时使用了A类的指针调用了这个虚函数,多态的要求满足,此时形成多态,变量实际内容是B的对象,因此看似应该调用B类中的func函数,但虚函数的继承是接口继承,也就是说继承了A类的接口,使用B类的实现。
我们再看下面两段代码:
可以看到使用虚函数之后A类的大小明显变大,这是因为每个类中的为虚函数开设有一个虚表,存放虚函数们的地址,为函数重写备用,这就是为毛不写函数重写就不要写虚函数的原因,乱写虚函数会有消耗的。
如果忘记了结构体或类中成员的内存位置配置,或者对齐数和对其规则,跳转:
C语言·自定义类型:结构体-CSDN博客文章浏览阅读959次,点赞25次,收藏22次。本节讲解了结构体的特殊声明、结构体的自引用(链表)、结构体在内存中的对齐规则、通过offsetof宏获取结构体成员的偏移量、通过#pragma修改默认对齐数、结构体传参、位段https://blog.csdn.net/atlanteep/article/details/134717687?spm=1001.2014.3001.5501
4. 多态的原理
我们用下面一段代码讲解一下多态的原理
我们结合着右边的图好讲一点,首先我们用Base类和Derive类实例化出两个对象b和d
对于 b 对象来说,它里面只会存放成员变量和虚表地址(虚函数表地址),很遗憾,b没有成员变量,所以它里面只存了虚表的地址。虚表中存放着属于 Base类 的两个虚函数地址。
对于 d 对象来说,因为它是继承了 Base类 而出生的,所以它身体中天生有属于 Base类 的内容,就是我用虚线框化出来的的地方,这块地中圈着Base类的一切能继承过来的内容,包括Base类的成员变量和Base类的虚表,而虚线框外的地方存放着属于 Derive类 自己的成员变量 。
虚表地址中存放着 Base类 虚函数的地址,和 Derive类 自己的虚函数地址,但是如果这里面有形成函数重写的两个虚函数的情况的话,比如 Func1 那么此事虚表中指向 Func1 的指针的内容就是将父类的接口融合子类的实现的这么一个函数,也就是说 Deriver类 虚表中存了3个函数指针,分别指向被重写后的虚函数Func1,虚函数Func2,虚函数Func4。
所以说重写这个操作又被叫做覆盖,就是本来虚表中存放的应该是父类的Func1,但是因为子类中重写了Func1的实现,所以就用子类的Func1的实现覆盖了父类Func1的实现。
好了,这样多态的整个原理的线索就说完了,我们把这些线索串起来:
传参时传递的是Deriver的对象d,此时虚表已经准备好了(该重写的已经重写完毕),接收参数的变量类型是父类的指针,此时发生切割,将d中属于父类的部分切割了下来(虚线框中的部分),然后用这个部分中的虚表搜索并调用了Func1,此时的Func1就是被重写了之后的Func1。
到此多态的实现原理是不是就通透了。
观察这个继承的结构其实跟内部类其实挺像的,但是继承是一种虚假的内部类,他俩只是结构上相似,使用起来完全不是一回事,注意区分,关于内部类的内容传送至:
C++语言·类和对象(下)-CSDN博客文章浏览阅读806次,点赞15次,收藏22次。本文详细阐述了C++中构造函数、初始化列表、隐式类型转换、explicit关键字、静态成员、友元、内部类和匿名对象的概念,以及编译器在某些场景下的优化策略,帮助读者掌握这些关键概念和实践技巧。https://blog.csdn.net/atlanteep/article/details/137886869?spm=1001.2014.3001.5501 虚函数表,简称虚表,根普通函数和虚函数一样存储在常量区,虚函数表指针存储在对象里。
4.1 动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如函数重载,函数模板。
2. 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。比如函重写。