C++多态详解
一、多态的基本语法
1、C++的多态分为两种:
- 静态多态:如函数重载和运算符重载;
- 动态多态:派生类和虚函数实现运行时多态
2、静态多态和动态多态的区别:
- 静态多态的函数地址早绑定--编译阶段确定函数地址
- 动态多态的函数地址晚绑定--运行阶段确定函数地址
3、动态多态满足条件
- 有继承关系
- 子类要重写父类的虚函数
ps:
- 重写:函数名相同,返回值相同,传入值相同
- 重载:函数名相同,返回值不同,传入值不同
4、动态多态的使用
父类指针或引用指向子类对象
5、多态的好处
- 结构组织清晰
- 可读性强
- 对于前、后期扩展以及可维护性强
二、动态多态原理
现有如下代码:
class Animal{ pubulic: virtual void speak(){ cout << "动物在说话" << endl; } };
在有如上代码后,Animal的内部结构中会有一个4个字节大小的指针 vfptr(virtual function pointer,虚函数指针),vfpt会指向一个vftable(虚函数表),表的内部会记录一个虚函数的地址,表的内部结构如下:
如果有一个子类继承于Animal,则也会继承这个虚函数表(无重写)
代码:
class Cat :public Animal{ public: };
结构:
如果这个派生类中重写了父类的虚函数,则子类中的虚函数表会替换成 子类的虚函数地址
代码:
class Cat :public Animal{ public: virtual void speak(){ cout << "小猫在说话" << endl; } };
结构:
ps:父类的虚函数表不会发生改变
当父类的指针或引用指向子类对象时,就发生了多态,如:
Animal & animal = cat;
animal.speak();
animal传入的是Cat类型对象,所以当调用公共的speak函数的时候,它就会去Cat的虚函数表中去找它
三、纯虚函数和抽象类
在多态中,通常父类中的虚函数的实现是毫无意义的,主要都是调用子类重写的内容,
因此,可以将虚函数改为纯虚函数。
纯虚函数语法格式:
virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了虚函数,这个类也称作抽象类。
class Base{ public: virtual void func() = 0;//func()为纯虚函数,Base类就是抽象类 };
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象中的纯虚函数,否则也属于抽象类
多态的目的就是为了让函数的接口更通用化,通过一个父类指针,再根据创建的对象不同,可以调用多种形态的函数
多态就是一个接口拥有多种形态,传入的对象不一样,但因为是同一个接口,会实现不同的功能
四、虚析构和纯虚析构
1、C++堆区和栈区
- 堆区:用于程序员申请的内存空间
当连续的内存空间不足时会在闲置的栈区空间寻找大于分配空间的空间分配
- 栈区:存储程序运行时分配的变量
分配的物理空间是连续的,运行速度快,例如使用数组,但是一旦越界,结果将会不可控。
2、为什么要有虚析构和纯虚析构?
多态在使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方法:
将父类中的析构函数改为虚析构或者纯虚析构
3、虚析构和纯虚析构的共性
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
4、虚析构和纯虚析构的区别
如果是纯虚析构,那么类也就属于抽象类,无法实例化对象
5、虚析构和纯虚析构语法
//虚析构 virtual ~类名(){} //纯虚析构 virtual ~类名() = 0; 类名::~类名(){}
6、实例
class Animal { public: Animal() { cout << " Animal的构造函数" << endl; } virtual void Speak() = 0; /*析构函数加上virtual关键字加上 虚析构函数 virtual ~Animal() { cout << " Animal的虚析构函数" << endl; }*/ virtual ~Animal() = 0; }; Animal::~Animal() { cout << " Animal的纯虚析构函数" << endl; }
7、总结
- 虚析构和纯虚析构就是用来解决通过父类指针释放子类对象
- 如果子类中没有堆区数据,可以不写虚析构或纯析构
- 拥有纯析构函数的类也属于抽象类,无法实例化