目录
概念:
定义及实现:
虚函数重写的两个例外:
1.协变:
2.析构函数的重写:
final关键字:
override关键字:
多态是如何实现的(底层):
面试题:
概念:
所谓多态就是,当去完成某个行为时,不同的对象会产生不同的状态,导致不同的结果。
举个简单的例子:当去一个景区旅游,成人买票是全价,儿童买票是半价,这就是一种典型的多态。还有个典型的例子:大家在使用某多多助力拿红包时,有的能拿到,有的却邀了几百个人还是拿不到,这可能就是一种多态,如果你是新用户,它就会让你拿到,如果你是老用户,它可能让你拿不到。(仅个人猜测)
定义及实现:
c++中构成多态需要满足2个条件:
1.父类的指针或者引用去调用虚函数。
2.完成虚函数的重写,满足三同(函数名,返回值,参数)。
上述的三同有特例,后面会讲,我们先来简单实现一下上面买票这个例子的多态:
试试传子类的引用看看结果如何:
注意:虚函数不能写成全局的,只能写在类里面。虚函数和正常的成员函数一样都存在代码段。
虚函数重写的两个例外:
上面讲到了虚函数重写需要满足三同,函数名,返回值,形参都要保证和父类里面的对应虚函数相同,但也有两个例外。
1.协变:
子类重写父类虚函数时,与父类虚函数返回值类型不同。即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时,称为协变。(基类与派生类虚函数返回值类型不同)
不一定返回自己的父类或者子类,别的父类或者子类也可以:
2.析构函数的重写:
如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,
都与父类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor。
看下面这个场景:
因为有切片的概念,父类的指针可以指向子类,注意看这里调用了两次父类的析构,但我们new出来的时一个父类和一个子类,这里看似没什么影响,但我们稍微对子类修改一下 :
这时如果不调用子类的析构就会造成内存泄漏,解决办法:
结论:建议将析构函数写成虚函数,防止内存泄漏。
final关键字:
当final修饰虚函数时,则改虚函数不能被重写:
当final修饰一个类时,这个类为最终类,无法被继承:
override关键字:
加到子类重写的虚函数检查是否完成重写:
多态是如何实现的(底层):
看下面这个场景
A里面明明只有一个变量_a,虚函数func是存在代码段的,没在类里面,按理来说应该算出4字节为何这里是16字节?
答:一旦类里面有虚函数,类的头4个(32位机器)或者8个(64位机器)字节就会有一个虚表指针 ,我这里是64位机器,然后又要满足结构体对齐,所以是16,我们用监视窗口看一下:
确实有个虚表指针,它指向该类虚函数的地址:
想必已经猜出来了,虚表指针,指向的就是该类中所有虚函数的起始地址。
相同类型的类共用一个虚表,也就是虚表指针是相同的。
当我们在子类对虚函数进行重写后,子类的虚表就存的是重写后的虚函数,父类的虚表存的还是重写前的虚函数,当我们用父类或者子类的引用或者指针去访问虚表时,就会访问到不同的虚表,从而完成不同的行为,构成多态。
那要是多继承,虚表指针是怎么存的呢?
再复杂点,多继承中的菱形继承又是怎么存虚表指针的呢?
此时D中的结构就如下图:
再再再复杂一点,多继承中的菱形虚拟继承又是怎么存虚表指针的?
此时D中的结构就如下图:
虚基表指针的知识我以后会讲 。
注意:虚表是存在常量区的,不是存在类里面,只是虚表指针存在类的头4个或者8个字节。
面试题:
该程序运行的结果是什么?
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
答案:B
因为虚函数的重写,重写的只是定义,它的形参的缺省值用的还是父类里面的。