1. 多态基本内容
C++ 中的多态是面向对象编程的一个重要特性,指的是同一个函数或对象在不同的情况下可以表现出不同的行为。多态通常通过继承和虚函数来实现。它分为两种类型:编译时多态(静态多态)和运行时多态(动态多态)。
多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
动态多态满足条件:
- 有继承关系
- 子类要重写父类
动态多态使用:父类的指针或者引用指向子类对象
2. 多态原理
在C++中,多态的底层原理主要依赖于虚函数表(Virtual Table, vtable)和虚函数指针(Virtual Table Pointer, vptr)。当类中包含虚函数时,编译器会为类生成虚函数表,并为每个对象分配一个指向该虚函数表的指针。这种机制在运行时通过虚函数表来动态决定调用哪个函数,从而实现多态。
虚函数表(vtable):
- vtable是编译器为每个包含虚函数的类生成的一个隐藏的数据结构。它实际上是一个指针数组,存储了类中所有虚函数的地址。
- 每个含有虚函数的类都有自己的vtable。如果某个类继承了基类并重写了基类中的虚函数,那么派生类的vtable中将存储派生类的虚函数实现的地址,而非基类的实现。
虚函数指针(vptr):
- 每个包含虚函数的对象都拥有一个隐藏的指针(vptr),这个指针指向该对象所属类的vtable。
- 当对象被创建时,vptr被初始化,指向该对象所属类的vtable。
运行时多态的实现过程:
当我们通过基类指针或引用调用虚函数时,编译器会在运行时通过vptr找到该对象的vtable,然后从vtable中找到对应虚函数的地址并进行调用。这就是运行时动态绑定的过程。
- 对象创建时:
- 对象中包含一个vptr,它被指向所属类的vtable。
- 调用虚函数时:
- 程序会通过vptr查找vtable。
- 根据vtable找到实际要调用的虚函数地址。
- 调用实际函数,实现多态。
动态绑定的开销:使用虚函数和多态机制引入了额外的运行时开销
- 内存开销:每个对象需要存储一个指向vtable的vptr。
- 性能开销:每次调用虚函数都需要通过vptr进行间接查找,而非直接调用函数。
总结:多态的底层原理依赖于虚函数表(vtable)和虚函数指针(vptr)。在编译时,编译器为每个包含虚函数的类生成vtable,并在对象中添加vptr。在运行时,通过vptr指向的vtable实现动态绑定,从而实现多态。这种机制虽然灵活,但也带来了一定的性能开销。
3. 代码示例多态原理
#include<iostream>
using namespace std;
class Animal {
public:
virtual void speak() {
cout << "动物在说话" << endl;
}
};
class Cat : public Animal {
public:
void speak() {
cout << "猫在说话" << endl;
}
};
class Dog : public Animal {
public:
void speak() {
cout << "狗在说话" << endl;
}
};
void doSpeak(Animal& animal) {
animal.speak();
}
int main()
{
Cat cat;
doSpeak(cat);
Dog dog;
doSpeak(dog);
system("pause");
return 0;
}
这段代码通过虚函数实现了C++中的运行时多态。下面详细叙述其实现过程:
3.1 类结构与继承
- 代码中有一个基类
Animal
,它包含一个虚函数speak()
,表示动物发出声音的行为。 Cat
和Dog
是从Animal
类继承的派生类,它们分别重写了基类的speak()
函数,提供了自己的实现。
3.2 虚函数的作用
- 在基类
Animal
中,speak()
被声明为虚函数(virtual
关键字),这意味着派生类可以重写该函数,而当通过基类指针或引用调用该函数时,会根据对象的实际类型调用相应的重写函数。这就是运行时多态。
3.3 多态的实现过程
3.3.1 对象创建时
- 当
Cat
对象和Dog
对象在main()
函数中被创建时:- 编译器会为
Cat
和Dog
类分别生成虚函数表(vtable),表中记录它们重写的speak()
函数的地址。 - 每个
Cat
和Dog
对象会有一个虚函数指针(vptr),指向其对应类的虚函数表。
- 编译器会为
3.3.2 调用 doSpeak()
时的多态行为
- 在
main()
函数中,通过doSpeak(Animal& animal)
,使用基类Animal
的引用调用了不同对象的speak()
函数:- 当
doSpeak(cat)
被调用时:- 传入的
cat
是Cat
类型的对象,但它通过Animal& animal
基类引用传递。 - 由于
speak()
是虚函数,编译器会根据传入对象的实际类型(Cat
)查找Cat
类的虚函数表,找到Cat::speak()
函数的地址,并调用Cat
类的speak()
函数。 - 因此,输出为 “猫在说话”。
- 传入的
- 当
doSpeak(dog)
被调用时:- 传入的
dog
是Dog
类型的对象,但它同样通过Animal& animal
基类引用传递。 - 在运行时,编译器根据
dog
的实际类型(Dog
),查找Dog
类的虚函数表,找到Dog::speak()
的地址,调用Dog
类的speak()
函数。 - 因此,输出为 “狗在说话”。
- 传入的
- 当
3.3.3 总结
- 通过基类的引用
Animal&
来调用speak()
,编译器在运行时根据实际对象类型(Cat
或Dog
)来决定调用哪个重写的函数。 - 这种动态决策的过程就是运行时多态,它是通过虚函数表(vtable)和虚函数指针(vptr)机制来实现的。
3.4 输出结果
代码的结果:
猫在说话
狗在说话
4. 多态特点
优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
示例:用多态实现计算器
#include<iostream>
#include<string>
using namespace std;
class Calculator { //计算器类
public:
virtual int getResult() { //得到计算结果,用virtual修饰
return 0;
}
int m_A;
int m_B;
};
class AddCalculator : public Calculator { //加法类继承计算器类并重写getResult方法
public:
int getResult() {
return m_A + m_B;
}
};
class SubCalculator : public Calculator { 减法类继承计算器类并重写getResult方法
public:
int getResult() {
return m_A - m_B;
}
};
int main() {
Calculator* c = new AddCalculator; //实例化加法类,用父类指针指向
c->m_A = 10; //赋初值
c->m_B = 10; //赋初值
cout<<"加法运算结果为:"<<c->getResult()<<endl;
delete c; //释放堆内存空间
c = new SubCalculator; //实例化减法类,用父类指针指向
c->m_A = 10;
c->m_B = 10;
cout << "减法运算结果为:" << c->getResult() << endl;
delete c; //释放堆内存空间
}
5. 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
6. 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。具体而言就是在使用多态时,如果通过基类指针或引用来指向派生类对象,调用 delete
操作符时,只会调用基类的析构函数,不会调用派生类的析构函数。这样一来,如果派生类中有动态分配的资源,这些资源将无法正确释放,导致内存泄漏。
解决方法:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名(){}
纯虚析构语法:virtual ~类名() = 0;
还有:类名::~类名(){}
总结:
- 如果类中存在虚函数,并且你打算通过基类指针来操作派生类对象,必须将基类的析构函数声明为虚函数,以确保删除基类指针时,能够调用派生类的析构函数。
- 否则,派生类的析构函数不会被调用,可能会导致堆区资源无法正确释放,产生内存泄漏。
- 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
- 如果子类中没有堆区数据,可以不写虚析构或纯虚析构
- 拥有纯虚析构函数的类也属于抽象类