C++ --- 多态
一、什么是多态
多态是面向对象语言三大特性之一,简单来说就是多种形态,通常多态分为编译时多态和运行时多态。
(1)编译时多态:函数重载,运算符重载,模板等
(2)运行时多态:不同对象调用同一个方法会产生不同的结果
以下说明都是运行时多态。
二、满足多态的两个条件
1、必须是基类的指针或者引用调用其虚函数
2、被调用的函数必须是虚函数,并且完成了虚函数的重写/覆盖
三、虚函数
虚函数 和继承章节里提到的虚继承是使用同一个关键字virtual,被virtual修饰的成员函数就称为虚函数(注意一定是成员函数!!!)
四、虚函数的重写/覆盖
虚函数的重写/覆盖 派生类中存在与基类完全相同的虚函数,就构成重写关系。
完全相同指的是返回类型相同,参数列表相同,函数名相同,三同。
引申一个协变关系:当返回类型是父子继承关系的指针的时候,此时也满足多态,叫做协变关系。
注意:派生类中的虚函数可以不用写virtual关键字,因为重写实际上是基类的函数声明加上派生类中重写的函数实现,跟派生类的此虚函数的声明是没有关系的。
协变关系举例:
cpp
// 一组继承关系的类
class A {};
class B : public A {};
class 基类
{
virtual A* Func() { return nullptr; }
};
class 派生类
{
virtual B* Func()
{
std::cout << "协变关系" << std::endl;
return nullptr;
}
};
// 调用函数调用Func()
...
多态举例:
cpp
#include<iostream>
#include<string>
// 基类Person
class Person
{
public:
virtual void study() = 0; // 这个是纯虚函数
protected:
std::string _name = "张三";
int _ID_number = 0;
};
// 派生类Student
class Student: public Person
{
// 实现虚函数study的重写
public:
virtual void study()
{
std::cout << "学生在学习" << std::endl;
}
private:
int _Suser_ID = 0;
};
// 派生类Teacher
class Teacher : public Person
{
// 实现虚函数study的重写
public:
virtual void study()
{
std::cout << "老师在备课" << std::endl;
}
private:
int _Tuser_ID = 0;
};
void study(Person& p) // 这里是Person* p也是可以的
{
p.study();
}
int main()
{
Student s;
Teacher t;
study(s);
study(t);
return 0;
}
运行结果:

上面演示了一个继承Person基类的派生类Student和Teacher,分别调用了同一个虚函数study,展现了不同的结果。
重点:
(1)使用基类的指针或引用对象来调用此虚函数,这里也使用了赋值兼容转换,继承章节讲过,派生类生成的对象可以给给基类的指针或者引用。
(2)虚函数由virtual关键字修饰,并且派生类需要完成对虚函数的重写/覆盖。
五、重载,重写,隐藏
这三个关系均是出现在函数当中的:
(1)重载,在同一域下,函数名相同,参数列表不同,返回值可同可不同,构成重载关系。
(2)重写,在基类,派生类类域下,有三同的虚函数,在派生类中构成重写关系。
(3)隐藏,也是在基类,派生类类域下,只要函数名相同,并且没有构成重写关系,则就构成隐藏关系。
六、纯虚函数和抽象类
在刚刚的多态代码演示案例中就已经演示出了纯虚函数的写法,在虚函数末尾添加 = 0,则表明这是一个纯虚函数 ,同时包含纯虚函数的这个类被称为抽象类。
抽象类特点:
(1)抽象类不能实例化对象。
(2)强制了派生类进行对虚函数的重写,不重写实例化不了对象。
抽象类举例:
cpp
class A // 抽象类
{
public:
virtual void func() = 0; // 纯虚函数
protected:
int a;
};
class B : public A
{
public:
virtual void func()
{
; // 重写实现
}
private:
int b;
};
int main()
{
A a; // A不能实例化对象
B b;
return 0;
}

七、C++11新增多态关键字
7.1override
虚函数加上此关键字帮助用户判断是否完成重写,没有完成重写会报错。
7.2final
此关键字修饰成员函数,则此函数不能被重写;
此关键字修饰类,则此类不能被继承;(延申一下,C++98中使用构造函数私有化来限制类不能被继承)
八、多态底层原理
对于编译时多态,其底层有一个函数名修饰规则,在程序编译时会将函数名,参数列表等函数信息进行固定格式的组合,便于程序方便的定位调用哪一个函数,函数重载就是如此实现仅传入参数不同就能够锁定需要调用的函数。
对于运行时多态则是引入了一个新东西,虚函数表指针,此指针存在于拥有虚函数的类实例化出来的对象当中,所以在对拥有虚函数的类进行内存对齐的分析时需要注意增加这个指针的大小。
虚函数表指针
虚函数表指针是用于存储虚函数的地址,所以虚函数表本质上就是函数指针数组。
举例刚刚的Student类实例化出来的对象s:

这里__vfptr就是虚函数表指针,里面存储的就是study这个虚函数的地址。
汇编下的call虚函数表指针:

在基类的指针或者引用调用虚函数时会call这个虚函数表指针,从对应传递过来的对象中找到对应的虚函数,从而去调用它,以实现多态这一特性。
注意:同类型对象虚表共用同一个 ,不会实例化不同对象而新开一个虚表;虽然派生类继承了基类的虚表,但是由于派生类中独有的属性这些信息,导致基类和派生类中的虚表并不是同一个虚表,而是独立的虚表;vs这个IDE将虚表存在在常量区里面的,这个存放位置视ide而定。