多态
虚函,协变,重写
定义
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
分类:
- (静态绑定)静态多态:不同的参数,调用不同的函数。——本质是重载
- (动态绑定)动态多态:通过继承中的虚函数重写 + 父类指针或者引用对齐进行调用,并且根据不同对象调用不同的函数。本质是虚函数表。
条件
- 必须通过基类的指针(切片)或者引用调用虚函数。
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行——重写。
总结:多态 = 基类的(指针或者引用)调用虚函数 + 重写。
虚函数
注意:
成员函数才可以使用virtual。
派生类可以不加virtual,而父类则不行,因为子类重写时,会在virtual的基础上进行重写。 ——但建议都加上。
由于虚函数要进入虚表从而不能内联inline,就算加上也会被编译器忽略。
类中定义的函数默认会成为内联
虚函数的重写
条件:派生类虚函数与基类虚函数的以下条件完全相同,则称称子类的虚函数重写了基类的虚函数 —— 即构成多态。
条件:
- 返回值类型
- 函数名字
- 参数类型和参数个数(注意:和形参无关,也和缺省值无关)——故虚函数继承也被称为接口继承
协变
在破坏了以上条件中的返回值类型相同的条件后,依然维持多态的一种方式。
1 |
|
允许返回值类型不同实现的多态,但要求返回值必须是父子关系的引用或者指针。
- 父子关系的引用或者指针(父子都需要是引用,或者都是指针)
- 父类返回父类型的引用或者指针。
- 子类返回子类型的引用或者指针。
多态原理
虚函数是通过虚函数表(vtable)和虚函数指针(vptr)来实现的:
虚函数表(vtable):
- 每个包含虚函数的类都有一个虚函数表,虚函数表是一个函数指针数组,数组中的每个指针指向该类的虚函数的实现。
虚函数指针(vptr)
- 每个对象都有一个虚函数指针,指向类的虚函数表。当通过基类指针或引用调用虚函数时,程序会通过对象的虚函数指针找到对应的虚函数表,并从表中调用正确的函数。
虚函数表
用于记录虚函数(__vfptr)的地址(位于代码段)
对于一个不符合多态的普通调用,会在编译阶段,(根据调用者的类型,再更具函数名修饰规则),找到要调用函数的地址。
符合多态,那就在运行时,到所指向对象的虚函数表当中找到调用函数的地址
- 如果是父类,那么就去父类的虚函数表中找。
- 如果是子类,那么就去子类的虚函数表中找,子类会将继承的虚表用自己的虚表——覆盖。
并且,在一个子类赋值给父类的时候,即对象切片发生的时候,不会拷贝虚表。否则会导致使用父类的对象却使用子类的方法。
纯虚函数
虚函数的后面写上 =0 ,则这个函数为纯虚函数。
例如:
1 |
|
抽象类
- 包含纯虚函数的类叫做抽象类(也叫接口类)。
- 抽象类不能实例化出对象。派生类继承后也不能实例化出对象。
- 只有重写纯虚函数,派生类才能实例化出对象。
- 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
final
final:修饰虚函数,或者基类(c++11,使其无法被继承),表示该虚函数不能再被重写。
override
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
经典题目
- 求下述程序的运行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int va=0){ std::cout<<"B->"<< va <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
分析
- 由于函数是保存到代码段的,所以派生类不会额外生成函数,因此在调用的时候,也是由其所属的类直接调用。
- 如
test()
位于基类,故调用的时候,也是基类的指针调用。
- 如
- 并且满足虚函数重写的条件,故满足多态,因此,回去调用派生类中的
func
,故有B->
。 - 由于重写是在基类的基础上,重写的是实现,其参数列表的值都来自于基类,派生类参数列表只是为了检查是否满足多态条件 **(参数类型,参数个数相同,和形参无关) **,故结果是
B->1
。
多态
https://weihehe.top/2024/07/05/多态/