1.多态的概念
多态是什么?首先我们从概念讲起,简单来讲,多态就是多种形态,当你要去完成同一件事情的时候,不同的人去完成这件事情会有不同的结果.
比如在买票的时候,如果是成人去买票,则会买到成人票;如果是学生,则会买到学生票.
2.多态的实现以及构成条件
首先,多态的实现基础是继承,我们可以通过继承来实现多态.
具体的实现条件是什么?
1.调用的函数必须是虚函数,且虚函数必须要构成重写
2.必须要用基类或者派生类的指针或者引用去调用虚函数
那么这里就会有产生了两个问题.
1.什么是虚函数(父子类的虚函数)?
2.什么是重写?
1.虚函数
虚函数的写法很简单,直接在函数前面加一个virtual即可
这个在这里先简单介绍一下,后面会详细讲解
2.虚函数构成重写
那么什么是重写?
这里的虚函数构成重写是什么?
他跟函数重载不一样,这里的重写指的是,父类的子函数和基类的子函数,函数名,参数,返回值相同的情况下,则构成重写.
这种则是一个很经典的多态,里面的虚函数则构成了重写,可以看到,父类和子类的函数名,参数列表和返回值都相同.
但是重写的情况下,也会有两个例外.
例外1.协变
协变是派生类在重写基类虚函数时,与基类虚函数返回值类型不同.
注意,这里的返回值类型不同指的的父类或子类返回值是指针或者引用类型,如果返回类型一个是int,一个是void,则这个时候会编译报错,并不是重写.
也可以理解为当返回值是自定义类型的时候,他构成的则是重写,如果是一个内置类型,则会报错.
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
例外2.析构函数的重写(基类与派生类析构函数名字的不同)
如果基类的析构函数为虚函数,则子类的析构函数无论是否加virtual关键字,都是与父类的析构函数构成重写,虽然看起来,名字和类型都不一样,违反了重写规则,其实这也是构成重写的一种规则.可以理解成编译器对析构函数做了一种特殊处理,而实际上,经过编译器处理后的父子类的析构函数,都统一有个名字,叫做destructor.
3.override和final
final的作用是修饰虚函数,使得其不能再被重写.
class Person
{
public:
virtual void Buy_Ticket() final
{
cout << "全票" << endl;
}
};
override的作用是检查派生类的虚函数是否重写了基类的某个虚函数,如果没有则编译报错
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
3.抽象类
在虚函数的后面加个=0,则这个函数为纯虚函数.包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象
4.多态的原理
1.虚函数表指针
如果我们要讲清楚多态的原理,首先得引入一个指针,叫做虚函数表指针,简称虚表指针
虚表指针是什么?他存在于哪个地方?我们可以用接下来的这个代码来进行测试
class Base
{
virtual void Fun1()
{
cout << "Fun1()" << endl;
}
virtual void Fun2()
{
cout << "Fun2()" << endl;
}
void Fun3()
{
cout << "Fun3()" << endl;
}
};
int main()
{
Base b;
return 0;
}
我们在这里首先用Base来创建了一个b对象,通过调试我们可以看到,这个对象里面包含着一个vfptr
这个_vfptr就是所谓的虚表指针,这个指针里面存放着Fun1的地址和Fun2的地址
下面这个则是_vfptr的地址
当前的环境是x64,所以可以看出来这个的大小是8个字节.
我们也可以观察到,里面是没有Fun3()这个函数指针的,也就是说,只有在函数面前加了关键字virtual, 虚表指针才能指向这个函数的指针.
我们也得出一个结论,如果有虚函数,则就一定会有虚表指针.
2.多态怎么去实现调用的
要明白多态是怎么实现调用谁就是谁的,我们首先来看这段代码
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
首先来简单看一下这段代码,我们用Person 来创建了Mike这个对象,然后用Student 来创建Johnson这个对象,那么我们现在看看Mike对象的内存
Mike :
Johnson:
这里为什么会有一个小地址跟Mike对象是一样的呢?这是因为Mike是基类,Johnson是继承Mike类的,而继承是通过切片的方式来继承,所以会有一小段地址是一样的.
然后我们再来看这个虚表指针的指向,可以发现,Mike指向的是父类,在父类里面找到了父类的虚函数进行调用;而Johnson则是指向了子类,在子类里面找到了子类重写的虚函数进行调用,最终达到了指向谁调用谁的目的.
我们由此可以得出一个结论,就是说,如果满足多态条件,就会去指向对象的虚表中找到对应的虚函数进行调用.