一、多态的基本概念
多态是面向对象程序设计语言中除数据抽象和继承之外的第三个基本特征。
多态:父类的引用或者指针指向子类对象
C++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。
#include <iostream>
using namespace std;
class Animal{
public:
void speak() {
cout << "动物在说话" << endl;
}
};
class Cat: public Animal{
public:
void speak() {
cout << "小猫在说话" << endl;
}
};
void doSpeak(Animal & animal) {
animal.speak();
}
//如果发生继承关系,编译器允许进行类型转换
void test() {
Cat cat;
doSpeak(cat); //会输出动物在说话
}
int main() {
test();
system("pause");
return 0;
}
早绑定,静态联编:上述结果会输出动物在说话,因为在调用doSpeak的时候,函数的地址已经绑定,直接找了animal类型的speak。
动态联编:如果想调用猫的speak,不能提前绑定函数地址,需要在运行时确定函数地址。动态联编的写法是将doSpeak写成虚函数 -- 在父类声明虚函数(子类中可写可不写),发生多态。
#include <iostream>
using namespace std;
class Animal{
public:
virtual void speak() {
cout << "动物在说话" << endl;
}
};
class Cat: public Animal{
public:
void speak() {
cout << "小猫在说话" << endl;
}
};
void doSpeak(Animal & animal) { //Animal & animal = cat
animal.speak();
}
//如果发生继承关系,编译器允许进行类型转换
void test() {
Cat cat;
doSpeak(cat); //会输出小猫在说话
}
int main() {
test();
system("pause");
return 0;
}
二、多态原理解析
Animal内部结构:
- vfptr虚函数表指针,指针指向虚函数表,构造函数中会将虚函数表指针指向自己的虚函数表
- 虚函数表中存放的是所有虚函数的地址
- 子类写父类的虚函数speak,这种写法叫重写
- 如果发生重写,会替换掉虚函数表原有的地址,比如Cat虚函数表会用&Cat::speak替换&Animal::speak
- 重写必须返回值、参数个数、类型、顺序都相同
以下函数会输出猫在说话,父类指针指向子类函数
void test02(){
Animal* animal = new Cat;
animal->speak();
}
三、多态案例-计算器案例
开发的原则:开闭原则 -- 对扩展开发,对修改关闭
利用多态实现计算器能够有利于代码后期的维护和扩展,无需修改原有代码,但缺点是效率较低,因为发生多态后内部的结构会更加复杂。
//利用多态实现计算器 -- 有利于维护和扩展,无需修改原有代码
#include<iostream>
using namespace std;
//抽象计算器类
class Calculator {
public:
virtual int getResult() {
return 0;
}
void setv1(int v) {
val1 = v;
}
void setv2(int v) {
val2 = v;
}
int val1;
int val2;
};
//加法计算器类 -- 继承抽象计算器类
class PluseCalculator : public Calculator {
public:
int getResult() { //重写父类的函数
return val1 + val2;
}
};
//减法计算器类 -- 继承抽象计算器类
class SubCalculator : public Calculator {
public:
int getResult() { //重写父类的函数
return val1 - val2;
}
};
//进行测试
void test01() {
Calculator* abc;
//加法计算
abc = new PluseCalculator;
abc->setv1(10);
abc->setv2(20);
cout << abc->getResult() << endl;
delete abc; //不赋为空
//减法运算
abc = new SubCalculator;
abc->setv1(10);
abc->setv2(20);
cout << abc->getResult() << endl;
return;
}
int main() {
test01();
system("pause");
return 0;
}
四、抽象类和纯虚函数
虚函数 -- 上面案例中父类求结果的函数为虚函数 virtual int getResult() {return 0;}
由于该函数没有实质内容,所以可以写成纯虚函数。
纯虚函数 -- virtual int getResult() = 0;
- 如果父类中有纯虚函数,则子类继承时必须要实现纯虚函数。
- 如果父类中有纯虚函数,则这个父类就无法实例化对象。
- 有纯虚函数的类通常称为抽象类,所以抽象类无法实例化对象。
五、虚析构和纯虚析构
以下代码中希望调用的是Cat的析构函数,但是实际运行中调用的是animal的析构函数,因为普通的析构是不会调用子类的析构,所以可能会造成释放不干净。
#include <iostream>
using namespace std;
class Animal{
public:
virtual void speak() {
cout << "动物在说话" << endl;
}
~Animal() {
cout << "Animal的析构调用" << endl;
}
};
class Cat: public Animal{
public:
Cat(const char* name) {
this->m_Name = new char[strlen(name)+1];
strcpy(this->m_Name, name);
}
void speak() {
cout << "小猫在说话" << endl;
}
~Cat() {
cout << "Cat的析构调用" << endl;
if(this->m_Name != NULL) {
delete[] this->m_Name;
this->m_Name = NULL;
}
}
char* m_Name;
};
//如果发生继承关系,编译器允许进行类型转换
void test() {
Animal* animal = new Cat("TOM");
animal->speak(); //会输出小猫在说话
delete animal; //调用的是animal的析构函数
}
int main() {
test();
system("pause");
return 0;
}
利用虚析构解决通过父类指针指向子类对象释放时释放不干净导致的问题。
virtual ~Animal() {
cout << "Animal的虚析构调用" << endl;
}
纯虚析构:
纯虚析构需要声明,也需要实现(因为父类的析构也会调用到),并且在类内声明,类外实现。
如果类中出现了纯虚析构函数,这个类也算抽象类,不能实例化对象。
class Animal{
public:
virtual void speak() {
cout << "动物在说话" << endl;
}
virtual ~Animal() = 0;
};
Animal::~Animal() {
cout << "Animal的纯虚析构调用" << endl;
}
六、向上类型转换和向下类型转换
向上类型转换:派生类转换为基类,安全,不会有数据的丢失。
Cat* cat = new Cat;
Animal* animal = (Animal*) cat;
向下类型转换:基类转换为派生类,不安全,会导致数据的丢失,原因是父类的指针或者引用的内存中可能不包含子类的成员的内存。
Animal* animal = new Animal;
Cat* cat = (Cat*) animal;
如果发生了多态,向上和向下类型转换都是安全的,强转之后可以操作全部的内存空间。