目录
多态概念与实践
多态是面向对象编程的四大基本原则之一,它让程序能够以统一的接口处理不同的对象类型,从而实现了接口与实现分离,提高了代码的灵活性和复用性。多态主要体现在两个层面:静态多态(编译时多态,如函数重载)和动态多态(运行时多态,主要通过虚函数实现)。
1. 多态的基础
- 继承与多态:继承是实现多态的前提条件。通过继承,子类可以继承父类的属性和方法,并在此基础上进行扩展或重写,从而实现不同类之间相同接口的多种行为。
2. 函数重载(静态多态)
- 定义:在同一作用域内,函数名相同但参数列表不同(类型、数量或顺序不同),实现不同的功能,这就是静态多态,编译器在编译时期就能确定调用哪个函数。
3. 动态多态
- 虚函数:通过在基类中声明虚函数(使用
virtual
关键字),允许子类重写(override)该函数,实现运行时的多态性。虚函数的关键在于,它允许通过基类指针或引用调用子类的实现。 - 虚函数表(V-Table):每个包含虚函数的类都有一个虚函数表,存储虚函数的地址。对象实例包含一个指向虚函数表的指针,该指针在对象创建时初始化。当子类重写了虚函数,子类的虚函数表会在相应位置存储子类的函数地址。
- 实现机制:在运行时,通过基类指针调用虚函数时,实际调用的是该指针所指向对象的虚函数表中的函数地址,从而实现动态绑定,达到多态的效果。
4. 虚析构函数
- 重要性:当使用基类指针删除派生类对象时,如果没有虚析构函数,只会调用基类的析构函数,导致派生类特有的资源无法被正确释放。因此,为保证多态对象能被正确销毁,基类的析构函数应声明为虚函数。
- 工作原理:虚析构函数确保通过基类指针删除对象时,会首先调用派生类的析构函数,然后再调用基类的析构函数,从而彻底释放派生类的资源。
【1】多态的前提
-
继承:多态的实现通常基于类的继承关系。一个类(子类)继承自另一个类(父类),子类可以继承父类的属性和方法,并且可以覆盖或扩展父类的行为。
-
虚函数:在基类中定义虚函数(使用
virtual
关键字),是实现动态多态的关键。虚函数允许子类重写父类中的同名函数,这样在运行时,根据对象的实际类型来决定调用哪个版本的函数。 -
父类指针或引用:通过父类的指针或引用来指向子类的对象,这是多态调用的常见方式。这样,即使使用的是父类的接口,也能调用到子类重写后的方法,实现动态行为。
-
方法重写:子类在继承父类的过程中,可以重写(override)父类中的虚函数,提供自己的实现。这是多态表现不同行为的基础。
在子类中重写父类的虚函数就是函数重写的过程,可以实现多态
【2】虚函数(virtual)
只要基类中是虚函数,后面的所有子类中该函数都是虚函数
常规来说,在继承时,给父类中的函数加上virtual关键字,定义成一个虚函数,
在子类中,可以对父类中的虚函数进行函数重写(override)
只要有虚函数的类,都会有一个虚函数表和一个虚(函数表)指针
虚指针是指向虚函数表的指针;
虚函数表,存储所有的虚函数的信息
虚函数表:保存所有虚函数的入口地址,每一个包含虚函数的类都会有一张虚函数表,
如果发生继承关系,子类会先复制父类的虚函数表,如果子类对某个虚函数重写,就去更改虚函数表中,该函数的入口地址
虚函数表指针:指向虚函数表的指针,父类中有一个虚函数表指针,子类中的虚函数表指针是从父类中继承下来的虚函数表指针,指向子类的虚函数表(虚函数表指针存在类中的第一个位置)
【4】虚析构函数
由于实现多态,需要使用父类的指针,指向子类的空间,父类指针可以操作的空间,只有父类自己的部分,所以,在delete父类指针时,并不会释放调子类的空间
解决方法:给基类(父类)的析构函数前面加上virtual关键字,只要基类是虚析构函数,后面继承的所有子类都是虚析构函数,虚析构函数会引导父类的指针,释放掉子类的空间
示例:
#include <iostream>
using namespace std;
class Person {
public:
virtual ~Person() { cout << "Person的析构" << endl; }
virtual void play() { cout << "吃饭" << endl; }
virtual void fun() { cout << "fun" << endl; }
};
class Student : public Person {
public:
void play() override { cout << "打游戏" << endl; }
~Student() { cout << "Student的析构" << endl; }
};
class SubStudent : public Student {
public:
void play() override { cout << "第二次继承" << endl; }
void fun() override { cout << "fun的第二次继承" << endl; }
};
int main() {
Person *p = new Student;
p->play(); // 通过父类指针调用子类重写的play方法
delete p; // 正确释放资源,由于虚析构函数的存在,Student的析构也会被调用
// 下面是注释掉的代码示例,展示了多态的其他用法
// Person *p1 = new SubStudent;
// p1->play(); // 第二次继承
// p1->fun(); // fun的第二次继承
return 0;
}
可以看到这里虽然是Person类型的指针但是调用出来的函数确实student的play()。