多态的概念
简单来说,就是去执行某个函数,但是不同对象去执行有不同的结果。
就好比买票,我拿纸币买票要六块,但是用支付宝可以九折,不同的方式去买票要付的钱不一样,这句是一种多态、
多态的定义和实现
多态的构成条件
1、必须用基类的指针或者引用调用虚函数
2、被调用的函数必须是虚函数且必须对基类的虚函数进行过重写
这里可以参考下图:
第一个对象调用Func函数的时候,函数参数接收类型的ali(父类),但是传过去的参数是子类,所以调用的是子类的虚函数
第二个参数就是父类调用它父类的虚函数
虚函数
被virtual修饰的称之为虚拟函数,如上图,在函数返回值前面加一个virtual(虚拟)。
虚函数的重写
虚函数的充血(也叫覆盖):指在派生类里面有一个跟基类完全相同(返回值、函数名、参数)的虚函数,称之为子类的虚函数重写了基类的虚函数。
class ali
{
public:
virtual void BuyTicker()
{
cout << "九折" << endl;
}
};
class me :public ali
{
public:
virtual void BuyTicker()
{
cout << "原价" << endl;
}
};
虚函数重写的例外
1、协变(基类和反派生的返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class a{};
class b :public a{};
class ali
{
public:
virtual a* BuyTicker()
{
cout << "九折" << endl;
return new a;
}
};
class me :public ali
{
public:
virtual b* BuyTicker()
{
cout << "原价" << endl;
return new b;
}
};
就像这样,了解一下就好了。
2. 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class ali
{
public:
virtual ~ali()
{
cout << "~ali()" << endl;
}
};
class me :public ali
{
public:
virtual ~me()
{
cout << "~me()" << endl;
}
};
//只有派生类的析构函数重写了父类的析构函数,才能构成多态,确保p1、p2指向的对象正确调用了虚构函数
int main()
{
ali* p1 = new ali;
ali* p2 = new me;
delete p1;
delete p2;
return 0;
}
override 和 final
从虚函数的重写条件来看是比较严格的,但是有的时候就会出现函数名写错一点无法构成重载,这种时候因为不会报错,所以很难发现,慢慢调试过于折磨,就出现了这个个关键字:
final
修饰虚函数,表示这个虚函数不能重写
override
检查派生类是否重写,如果没有重写就会报错
重载、覆盖、隐藏(重定义)的对比
重载:两个函数在同一个作用域,函数名相同但是参数略有不同
重写:两个函数分别在基类和子类,返回类型、函数名、参数都必须相同且是虚函数
重定义:两个函数分别在基类和派生类,函数名相同但不构成重写
抽象类
抽象类的概念
在了解抽象类之前,要先理解什么是纯虚函数
在基类中仅仅给出声明,不对虚函数实现定义,而是在派生类中实现。这个虚函数称为纯虚函数。普通函数如果仅仅给出它的声明而没有实现它的函数体,这是编译不过的。纯虚函数没有函数体。
纯虚函数需要在声明之后加个=0。
含有纯虚函数的类被称为抽象类。抽象类只能作为派生类的基类,不能定义对象,但可以定义指针。在派生类实现该纯虚函数后,定义抽象类对象的指针,并指向或引用子类对象。
在父类Buy里面,我声明了一个Ticket的纯虚函数,同时在继承的两个子类里面重写,然后调用即可得到对应的结果。
接口继承和实现继承
普通函数的继承是一种实现继承,可以直接使用函数。
虚函数的继承是接口继承,只是继承了对应的接口,实际内容需要重写。
如果不实现多态,不要把函数定义为虚函数。
多态的原理
虚函数表
存在虚函数的类都有一个虚函数表,称之为虚表。
虚表由类对象里面指向虚表的指针所指,这个指针称之为虚指针(vptr,一般是类里面的第一个成员)。
虚表里面放着的是类里面成员函数指针。
上图,一般来说Base类里面只有一个int,而函数会放到公共区不算大小,应该是4,但是这里是8,可见Fun1占了四个字节的大小,为了看的清晰一点,创建一个对象出来
在监视里面可以看到正如刚刚所说多了应该vptr,这就是虚表指针,指向的是虚表的地址,而虚表里面放着的正是虚函数的地址。
这里就直接说结论吧,证明过程网上都传烂了。
1、派生类也有一个虚表,如果虚函数完成了重写,那么虚表里面函数的地址是重写后的地址,也就是说两个虚表里面如果完成了重写,那么同名函数的函数指针地址是不一样的,所以重写也叫做覆盖。
2、如果继承下来的函数是虚函数,就会被放进虚表,如果不是,则不会进入虚表,但是会放在后面。
3、虚表的本质是一个存放虚函数指针的数组,由最后的nullptr结尾。
4、虚函数的生成:派生类先拷贝父类的虚表,如果派生类自己重写了虚函数,则在对应位置覆盖,如果派生类有新增的虚函数,会被放到最后。
5、虚函数存在虚表里面,虚表存在对象里面。
6、虚表存的是指针,不是函数。
多态的原理
当指针指向某一个对象的时候,这个指针会在对象的虚表里面找到对应的虚函数。
这样就可以解释为什么要完成虚函数的覆盖和为什么要用指针和引用调用。
动态绑定与静态绑定
静态绑定(前期绑定)可以理解为在编译的时候就确定了调用哪个函数,比如说函数重载,虽然看上去调用是函数名一样,但是底层(函数名)是不一样的。
动态绑定(后期绑定)是程序在运行的时候,根据拿到的类型来确定行为的,比如上面的多态,先要在虚表里面找到确切的虚函数地址再进行调用。