C++之 多态(Polymorphism)
目录
一、基本概念
多态的使用:
案例一——计算机类
多态的优点:
二、纯虚函数与抽象类
特点:
①无法实例化对象
案例二——制作饮品
三、虚析构与纯虚析构
因为父类指针在析构时,不会调用子类中的析构函数,从而导致堆区属性未释放
解决:将父类虚构改为虚析构
案例三——电脑组装
一、基本概念
多态是C++三大特性之一
分为、作用与区别:
作用 | 区别 | |
静态多态 | 函数重载 和 运算符重载,复用函数名 | 函数地址早绑定,编译阶段确定函数地址 |
动态多态 | 派生类 和 虚函数 实现运行时多态 | 函数地址晚绑定,运行阶段确定函数地址 |
动态多态需要满足:
①有继承关系
②子类重写父类的虚函数
重写:子类的 返回值 函数名 形参列表 要与父类完全相同,但virtual可写可不写
多态的使用:
父类指针或引用子类对象
例:
创建Animal与Cat与Dog类,其中每个都有dodif函数,最后单独实现dodif函数,创建测试函数并调用dodif函数
class Animal
{
public:void dodif(){cout << "动物" << endl;}
};
class Cat : public Animal
{
public:void dodif(){cout << "小猫" << endl;}
};
class Dog : public Animal
{
public:void dodif(){cout << "小狗" << endl;}
};
void dodif(Animal &animal)// Animal &animal = 子类传来的对象; 多态的使用
{animal.dodif();
}
void test01()
{Cat cat;dodif(cat);Cat dog;dodif(dog);
}
可以发现,无论我传参是什么,结果总是输出动物,也就是调用父类中的函数
这是因为dodif函数是静态多态,地址早绑定,在编译阶段就确定了函数地址,因此后面我怎么传参都是访问已经确定的函数地址
因此需要改为动态多态:父类中,函数前加virtual,使其变成虚函数,地址晚绑定
class Animal
{
public:virtual void dodif() // 虚函数{cout << "动物" << endl;}
};
接下来深度分析一下上面代码的实现逻辑:
首先,没有加virtual时,我们输出一下父类的大小
结果是1,说明是个空对象
加上virtual,再次输出
变成8,其实是变成了一个指针,即 虚函数指针cfptr
接下来,画出Animal类的内部结构
使用开发人员命令提示符工具查看
然后是,继承的Cat类的内部结构,此时并无重写
虚函数表内仍然是&Animal::dodif
而当我们加上重写后
虚函数表内就改成了&Cat::dodif
因为重写后,子类中的虚函数表会被替换成子类的虚函数地址
案例一——计算机类
使用普通方法和多态2种方法实现计算器类
①普通方法
class caculator
{
public:int getResult(){cin >> num1 >> oper >> num2;if (oper == "+"){return num1 + num2;}else if (oper == "-"){return num1 - num2;}else if (oper == "*"){return num1 * num2;}}string oper;int num1;int num2;
};
void test01()
{caculator c;int ret = c.getResult();cout << ret << endl;
}
现在实现了加法、减法和乘法三种功能,但是如果想要增加功能,则需要修改源码
实际开发中提倡开闭原则:对扩展进行开放,对修改进行关闭
②多态的方法
class caculator_abstract // 计算器抽象类
{
public:virtual int getResult(){return 0;}int num1;int num2;
};class Add:public caculator_abstract // 加法 Addition
{int getResult(){return num1 + num2;}
};
class Sub :public caculator_abstract // 减法 subtraction
{int getResult(){return num1 - num2;}
};
class Mul :public caculator_abstract // 乘法 multiplication
{int getResult(){return num1 * num2;}
};
class Div :public caculator_abstract // 除法 division
{int getResult(){return num1 / num2;}
};
调用
void test02()
{// 多态的使用方法://父类指针或者引用子类对象caculator_abstract* c = new Add; // 加法调用c->num1 = 10;c->num2 = 5;cout << c->getResult() << endl;delete c;// 堆区的数据,记得销毁c = new Sub;c->num1 = 10;c->num2 = 5;cout << c->getResult() << endl; // 减法调用delete c;c = new Mul;c->num1 = 10;c->num2 = 5;cout << c->getResult() << endl; // 乘法调用delete c;c = new Div;c->num1 = 10;c->num2 = 5;cout << c->getResult() << endl; // 除法调用delete c;
}
多态的优点:
①组织结构清晰
②可读性强
③对于扩展以及维护能力高
二、纯虚函数与抽象类
在多态中,通常父类中虚函数的实现没有什么意义,主要是调用子类重写的内容
因此可将父类中的虚函数改为纯虚函数
纯虚函数:写一个虚函数,使其 = 0,即为纯虚函数
语法:virtual 返回值类型 函数名 (参数列表)= 0;
而当类中有了纯虚函数,这个类也称为抽象类
特点:
①无法实例化对象
class Base
{
public:virtual void func() = 0;
};void test01()
{Base b;new Base;
}
无论是栈上还是堆区都无法实例化对象
②子类必须重写父类中的纯虚函数,否则也属于抽象类
首先,不重写:
class Son :public Base
{
public:};void test01()
{Son s;new Son;
}
一样的无法实例化对象
加上重写
class Son :public Base
{
public:virtual void func() {}; // 重写
};
即使重写是空实现,仍然可以实例化对象
简单修改一下
class Son :public Base
{
public:virtual void func() {cout << "Son_func的调用" << endl;}; // 重写
};void test01()
{Son s;s.func();// 多态的方法:Base* base = new Son;base->func();
}
案例二——制作饮品
简单制作一份饮品,咖啡or沏茶
咖啡:①煮水②冲泡咖啡③倒入杯中④加糖与牛奶
沏茶:①煮水②冲泡茶叶③倒入杯中④加柠檬
流程大致相同:①煮水②冲泡③倒入杯中④加佐料
class AbstactDrinking // 抽象类的饮品
{
public:virtual void Boil() = 0;virtual void Brew() = 0;virtual void Pour() = 0;virtual void Put() = 0;void makeDriking(){Boil();Brew();Pour();Put();}
};
class tea :public AbstactDrinking // 茶的具体实现
{
public:virtual void Boil(){cout << "煮水" << endl;}virtual void Brew(){cout << "冲泡茶叶" << endl;}virtual void Pour(){cout << "倒入杯中" << endl;}virtual void Put() {cout << "加柠檬" << endl;}
};
class coffee :public AbstactDrinking // 咖啡的具体实现
{
public:virtual void Boil(){cout << "煮水" << endl;}virtual void Brew(){cout << "冲泡咖啡" << endl;}virtual void Pour(){cout << "倒入杯中" << endl;}virtual void Put(){cout << "加糖与牛奶" << endl;}
};
void doDrinking(AbstactDrinking* abd)
{abd->makeDriking();delete abd; // new出的堆区数据,记得销毁
}
void test01()
{doDrinking(new tea);cout << "-----------------" << endl;doDrinking(new coffee);
}
没啥说的
三、虚析构与纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,导致子类的堆区属性未被释放,引起内存泄漏
解决方法:将父类中的析构函数改为虚析构或纯虚析构
共同点 | 不同点 | |
虚析构 | 可以解决父类指针释放子类对象 | \\ |
纯虚析构 | 都需要有具体的函数实现 | 属于抽象类,无法实例化对象 |
语法:
虚析构 | virtual~类名() {}; |
纯虚析构 | virtual~类名() = 0 ; 声明 类名 : : 类名() {} ;实现 |
接下来创建父类Animal类与子类Cat类
class Animal
{
public:virtual void speak() = 0;
};
class Cat:public Animal
{
public:virtual void speak(){cout << "小猫" << endl;}
};
void test01()
{Animal* animal = new Cat;animal->speak();delete animal;
}
接下来在Cat类中增加string属性c_name;
class Cat:public Animal
{
public:Cat(string name){c_name = new string(name); // 堆区创建,应有释放}virtual void speak(){cout << *c_name << "小猫" << endl;}string *c_name;
};
void test01()
{Animal* animal = new Cat("yomi");animal->speak();delete animal;
}
而c_name是堆区开辟的内存,应有释放,补全Cat的析构:
~Cat(){if (c_name != NULL){cout << "Cat的析构" << endl;delete c_name;c_name = NULL;}}
同时,我们把父类Animal的析构和构造以及子类Cat的构造也补全:
class Animal
{
public:Animal(){cout << "Animal的构造" << endl;}~Animal(){cout << "Animal的析构" << endl;}virtual void speak() = 0;
};
class Cat:public Animal
{
public:Cat(string name){cout << "Cat的构造" << endl;c_name = new string(name); // 堆区创建,应有释放}virtual void speak(){cout << *c_name << "小猫" << endl;}string *c_name;~Cat(){if (c_name != NULL){cout << "Cat的析构" << endl;delete c_name;c_name = NULL;}}
};
运行可以发现,没有子类Cat的析构,说明堆区new出的c_name属性并未释放
因为父类指针在析构时,不会调用子类中的析构函数,从而导致堆区属性未释放
解决:将父类虚构改为虚析构
virtual ~Animal(){cout << "Animal的析构" << endl;}
就走了子类的析构
接下来实现纯虚析构
class Animal
{
public:Animal(){cout << "Animal的构造" << endl;}virtual ~Animal() = 0; // 纯虚析构virtual void speak() = 0; // 虚函数
};
如果只在父类中写 virtual ~Animal() = 0; 运行将报错
因为这个相当于声明,没有实现
接下来在类外实现纯虚析构的实现
Animal::~Animal() // 纯虚析构的实现
{cout << "Animal的纯虚析构" << endl;
}
因为如果父类有开辟堆区的属性,也需要释放,因此需要纯虚析构的实现
同时,当类中有了纯虚析构,这个类也属于抽象类,无法实例化对象
总结:
①虚析构或纯虚析构是用来解决通过父类指针释放子类对象
②如果子类没有堆区数据,可以不写虚析构或纯虚析构
③纯虚析构的类也属于抽象类,无法实例化对象
案例三——电脑组装
电脑主要组成部件是处理器(计算)+显卡(显示)+内存条(存储)
将每个零件封装成抽象基类,并提供不同的厂商生产不同的零件
class CPU
{
public:// 抽象计算函数virtual void calculate() = 0;
};
class GraphicsCard
{
public:// 抽象显示函数virtual void display() = 0;
};
class Memory
{
public:// 抽象存储函数virtual void storage() = 0;
};
具体厂商零件:
// 具体零件厂商
// 1、Inter
class InterCPU :public CPU
{virtual void calculate(){cout << "Inter_CPU" << endl;}
};
class InterGPU :public GraphicsCard
{virtual void display(){cout << "Inter_GPU" << endl;}
};
class InterRAM :public Memory
{virtual void storage(){cout << "Inter_RAM" << endl;}
};
// 2、lenovo
class LenovoCPU :public CPU
{void calculate(){cout << "Lenovo_CPU" << endl;}
};
class LenovoGPU :public GraphicsCard
{void display(){cout << "Lenovo_GPU" << endl;}
};
class LenovoRAM :public Memory
{void storage(){cout << "Lenovo_RAM" << endl;}
};
创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
// 电脑类的实现
class Computer
{
public:Computer(CPU* cpu, GraphicsCard* gpu, Memory* ram){c_cpu = cpu;c_gpu = gpu;c_ram = ram;}void dowork() // 工作接口{c_cpu->calculate();c_gpu->display();c_ram->storage();}
private:CPU* c_cpu;GraphicsCard* c_gpu;Memory* c_ram;
};
测试时组装3种不同的电脑进行工作
void test01()
{// 创建第一台电脑的零件CPU* interCpu = new InterCPU;GraphicsCard* interGpu = new InterGPU;Memory* interRam = new InterRAM;// 创建第一台电脑Computer* computer1 = new Computer(interCpu, interGpu, interRam);computer1->dowork(); // 调用工作函数delete computer1;
}
在computer1工作完后,我们将其delete,但仍有一个问题:
上面new出的3个零件尚未delete,下面有2个方法
①在最后delete computer后,接着delete3个零件
②在电脑类中,提供析构函数,释放3个零件,下面实现方法②
~Computer(){if (c_cpu != NULL) // 释放cpu{delete c_cpu;c_cpu = NULL;}if (c_gpu != NULL) // 释放gpu{delete c_gpu;c_gpu = NULL;}if (c_ram!= NULL) // 释放ram{delete c_ram;c_ram = NULL;}}
这样就解决了堆区的问题
创建另外2台电脑
2:
void test01()
{// 创建第二台电脑的零件CPU* lenovoCpu = new LenovoCPU;GraphicsCard* lenovoGpu = new LenovoGPU;Memory* lenovoRam = new LenovoRAM;// 创建第二台电脑Computer* computer2 = new Computer(lenovoCpu, lenovoGpu, lenovoRam);computer2->dowork(); // 调用工作函数delete computer2;
}
3:
// 创建第三台电脑的零件CPU* InterCpu = new InterCPU;GraphicsCard* InterGpu = new InterGPU;Memory* lenovoRam = new LenovoRAM;// 创建第三台电脑Computer* computer3 = new Computer(InterCpu, InterGpu, lenovoRam);computer3->dowork(); // 调用工作函数delete computer3;