目录
一、多态的概念
多态(Polymorphism)是面向对象编程的三大核心特性之一。它允许不同类的对象对同一消息做出不同响应,具体来讲就是完成某个行为时,不同对象去完成时会产生不同的状态。相当于“一个接口,多种实现”。
现实中的多态随处可见:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。不同对象进行的同一行为,结果却各不相同,这就是一种多态行为。
下面讲解C++中是怎么定义多态,怎么理解多态:
二、多态的定义及实现
2.1、多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
这里通过基类引用接收不同派生类对象时,虽然从代码表面看是调用父类函数,但实际会根据对象的实际类型动态选择对应的函数实现。
2.2、虚函数与虚函数重写
虚函数:被virtual关键字修饰的类成员函数称为虚函数。
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }//虚函数的声明
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }//虚函数的重写
//这里的virtual也可以不加,不过不建议
}
虚函数的重写(覆盖):
派生类中定义与基类虚函数同名、同参数、同返回类型的函数
函数名相同、参数列表相同、返回类型相同(协变返回类型除外)
就称派生类的虚函数重写了基类的虚函数
2.2.1、虚函数重写的两个例外:
1、协变返回类型(特殊规则)
重写的返回类型可以不相同,不过要满足基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用。
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。
这里delete p1:p1指向一个Person对象,调用~person()。
delete p2:p2指向一个Student对象,首先调用~student(),然后调用~person(),
只有派生类Student的析构函数重写了Person的析构函数,delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
所以注意:如果一个类可能被继承,它的析构函数必须是虚函数。
2.3、override和final (C++11)
1. final:修饰虚函数,表示该虚函数不能再被重写。
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Animal
{
public:
virtual void sound() final { ... } // 此函数不可被重写
};
class Dog : public Animal
{
public:
void sound() override { ... } // 检查有没有重写
};
2.4、重载、重写(覆盖)、隐藏(重定义)的对比
三、抽象类
纯虚函数:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
抽象类:包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
比如:
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
四、多态的原理:虚函数表与动态绑定
1. 虚函数表(vtable)
-
每个包含虚函数的类都有一个虚函数表,存储该类所有虚函数的地址。
-
对象内存布局中首部包含一个虚函数表指针(vptr),指向所属类的虚函数表。
比如:
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 _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
这里通过调试可以看出,b对象内部不仅存了_b变量,还存了一个_vfptr指针(这个指针叫虚函数表指针),指针指向虚函数表,虚表的本质是一个存放虚函数地址的指针数组,这个数组的最后放了一个nullptr指针。
而d对象里面也包含两个部分,一部分是_d变量,也就是子类自己的成员,还有一部分是从父类继承下来的成员,继承下来的也有一个虚表指针,不过和父类的虚表指针不同:
- 这里Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
- 所以派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中;如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;派生类自己 新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
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;
}
调试后:
原理:
调用
Func(Mike)
:
p
是Person&
类型,实际指向Mike
对象。通过
Mike
的vptr
找到Person
的虚函数表。调用
Person::BuyTicket()
,输出"买票-全价"。调用
Func(Johnson)
:
p
是Person&
类型,但实际指向Johnson
对象(派生类)。通过
Johnson
的vptr
找到Student
的虚函数表。调用
Student::BuyTicket()
,输出"买票-半价"。
五、多继承中的虚函数表
当一个类继承多个含有虚函数的基类时:
每个基类子对象在派生类中都有自己的虚函数表指针(vptr)。
派生类中新增的虚函数可能附加到第一个基类的虚函数表中。
假设有两个基类 Base1
和 Base2
,派生类 Derived
继承自两者:
#include <iostream>
using namespace std;
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
int data1;
};
class Base2
{
public:
virtual void func3() { cout << "Base2::func3" << endl; }
virtual void func4() { cout << "Base2::func4" << endl; }
int data2;
};
class Derived : public Base1, public Base2
{
public:
// 重写基类的虚函数
void func1() override { cout << "Derived::func1" << endl; }
void func3() override { cout << "Derived::func3" << endl; }
// 新增虚函数
virtual void func5() { cout << "Derived::func5" << endl; }
int data3;
};
int main()
{
Derive d;
return 0;
}
虚表内容: