C++基础语法(六)多态
一、什么是多态
xxxx在编程语言多态就是在调用相同(外貌相同)接口时表现出来的多种形态,我们可以将多态划分为两种1、静态的多态;2、动态的多态
1、静态的多态
xxxx何为静态的多态,就是在程序编译时就决定了该函数被调用时所出现的形态。静态的多态其实就是指函数重载。
举例说明
void swap(int* a, int* b)
void swap(double* a, double* b)
这里,我们通过对函数的重载,实现在在不同数据类型变量在调用同名函数时出现了不同的形态(分别交换整形和浮点型),或者说执行了完全不同的函数体(看似)。
2、动态的多态
xxxx动态的多态就是大部分人讲的“狭义的多态”,是一种基于继承语法上的新语法。对于不同继承关系的类对象,在调用同一函数时产生的不同行为。
xxxx那为什么叫动态的多态呢?是因为,调用函数的具体形态并不是在编译时就确定好的,而是在执行过程中,根据调用函数的对象指针或者对象的引用的具体内容而决定(具体后面会仔细解读)。
<注> 由于静态的多态在函数重载中已经详细讲过,所以下面所谈的多态都是第二种动态的多态 《函数重载链接》
二、构成多态的条件与实现
xxxx刚刚提到,动态的多态是基于继承的一种新语法。那么就需要在类与类的继承关系上完成多态的条件
1、多态的构成条件
条件1:通过基类指针 / 引用调用虚函数
条件2:被调用的函数为虚函数,并且派生类需要对基类中该虚函数进行重写
xxxx看到这里大家可能会有点懵。什么是虚函数?什么是重写?接下来我会先着重解决这两个问题。
问题一:什么是虚函数
xxxx虚函数,就是由关键字virtual修饰的函数。virtual关键词应该大家不陌生,在学习继承的时候虚继承那里应该已经见到过了。但是多态这里修饰成员函数的virtual和修饰继承类的virtual没有任何联系,切不可混为一谈。
xxxx但是并不是所有的函数都可以被修饰为虚函数,需要满足一定的条件。
条件: virtual只能修饰类中非静态成员函数成员函数
class Person
{
public:
virtual void BuyTickets() //这里BuyTickets就是虚函数
{
cout<<"买票-全票"<<endl;
}
};
问题二:什么是函数重写
xxxx当派生类中的函数与基类中虚函数的函数原型完全相同时构成重写(覆盖)。函数原型相同就是指:函数返回值、函数名称、函数参数列表完全相同。
class Person
{
public:
virtual void BuyTickets() //这里BuyTickets就是虚函数
{
cout<<"买票-全票"<<endl;
}
};
class Child : public Person
{
public:
virtual void BuyTickets()
{
cout<<"买票-免票"<<endl;
}
};
注:虚函数与重载的几种特例!!
xxxx①当基类函数用virtual修饰后,子类函数不必要用virtual修饰,自动别识别为重写父类虚函数(满足多态条件)
xxxx②构造函数不能是虚函数。未执行构造函数对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义!
xxxx③析构函数本身构成重写。可能有些人会疑惑,为什么析构函数会构成重写,明明他们的函数名称是(~类)这样的结构,不同类的类名不同,不应该构成重写。能构成重写的原因是,所有析构函数都会被编译器看做destruction这个函数名,所以构成重写。
xxxx④建议析构函数写成虚函数构成多态: 因为正常情况下,不会出现什么问题,但是在下面这种情况,就可能会出现错误。
//A类是基类,B类是派生类
int main()
{
B b;
A* ptra = &b;
delete ptra;
}
对于上述的情况,如果不构成多态,那么ptra是A类型的指针,当我们delete时,就会调用A的析构函数,可是ptra指向的是B类的对象,就会出现问题。
但是,如果我们这个时候将析构函数形成多态,那么当delete指针时候,就会知道,ptra指向的是B类对象,就会去调用B类的析构函数 ,就不会发生安全隐患。
xxxx⑤协变: 对于基类和派生类的虚函数,在返回值不同,但是都是基类或派生类的指针或者引用,任何类就行,只要有父子关系时,也构成重写关系。
class A
{
public:
virtual A* func(){}
};
class B : public A
{
public: //既演示了协变,有演示了派生类不需要加virtual
B* func (){}
};
2、纯虚函数和抽象类
xxxx纯虚函数是一种特殊的虚函数,具有纯虚函数的类叫做抽象类
纯虚函数的定义
class A
{
public:
virtual void func() = 0; //虚函数
};
xxxx纯虚函数不需要函数实现,是专门为多态服务的。
抽象类
xxxx在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
xxxx为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重载以实现多态性。同时含有纯虚拟函数的类称为抽象类,抽象类不能实例化生成对象。这样就很好地解决了上述两个问题。
xxxx将纯虚函数实现时没有意义的,因为抽象类无法实例化对象,因此根本没有对象能调用到纯虚函数,所以不需要实现,显示纯虚函数也是没有用的。
xxxx继承下抽象类的派生类必须重写抽象类的纯虚函数,否则派生类将纯虚函数原模原样的继承下来后,自己也成为了抽象类,也不能实例化对象。
3、override和final关键字
override
final
4、实现继承和接口继承
三、多态的底层原理
1、虚函数表指针
xxxx就是当类中存在虚函数后,就会在对象中多出来一个指针,就是虚函数表指针。
xxxx虚函数表指针指向的是一个虚表,虚表存放的是属于该类的虚函数的指针。
三个类分别有一个实例对象,查看他们的内容
注:每个类实例化出的对象时候,每个对象都有属于自己类的虚函数表,都有属于自己的虚函数指针。就是说,同一个类实例化出多个对象时,他们的_vfptr是完全相同的,指向的是同一块空间(这个空间就是虚函数表)。
2、为啥必须使用基类指针或者引用才能构成多态
xxxx如果使用普通对象是,将派生类对象赋值给基类对象就会发生切片。切片具体内容在继承一文中有具体介绍。
xxxx赋值切片以后,基类对象就会产生属于基类的虚函数表指针,没有办法把派生类对象的虚函数表指针给基类对象。
xxxx正是由于这样的机制,所以就保证了,指针或者引用调用虚函数时,就会通过虚函数表指针访问到属于该类的虚函数表,在虚函数表中找到匹配的虚函数。这样就做到,指向基类调用基类的虚函数;指向派生类调用派生类的虚函数。
这就是多态的根本原理。
3、关于虚表的一些扩展
(1)虚函数表的“不正确”
xxxx事实上,是存放在虚函数表的,但是就是vs自身的原因,让监视窗口不能反映真正的内部存储结构了。
(2)何时初始化_vfptr?
xxxx虚函数表是在编译时就创建完成,但是每个对象的虚函数表指针是在初始化列表是完成。
(3)如何理解重写 == 覆盖?
xxxx当派生类继承含有虚函数的基类时,首先派生类要将基类的虚函数表整拷贝一份,然后让属于派生类自己的虚函数表指针指向拷贝的虚函数表,当派生类对基类的虚函数重写后,将对应位置的虚函数表的地址改成自己重写的虚函数的地址。(简单来说,不重写时,虚函数表指针不同,指向的虚函数表相同)
(4)虚函数表存放在哪里?
xxxxvs编译器下,虚表存放在代码段中!!
4、多继承中的虚函数表
xxxx我们可以看到,当Derive进行多继承的时候,Derive就会有多个虚函数表指针,指向多个虚函数表。每个虚函数表都是从每一个基类中对应继承过来的。
xxxx同时,我们还发现,Driver中的一个func可以同时对两个基类的func都构成重写,但是他们的地址是不一样的,就是说,还是形成了两份重写函数。
5、菱形虚拟继承中的虚函数表
xxxx这里已经深入了,但是想提醒一点。
一开始在学虚拟继承的时候,我们发现虚表在存储对于公共子类的偏移量是,不是直接存储,而是空出了4个字节的空间,现在我们就可以知道了,空出来的空间也是存的偏移量,只不过存储的是,虚表指针到虚函数表指针
四、一些坑
1、初始化的顺序与初始化列表写的顺序无关,与继承的顺序有关
2、多态只会重写函数实现,不会对函数参数列表进行重写(当有参数缺省值时,不论调用的是子类还是父类的函数,都使用父类虚函数的参数列表)
3、inline函数可以是虚函数吗?可以,但是一旦被virtual修饰,inline就失去效果了。因为inline函数没有地址,是直接在调用处展开(类似宏),所以一旦被修饰成虚函数,必然要把虚函数地址放进虚函数表中。所以就不会是内联函数了。
4、静态成员函数可以是虚函数吗?不可以,静态成员函数是属于整个类的,不依赖对象而存在,所以调用虚函数的时候是依赖类的(类名::函数())。因此在调用是就没有this指针,这样就没有虚函数表指针,就无法访问到虚函数表,因此无法调用。(编译器会对静态成员函数被virtual修饰的情况直接报错)
5、接口继承:接口继承是指,派生类并没有将基类的函数完整的继承过来,而是在形成多态后仅仅将函数接口(函数名、返回值和参数列表)继承下来。函数的具体实现由派生类自行实现。