多态就是调用一个函数时,展现出多种形态。比如买火车票这件事,普通人是全价,学生是半价,这就是一种多态。
多态分为静态的多态和动态的多态:
静态的意思是编译时由编译器决定具体调用哪个函数,函数重载和模板就是一种静态的多态。
动态指的是运行时才确定到底调用哪个函数。
条件:要同时满足两个条件。
效果:调用函数跟对象有关,指向那个对象就调用谁的虚函数。
我们假设一个场景:Student 继承了 Person 类。Person 类对象买票全价,Student 类对象买票半价。
虚函数:被 virtual 修饰的类的成员函数称为虚函数,直接在函数的返回值类型前加上关键字 virtual 即可。
派生类中有一个跟基类完全相同的虚函数(即两个函数的返回值类型、函数名、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
【Note1】 子类中重写的虚函数可以不加 virtual 关键字
在重写基类虚函数时,派生类的虚函数不加 virtual 关键字,这样也可以构成重写,因为在基类中已经确定了该函数是虚函数,自然它的虚函数属性也被派生类继承了下来。但是该种写法不规范,不建议这样使用。
【Note2】 协变(基类与派生类虚函数返回值类型可以不同)
有个特殊的例外:派生类重写基类虚函数时,与基类虚函数返回值类型可以不同。即基类虚函数返回自己或其他基类对象的指针或者引用,派生类虚函数返回自己或其他派生类对象的指针或者引用依然可以构成多态,这种情况称为协变。
【Note3】 建议析构函数也定义成虚函数
当一个基类的指针指向 new 出来的派生类对象时,为了保证 delete 基类指针能够去调用派生类析构函数(也就是实现多态),最好把析构函数也定义为虚函数,以避免内存泄漏。
另外虽然基类与派生类析构函数名字不同,这看起来违背了重写的规则,其实不然,最终编译时编译器会对析构函数的名称做特殊处理:把所以对象的析构函数的名称统一处理成 Destructor。
在继承中要构成多态还有两个条件:
从上面可以看出,C++ 对函数重写的要求比较严格,但是有些情况下由于疏忽,比如:函数名字母次序写反,导致无法构成重写,这种错误在编译期间是不会报错的,只有在程序运行时没有得到预期结果后 debug 找出,因此:C++11提供了 override 和 final 两个关键字来帮助程序员检测是否重写正确。
override关键字
这个关键字用来修饰派生类中需要重写的虚函数,强制要求派生类去重写该虚函数。
final关键字
这个关键字用来修饰基类中不想要被子类重写的虚函数,用来限制不让子类重写该函数。
在认识抽象类之前必须先了解什么是纯虚函数。
纯虚函数:在虚函数声明的最后加上 =0 ,这个函数就叫做纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类)。
抽象类:包含纯虚函数的类叫做抽象类(也叫接口类)
抽象类特点:不能实例化出对象。
算一算 32 位平台下 sizeof(Base) 等于多少?
通过计算结果是 8 字节,除了成员变量 _a 的 4 个字节外,还有一个虚函数指针(_vfptr)也是四个字节。
说说这个虚函数指针,它指向的是一个函数指针数组
,这个函数指针数组我们叫虚函数表(简称虚表),它的元素是虚函数的地址。
那派生类也有虚表吗?有的话和基类的虚表是同一个吗?我们看看下面两个对象的数据模型:
通过上面的对象模型图,我们发现了以下几个值得注意的地方:
首先派生类对象 d 中也有一个虚表指针,d 对象由两部分构成,一部分是基类继承下来的成员(包括基类的虚表),另一部分是派生类自己的成员。
虽然说是继承了父类的虚表,但不是照搬这么简单。总结一下派生类的虚表生成过程:
a、先将基类中的虚表内容拷贝一份到派生类虚表中 。
b、如果派生类重写了基类中某个虚函数,用派生类自己的虚函数地址覆盖虚表中基类虚函数的 地址。
c、派生类自己定义的虚函数增加到派生类虚表的最后位置(上图中没有看到是vs编译器隐藏了他们)。
既然派生类虚表内容是根据基类虚表深拷贝过来的,那么基类和派生类的虚表指针(_vfptr)所存储的值也就不一样了,它们是两个不同的地址,分别指向两张虚表。
Fun3()是派生类自定义的普通函数,对于普通函数不论是派生类自定义出来的还是基类继承下来的,他都不会被放入虚表。
补充1:虚表的元素是一个个虚函数的地址,最后放空指针作为结束标志
补充2:一个类只要有虚函数就一定会有自己的虚表,包括后面它派生出来的类也会深拷贝它的虚表内容
补充3:同一个类定义出来的对象共用同一张虚表
问题1:对象中虚表指针在什么阶段初始化?虚函数又在什么阶段生成的呢?
答:虚表指针在初始化列表中初始化(由操作系统帮我们完成),虚函数在编译阶段确定地址。
问题2:虚函数放到虚表里面的,这句话对吗?
答:这句话不准确,虚表里面放的是虚函数地址,虚函数和普通函数一样,编译完成后都放在代码段。
前面说过要实现多态,有两个条件,一个子类重写基类的虚函数,另一个是要求基类的指针或引用调用虚函数 why?
问题1:为什么子类要重写基类的虚函数?
我们可以看到
问题2:为何要父类的指针或引用去调用虚函数,父类的对象不可以码?
不能,首先我们要明白子类赋值给基类的对象、指针、引用这些都叫做切片,但它们的实现原理是不同的:
前面我们一直搞的是单继承,现在我们看看多继承(不考虑菱形继承和菱形虚拟继承)
派生类自己定义的虚函数在派生类的虚表里不会显示出来,只会显示基类的,这是编译器的监视窗口故意隐藏了这两个函数
我们看下面两个类,通过监视窗口我们观察到派生类的虚表里确实重写了基类的Fun1(),但隐藏了自己定义的虚函数Fun2()。
我们可以打印虚表来验证编译器确实隐藏了派生类自己定义的虚函数。
思路:
取出b、d对象的头4bytes,就是虚表的指针,然后遍历虚函数的地址。前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
依然打印的虚函数表,看看多继承中派生类的虚函数表示什么样。
既然是多继承,那派生类应该继承了两个虚表。我们分别打印这两个虚表看看。
打印结果发现,多继承中派生类自己定义的未重写的虚函数放在第一张虚函数表中。
单继承
多继承