通俗来说,多态就是指同一个操作或者行为在不同的对象上可以有不同的表现形式或实现方式。举个例子:以 “吃” 这个行为为例,不同的动物有不同的 “吃” 的方式和内容。比如,猫吃鱼、狗吃肉、兔子吃草,虽然都是 “吃” 这个行为,但不同的动物表现出了不同的具体行为和特点,这就是一种多态现象。
在编程语言里,多态通常体现在函数或方法的调用上。假设有一个父类和几个子类,它们都有一个名为 “draw” 的方法,父类的 “draw” 方法可能只是一个通用的框架,而子类会根据自身的特点重写 “draw” 方法来实现不同的绘图功能。比如,圆形子类的 “draw” 方法用于绘制圆形,矩形子类的 “draw” 方法用于绘制矩形。当使用父类的引用去调用 “draw” 方法时,根据实际指向的是圆形对象还是矩形对象,会调用到不同子类中重写后的 “draw” 方法,表现出不同的绘图效果。
1、多态的构成条件
1、被调用的函数必须是虚函数,且构成函数重写(覆盖);
2、必须由父类的指针或者引用来进行调用;
说明:1、成员函数才能被修饰成虚函数,普通的函数无法被修饰为虚函数。
2、虚函数的重写要求成员函数满足三同条件(函数名相同、参数相同、返回值相同),这里有一个特殊情况叫做协变(返回值可以不同,要求父类返回父类的指针或者引用,子类返回子类的指针或者引用),虚函数的重写仅仅是函数方法的重写。
#include<iostream>
using namespace std;class Person
{
public:virtual void BuyTicket(){cout << "买全价票" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "买半价票" << endl;}
};void fuc(Person& p)
{p.BuyTicket();
}int main()
{Person p;Student st;fuc(p);fuc(st);return 0;
}
问题:为什么编译器要将父类和子类的析构函数名称处理成destructor?
这里有这样一个案例,如下面的代码所示:
#include<iostream>
using namespace std;class Person
{
public:~Person(){cout << "~Person()" << endl;}
};class Student : public Person
{
public:~Student(){cout << "~Student()" << endl;}
};int main()
{Person* p = new Person;delete p;p = new Student;delete p;return 0;
}
这里我们在释放对象的时候,预期先调用~Person(),再调用Student的析构函数。也就是这里期望要形成多态。Person* p 指针指向什么对象就调用该对象的析构函数。而目前的状态下程序的运行结果为:
也就是这里的函数调用只满足的多态的一个条件(调用使用父类的指针或者引用),那么我们尝试满足形成多态的第二个条件: 构成虚函数的重写,先加上virtual ,这里就会注意到如果编译器不统一进行析构函数名称的处理,那么就无法完成重写的条件(三同)。所以,为了构成多态,编译器就需要将子类和父类的析构函数处理成名称一样的函数。下面我们看形成多态之后的程序运行效果
根据代码的析构条件,父类先行析构,子类先析构子类的部分,父类部分再调用父类的析构函数。
1.1、重载、重定义与重写
- 函数重载要求两函数必须在同一作用域,函数名要求一致,参数列表不同。这就构成了函数重载。
- 重定义是基类与派生类中的成员函数名相同,这种情况也叫做隐藏。
- 函数重写的要求比较严格,要求基类与派生类中的虚函数保持三同(返回值、函数名、参数列表),在协变这同特殊情况下可以返回值不同。需要注意的的是函数重写仅仅是函数方法的重写。
2、多态的实现原理
2.1、虚函数与虚函数表
虚成员函数与普通的成员函数一样存放在内存中的代码段部分。在含有虚函数的对对象里会存在一个虚函数表指针。
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
sizeof(Base)的大小并非是4个字节,经过在X86环境下验证可以发现sizeof(Base)的大小为8字节。实际上在Base的内部存放了一个虚函数表指针,该指针指向Base的虚函数表。 下面来详细分析一下继承中的虚函数表。
class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _a = 1;
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _b = 2;
};int main()
{Base b;Derive d;return 0;
}
观察监视窗口我们可以看到:
实际上在继承的过程中派生类会将基类的虚函数表进行拷贝,如果存在重写的虚函数,就会将重写之后的虚函数覆盖基类原来的函数。这里会就引出一些问题
1、为什么多态的实现需要基类的指针或者引用来调用呢?
假设使用派生类的指针或者引用来调用函数,那么就会直接调用派生类的成员函数,不会体现出多态性。而是用基类的指针和引用时,在编译阶段,编译器并不知道实际要调用的是基类的虚函数版本还是派生类的虚函数版本,而是在运行时根据指针或者引用指向对象的类型来确定具体的版本。
2、为什么不能使用基类对象直接调用虚函数实现多态呢?
首先我们要理清楚,使用基类指针或者引用来调用虚函数实现多态的原理。1)当一个派生类对象的指针或者引用直接赋值给基类的指针或引用,从该基类指针看到的派生类对象就是一个以派生类虚函数表指针开头的基类对象,从而在派生类的虚函数表中找到要调用的虚函数。2)当一个基类的对象或者指针调用基类的虚函数时,自然的就会先从基类的虚函数表中找到要调用的虚函数。
然而,如果使用的是基类对象来调用虚函数,派生类赋值给基类会引发对象的切片操作,这个过程会创建一个新的基类实例,并将派生类中属于基类的数据复制给新创建的基类对象。在这个过程中,派生类的虚函数表并没有被直接删除或者进行修改,仍然存在在内存中。新创建的基类对象有自己独立的内存空间和虚函数表指针,它的虚函数表指针指向基类的虚函数表。这种情况下就不能实现派生类要实现的功能,因此无法实现多态。