多态的概念
多态就是多种形态,在执行某个行为时,当不同对象去完成时,执行结果不同。
就好比买火车票:普通人买全价票、学生买学生票、儿童买半价票。虽然大家都是人,但是在买票时结果却不同。这就是典型的多态。
多态的定义和实现
- 必须通过基类的指针或者引用才能调用虚函数,这是因为只有基类的指针或引用能够同时指向基类和派生类的对象,从而实现多态的效果。
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
举例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket() // 虚函数
{
cout << "全价票" << endl;
}
};
class Child : public Person
{
public:
virtual void BuyTicket() // 虚函数重写
{
cout << "半价票" << endl;
}
};
void Fun(Person& p) // 基类的引用
{
p.BuyTicket();
}
int main()
{
Person p1;
Child c1;
Fun(p1);
Fun(c1);
return 0;
}
虚函数
被virtual修饰的类成员函数才是虚函数。非成员函数不能被virtual修饰。
class Person
{
public:
virtual void BuyTicket()
{
cout << "全价票" << endl;
}
};
虚函数的重写(覆盖)
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person
{
public:
virtual void BuyTicket()// 虚函数
{
cout << "全价票" << endl;
}
};
class Child : public Person
{
public:
virtual void BuyTicket()// 虚函数重写
{
cout << "半价票" << endl;
}
};
举例:
// 派生类 Dog
class Dog : public Animal {
public:
void talk() const override {
cout << "汪" << endl;
}
};
// 派生类 Cat
class Cat : public Animal {
public:
void talk() const override {
cout << "喵" << endl;
}
};
// 测试函数,用于展示多态
void test_animal_talk(const Animal& animal) {
animal.talk();
}
int main() {
Dog d1;
Cat c1;
// 使用基类引用调用多态函数
test_animal_talk(d1); // 输出 "汪"
test_animal_talk(c1); // 输出 "喵"
return 0;
}
来一道虚函数的题:
// 基类 A
class A {
public:
virtual void func(int val = 1) {
cout << "A->" << val << endl;
}
virtual void test() {
func();
}
};
// 派生类 B
class B : public A {
public:
void func(int val = 0) {
cout << "B->" << val << endl;
}
};
int main() {
B* p = new B();
p->test();
delete p; // 释放动态分配的内存
return 0;
}
A: A->0 | B: B->1 | C: A->1 | D: B->0 | E: 编译出错 | F: 以上都不正确
我第一次做选的D,后来分析发现了错误:
派生类B并没有对test进行虚函数重写,所以当我使用派生类对象调用test时,如果在派生类里没有,他会去基类里寻找,因为没有进行虚函数重写,所以答案是B。
虚函数重写的例外
- 协变
重写(Override)虚函数时,如果基类函数返回一个指向基类对象的指针或引用,派生类中的重写函数可以返回一个指向派生类对象的指针或引用。这被称为返回类型的协变。(这里的基类对象可以是可以来自自身的继承体系,也可以来源于其他继承体系。)
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;
}
};
// 同一继承
class Person
{
public:
virtual Person* f()
{
return new Person;
}
};
class Student : public Person
{
public:
virtual Student* f() //协变
{
return new Student;
}
};
- 析构函数的重写
为什么要重写虚析构函数呢?
当你有一个基类,并且从这个基类派生出多个子类时,如果基类有一个析构函数(无论是否为虚),而你想通过基类的指针来删除派生类的对象,那么你应该将基类的析构函数声明为虚的。这样做是为了确保当通过基类指针删除派生类对象时,能够调用到派生类的析构函数,从而正确释放派生类特有的资源。
我们在前面学习继承时就知道编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以只要基类的析构函数加了virtual关键字,它就一定会形成重写。
class Person
{
public:
//~Person()
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student() //构成重写
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
override 和 final关键字
override
override 关键字用于在派生类中明确标记一个成员函数是要重写(Override)基类中的虚函数。
好处:
- 如果没有构成虚函数的重写在编译时就会报错,能够及时发现错误。
- 增强代码可读性,可以让阅读的人清楚的知道该函数是重写了某个基类的函数。
class Person
{
public:
virtual void func() {
// ...
}
};
class Student : public Person
{
public:
virtual void func() override {
// ...
}
};
final
final 关键字用于防止类被继承或防止类中的虚函数被进一步重写。
好处:
- 防止虚函数被重写,确保某些成员函数不会因为继承而意外被改写
- 如果一个类被声明为final,它就不能被继承,有助于保护类不被扩展
class Person
{
public:
virtual void func() final{
// ...
}
};
class Student : public Person
{
public:
virtual void func() {
// ...
}
};
重载、重写和重定义(隐藏)
纯虚函数和抽象类
在虚函数的后⾯写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。
如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。
class Person
{
public:
virtual void func() =0{
// ...
}
};
class Student : public Person
{
public:
virtual void func() { // 必须重写,不然也是抽象类,无法实例化
// ...
}
};
多态的原理
虚函数表
下图中_vfptr就是虚函数表指针,一个包含虚函数的类中最少都有一个虚函数表指针。
因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
class Person
{
public:
virtual void BuyTicket() // 虚函数
{
cout << "全价票" << endl;
}
};
class Child : public Person
{
public:
virtual void BuyTicket() // 虚函数重写
{
cout << "半价票" << endl;
}
};
void fun(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p1;
Child c1;
fun(p1);
fun(c1);
return 0;
}
虚函数表的原理
通过下图我们可以看到,满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。
动态绑定和静态绑定
静态绑定
重载就是典型的静态绑定(前期绑定)
对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤
函数的地址,叫做静态绑定。
动态绑定
满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数
的地址,也就做动态绑定。