目录
注意子类不写virtual父类加上了virtual也可也进行虚函数的重写,但是不太建议
概念:
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。
举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人 买票时是优先买票。 再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的 活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5 毛....。其实这背后也是一个多态行为。
多态产生的条件:
- 父子类之间完成虚函数的重写
- 父类的指针或者引用去调用虚函数,且子类必须对父类的虚函数进行重写操作
虚函数的重写:
父类要有虚函数,子类也需要有虚函数,且父子类虚函数是同一个函数名,统一的参数(参数的缺省值可以不同),父类和子类的虚函数返回值是要一致的。
而父类指针或者引用调用虚函数,就如下图所示:
虚函数:即被virtual修饰的类成员函数称为虚函数
- 尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态
- 同时静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名:.成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数
虚函数重写的两个例外:
协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
协变就是父子类的虚函数返回值不同,不过返回值必须是父子类关系的指针或者引用
如图所示,类A是类B的父类,而person中的虚函数使用了返回值类型是A*而他的子类student的虚函数使用的返回值是B* 刚好AB是父子关系,所以这就变成了协变
当然这个父子类指针是可以指其他类型的父子类,也可也是自己的父子类。
析构函数
父类和子类的虚函数的析构函数函数名并不相同,但是因为析构函数在多态中会被编译器在暗地里变成同一个名字。
为什么会变成同一个名字?因为析构函数会因为父子类关系,在子类调用析构后会自动调用父类,但是:
person是一个类指针,既可以指向自己也就是父类,又可以指向子类,这是因为切片的概念(这里的父子类没有变成多态关系) ,而在使用delete调用析构时出了问题!没有调用子类的析构,即使我们指向了子类。
而为什么没有调用到子类呢?
- 因为delete的内部构成是:析构函数和operator delete()
- 而operator delete 有一个特点,那就是调用delete 的指针是什么类型的,就会调用什么类型的析构函数,而这里的两个指针都是父类person 所以就都调用了父类person的析构函数,所以没有调动子类的析构函数
所以这样子写就会有一种后果:
在子类的析构函数还有东西,例如一个新的空间,所以如果只调用了父类,那么子类内部构建的东西就会变成内存泄露,内部有空间没有释放!
所以想要指向父类调用父类析构,指向子类调用子类析构,在希望出现这种情况时,就需要把析构函数的名字进行了多态暗地里的统一,变成了destructor,所以加上了虚函数加上destructor就变成了重写。
++注意子类不写virtual父类加上了virtual也可也进行虚函数的重写,但是不太建议++
重写、重定义、重载区别
继承体系区分:
- 重写确实发生在继承体系中,子类重写父类的虚函数。
- 重定义通常指的是在派生类中定义一个与基类同名的成员,这可以发生在继承体系中,但它与重写不同,因为重写特指虚函数的覆盖。
- 重载是发生在同一个类中的,它指的是在同一个作用域内使用相同的函数名但参数列表不同的函数。重载与继承体系无关,重载只能在一个范围内,不能在不同的类里
原型的区分:
- 重载要求函数名相同但参数列表不同。
- 重写要求函数名和参数列表都与基类中被重写的虚函数相同。
- 重定义的原型可能与基类中的成员不同,但这并不是关键区别点。
多继承中的指针偏移问题:
答案选C,p1和p3指向的位置是相等的,但只是巧合,这里根据的是切片原理,p1只是指向第一个方形区域,p2指向的是第二个方形区域,而p3是指向两个方形区域外面的大方形区域!
虚表与虚表指针:
虚表指针概念:
- 虚表指针(vptr)并不是每个虚函数一个虚表指针。
- 实际上,每个包含虚函数的类 的 对象 都有一个虚表指针(vptr)而不是每个虚函数一个。
- 这个虚表指针指向一个虚函数表(vtable),虚函数表中存放了该类所有虚函数的地址。
- 当子类继承父类并覆盖父类的虚函数时,子类的虚函教表会包含父类的虚函数地址(如果子类没有覆盖这些函数)以及子类新增或覆盖的虚函数地址。
指向谁调用谁:
- 父类对象的虚表与子类对象的虚表没有任何关系,这是两个不同的对象,这也是虚函数中,指向谁调用谁的概念本质。
- 通过虚表指针和虚函数表,就可以实现多态性,即可以在运行时确定应该调用哪个类的虚函数。
- 虚表指针是类级别的,而不是函数级别的。
- 每个类只有一个虚表指针,指向其对应的虚函数表,但是多继承的时候,就会可能有多张虚表。
- 每一个类中的虚函数在虚表中都要有地址。
指向谁调用谁,传父类调用父类,传子类调用子类:
指向谁调用谁:
1.父类和子类的func构成了重写,即使子类的func没有加上virtual,但是还是具有重写,别看参数的缺省值不一样,但只要参数一样就行,所以父类和子类的func构成了重写
2.给了一个test加上了virtual,然后子类指针遍历p调用了父类的test,这是因为继承所以可以调用父类,同时,这里面test内部的指针是A*this,而不是B*this,因为子类是继承不是拷贝,所以是调用父类的成员,所以这里面隐藏的this是父类的类型,所以这里将p传给了A*this,而this调用了func(),因为this是A所以这里是多态调用
3.换句话说,这里面的test就可以当作一个多态调用的函数了,就例如上面迈火车票的函数调用一样,可以变换成 void test(A*this){this->func()}
4.而这里传值传的p 是 指向的是 一个new B 的地址,所以只能调用相对应类型的方法,也就是B的方法,从而调用了B的func()
答案是B::f(),
虽然 B
的 f
函数被声明为 private
,但这并不影响多态性的工作。当您使用基类的指针或引用来调用虚函数时,C++ 运行时系统会检查对象的实际类型,并调用正确的函数。访问权限(如 public
、protected
、private
)仅影响代码在何处可以访问该函数,而不影响多态性的行为。
因此,即使 B
的 f
函数是 private
的,当通过基类指针 pa
调用 f
函数时,由于 pa
实际上指向一个 B
对象,所以仍然会调用 B
的 f
函数。这就是为什么输出是 B::f()
。
传父类调用父类,传子类调用子类:
如图所示, 图中父子类各自的虚表指针指向的虚表以及虚表内部的地址并不相同,所以这里证明了父类的虚表指针和子类的虚表指针指向的虚表并不是同一个。
并且,对于编译器而言在指向谁调用谁 在使用这个指令之前,编译器会自行的判断这个父子类是否是产生了多态,如果是则编译器会如上图所示,如果不是编译器则会如下图所示:
单继承的虚表状态:
如图所示,只有f1进行了多态的重写,func2没有重写因为子类没有func2 ,func3没有重写因为父类么有func3。
父类虚表
子类虚表
可以看到 子类的虚表不正常,因为每一个类中的虚函数在虚表中都要出现,所以这里少了两个虚函数的地址,func1是重写的,func2是继承的没有重写,而func3和func4不见了!作为子类自己的虚函数消失了。
不过这里可以解释为一种bug:
因为虚函数都需要进入虚表内部,但这里并没有放入,这里是VS监视窗口的bug
也可也理解为,子类的虚表实际上是拷贝了父类的虚表,重写的部分进行覆盖,没有重写的部分不变动的拷贝,也可也说是子类的虚表被隐藏了。
final
如果不想要一个类被继承就加上final,如图外面不想要car被继承就加上了final,加上final后类不能被继承,虚函数不能被重现,被final修饰的类被叫做最终类。
oberride:
是写在子类的重写虚函数后边的,是可以帮忙检查是否完成重写,如果没有完成重写会报错,或者在重写时会出现一些问题时,overrdie会发出报错!