多态
OVERVIEW
一、多态
1.基本概念:
多态是面向对象三大特性之一,多态分为两类:静态多态、动态多态
- 静态多态:函数重载和运算符重载属于静态多态(复用函数名)
- 动态多态:派生类和虚函数实现运行时多态
静态多态与动态多态的区别:
静态多态的函数地址早绑定,编译阶段确定函数地址。
动态多态的函数地址晚绑定,运行阶段确定函数地址。
动态多态满足条件:
- 有继承关系
- 子类需要重写父类的虚函数
地址绑定早,在编译阶段确定函数地址:
#include<iostream>
using namespace std;
class Animal{
public:
void speak(){
cout << "动物在说话" << endl;
}
};
class Cat:public Animal{
public:
void speak(){
cout << "小猫在说话" << endl;
}
};
class Dog:public Animal{
public:
void speak(){
cout << "小狗在说话" << endl;
}
};
//地址早绑定 在编译阶段确定函数地址:Animal &animal = animal;
Animal &animal = cat;
void doSpeak(Animal &animal){
animal.speak();
}
void test01(){
Cat cat;
doSpeak(cat);
Dog dog;
doSpeak(dog);
}
int main(){
test01();
system("pause");
return 0;
}
在父类的speak函数前添加virtual
关键字转为虚函数(子类再重写函数),实现动态多态。
地址晚绑定,在运行阶段进行绑定:
#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;
}
};
//地址晚绑定 在运行阶段进行绑定:Animal &animal = cat; Animal &animal = dog;
void doSpeak(Animal &animal){
animal.speak();
}
void test01(){
Cat cat;
doSpeak(cat);
Dog dog;
doSpeak(dog);
}
int main(){
test01();
system("pause");
return 0;
}
注意:与函数重载不同,函数重写是指重写的函数的返回值类型、函数名、参数列表完全一致。
父类的指针 or 引用指向子类对象Cat、Dog
2.E1-计算器类
多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
分别使用普通写法和多态技术,设计实现两个操作数进行运算的计数器类:
#include<iostream>
using namespace std;
class Calculator{
public:
int getResult(string oper){
if(oper == "+"){
return num1 + num2;
}else if(oper == "-"){
return num1 - num2;
}else if(oper == "*"){
return num1 * num2;
}
}
int num1;
int num2;
};
void test01(){
Calculator c;
c.num1 = 10;
c.num2 = 20;
cout << c.num1 << " + " << c.num2 << " = " << c.getResult("+") << endl;
cout << c.num1 << " - " << c.num2 << " = " << c.getResult("-") << endl;
cout << c.num1 << " * " << c.num2 << " = " << c.getResult("*") << endl;
}
int main(){
test01();
system("pause");
return 0;
}
如果需要扩展计算机的新功能,需要对源码进行修改。
#include<iostream>
using namespace std;
//实现计数器的基类
class AbstractCalculator{
public:
virtual int getResult(){
return 0;
}
int num1;
int num2;
};
class AddCalculator:public AbstractCalculator{
public:
int getResult(){
return num1 + num2;
}
};
class SubCalculator:public AbstractCalculator{
public:
int getResult(){
return num1 - num2;
}
};
class MulCalculator:public AbstractCalculator{
public:
int getResult(){
return num1 * num2;
}
};
void test01(){
AbstractCalculator *abc;
//1.加法运算
abc = new AddCalculator;
abc->num1 = 10;
abc->num2 = 30;
cout << abc->num1 << " + " << abc->num2 << " = " << abc->getResult() << endl;
//new创建的数据存放在堆区,使用后需要手动释放
delete(abc);
//2.减法运算
abc = new SubCalculator;
abc->num1 = 10;
abc->num2 = 30;
cout << abc->num1 << " - " << abc->num2 << " = " << abc->getResult() << endl;
//new创建的数据存放在堆区,使用后需要手动释放
delete(abc);
//3.乘法运算
abc = new MulCalculator;
abc->num1 = 10;
abc->num2 = 30;
cout << abc->num1 << " * " << abc->num2 << " = " << abc->getResult() << endl;
//new创建的数据存放在堆区,使用后需要手动释放
delete(abc);
}
int main(){
test01();
system("pause");
return 0;
}
3.纯虚函数&抽象类:
在多态中通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容(因此可以将虚函数改为纯虚函数)
当一个类中有了纯虚函数,这个类也可称为抽象类。
纯虚函数语法:virtual 返回值类型 函数名(参数列表) = 0;
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
#include<iostream>
using namespace std;
class Base{
public:
virtual void func() = 0;
};
class Son1:public Base{
virtual void func(){
cout << "Son1::func()函数调用" << endl;
}
};
class Son2:public Base{
virtual void func(){
cout << "Son2::func()函数调用" << endl;
}
};
void test01(){
Base *base;
base = new Son1;
base->func();
delete(base);
base = new Son2;
base->func();
delete(base);
}
int main(){
test01();
system("pause");
return 0;
}
4.E2-制作饮品
制作饮品的流程分为:煮水->冲泡->倒入杯中->加入辅料,利用多态技术实现本案例:
#include<iostream>
using namespace std;
class Base{
public:
virtual void Boil() = 0;
virtual void Brew() = 0;
virtual void PourInCup() = 0;
virtual void PutSomething() = 0;
void makeDrink(){
Boil();
Brew();
PourInCup();
PutSomething();
}
};
class Coffee:public Base{
virtual void Boil(){
cout << "煮一壶自来水" << endl;
}
virtual void Brew(){
cout << "冲泡即食咖啡" << endl;
}
virtual void PourInCup(){
cout << "倒入咖啡杯中" << endl;
}
virtual void PutSomething(){
cout << "加入糖和牛奶" << endl;
}
};
class Tea:public Base{
virtual void Boil(){
cout << "煮一壶自来水" << endl;
}
virtual void Brew(){
cout << "冲泡茶叶" << endl;
}
virtual void PourInCup(){
cout << "倒入茶杯中" << endl;
}
virtual void PutSomething(){
cout << "加入枸杞" << endl;
}
};
void doWork(Base *base){
base->makeDrink();
delete base;
}
void test01(){
doWork(new Coffee);
cout << "--------------------" << endl;
doWork(new Tea);
}
int main(){
test01();
system("pause");
return 0;
}
注意:该案例中另外创建了doWork函数,将函数调用操作与堆区内存释放操作简化(只保留父类指针指向修改)优化程序结构。
5.虚析构&纯虚析构:
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
需要将父类中的析构函数改为虚构函数 or 存虚构函数。
虚析构:virtual ~类名(){}
纯虚析构:virtual ~类名() = 0; 类名::~类名(){}
虚析构与纯虚构共性:
- 可以解决父类指针释放,无法调用子类析构函数
- 都需要有具体的函数实现,否则报错。
虚析构与纯虚构区别:
- 如果是纯虚析构,该类属于抽象类(无法实例化)
#include<iostream>
using namespace std;
class Animal{
public:
Animal(){
cout << "Animal构造函数调用" << endl;
}
virtual void speak() = 0;
~Animal(){
cout << "Animal析构函数调用" << endl;
}
};
class Cat:public Animal{
public:
Cat(string s){
cout << "Cat构造函数调用" << endl;
name = new string(s);
}
virtual void speak(){
cout << *name << "小猫在说话" << endl;
}
~Cat(){
if(name != NULL){
cout << "Cat析构函数调用" << endl;
delete name;
name = NULL;
}
}
string *name;//让小猫的name数据创建在堆区,利用一个指针去维护
};
void test01(){
Animal *animal;
animal = new Cat("Tom");
animal->speak();
delete animal;
}
int main(){
test01();
system("pause");
return 0;
}
父类指针在释放时,不会调用到子类的析构函数(子类如果有堆区数据,会出现内存泄漏)。
为父类Animal析构函数添加virtual
关键字,将子类的析构函数改为虚析构解决。
子类对象Cat析构函数调用,堆区内存被释放。
6.E3-电脑组装
电脑主要组成部件为CPU、显卡、内存条。
将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenove厂商
创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口,测试时组装三台不同的电脑进行工作:
#include<iostream>
using namespace std;
class CPU{
public:
virtual void calculate() = 0;
};
class VideoCard{
public:
virtual void display() = 0;
};
class Memory{
public:
virtual void storage() = 0;
};
class Computer{
public:
Computer(CPU *c, VideoCard *v, Memory *m){
cpu = c;
vcd = v;
mem = m;
}
//(1)提供工作的函数
void work(){
cpu->calculate();
vcd->display();
mem->storage();
}
//(2)提供析构函数,释放开辟在堆区内存的三个电脑零件数据
~Computer(){
if(cpu != NULL){
delete cpu;
cpu = NULL;
}
if(vcd != NULL){
delete vcd;
vcd = NULL;
}
if(mem != NULL){
delete mem;
mem = NULL;
}
}
private:
CPU *cpu;
VideoCard *vcd;
Memory *mem;
};
//Intel厂商
class IntelCPU:public CPU{
public:
void calculate(){
cout << "Intel的CPU开始计算了" << endl;
}
};
class IntelVideoCard:public VideoCard{
public:
void display(){
cout << "Intel的VideoCard开始显示了" << endl;
}
};
class IntelMemory:public Memory{
public:
void storage(){
cout << "Intel的内存条开始存储了" << endl;
}
};
//Lenovo厂商
class LenovoCPU:public CPU{
public:
void calculate(){
cout << "Lenovo的CPU开始计算了" << endl;
}
};
class LenovoVideoCard:public VideoCard{
public:
void display(){
cout << "Lenovo的VideoCard开始显示了" << endl;
}
};
class LenovoMemory:public Memory{
public:
void storage(){
cout << "Lenovo的内存条开始存储了" << endl;
}
};
void test01(){
//1.创建第一台电脑
CPU *cpu = new IntelCPU;
VideoCard *vcd = new IntelVideoCard;
Memory *mem = new IntelMemory;
Computer *pc1 = new Computer(cpu, vcd, mem);
pc1->work();
delete pc1;
//2.创建第二台电脑
Computer *pc2 = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);
pc2->work();
delete pc2;
}
int main(){
test01();
system("pause");
return 0;
}
核心内容:
- 对于零件层(都是抽象处理、纯虚函数),不同的厂商实现不同的零件(具体厂商子类实现)
- 用三个指针接收数据开辟在堆区的三个零件,通过一个
work()
函数使三个零件工作(堆区开辟的数据需利用父类析构函数释放) - 利用多态的架构实现优化程序结构
二、运算符重载
c++中的运算符重载是指,对已有的运算符进行重新定义(赋予其另一种功能),以适应不同的数据类型。
1.加号运算符重载:
- 成员函数方式重载+运算符:
p1.operator+(p2)
- 全局函数方式重载+运算符:
operator+(p1, p2)
通过重载加号运算符,可以实现自定义数据类型运算规则的定制。
#include<iostream>
using namespace std;
class Person{
public:
int a;
int b;
//1.成员函数重载+号本质:Person p3 = p1.operator+(p2);
/*Person operator+(Person &p){
Person temp;
temp.a = this->a + p.a;
temp.b = this->b + p.b;
return temp;
}
*/
};
//2.全局函数重载+号本质:Person p3 = operator+(p1, p2);
Person operator+(Person &p1, Person &p2){
Person temp;
temp.a = p1.a + p2.a;
temp.b = p1.b + p2.b;
return temp;
}
Person operator+(Person &p1, int num){
Person temp;
temp.a = p1.a + num;
temp.b = p1.b + num;
return temp;
}
void test01(){
Person p1;
p1.a = 10;
p1.b = 10;
Person p2;
p2.a = 20;
p2.b = 20;
Person p3 = p1 + p2;
cout << "p3.a = " << p3.a << endl;
cout << "p3.b = " << p3.b << endl;
Person p4 = p3 + 50;
cout << "p4.a = " << p4.a << endl;
cout << "p4.b = " << p4.b << endl;
}
int main(){
test01();
system("pause");
return 0;
}
注意:对于内置的数据类型的表达式的运算符是不可能改变的(对于int、double等内置数据类型的运算规则已经不能修改)
2.左移>>运算符重载:
通过重载左移运算符,可以输出自定义的数据类型。
#include<iostream>
#include<string>
using namespace std;
class Person{
public:
int a;
int b;
//1.不能利用成员函数重载<<运算符,因为无法实现cout出现在左侧
//p.operator<<(cout) === p << cout
};
//2.只能利用全局函数重载<<运算符
//operator<<(cout, p) === cout << p
ostream & operator<<(ostream &out, Person &p){//cout对象全局只有一个需要利用&引用的方式传递
out << "a = " << p.a << " " << "b = " << p.b;
return out;
}
void test01(){
Person p1;
p1.a = 10;
p1.b = 10;
Person p2;
p2.a = 20;
p2.b = 20;
cout << p1 << endl;
cout << p2 << endl;
}
int main(){
test01();
system("pause");
return 0;
}
注意:要进一步理解左移运算符重载,必须深刻理解cout与cin对象(cout属于ostream类型、cin属于istream类型)
3.递增++运算符重载:
通过重载递增运算符++,可以自己模拟一个整型变量。
step1:首先重载左移运算符,实现对自定义的整型MyInteger
的输出操作。
#include<iostream>
#include<string>
using namespace std;
class MyInteger{
friend ostream& operator<<(ostream& out, MyInteger myint);
public:
MyInteger(){
num = 0;
}
private:
int num;
};
//利用全局函数重载<<运算符
ostream& operator<<(ostream& out, MyInteger myint){//cout对象全局只有一个需要利用&引用的方式传递
out << myint.num;
return out;
}
void test01(){
MyInteger myint;
cout << myint << endl;
}
int main(){
test01();
system("pause");
return 0;
}
step2:对自增运算符进行重载,实现自定义整型的自增操作(需要分别重载前置自增 与 后置自增)。
#include<iostream>
#include<string>
using namespace std;
class MyInteger{
friend ostream& operator<<(ostream& out, MyInteger myint);
public:
MyInteger(){
num = 0;
}
//1.重载前置自增运算符,返回引用是实现一直对一个数据类型进行操作
MyInteger& operator++(){
num++;
return *this;
}
//2.重载后置自增运算符,返回的是一个值(若返回引用&temp释放后为非法操作)
MyInteger operator++(int){
MyInteger temp = *this;
num++;
return temp;
}
private:
int num;
};
//利用全局函数重载<<运算符
ostream& operator<<(ostream& out, MyInteger myint){//cout对象全局只有一个需要利用&引用的方式传递
out << myint.num;
return out;
}
void test01(){
MyInteger myint;
cout << ++(++myint) << endl;
cout << myint << endl;
}
void test02(){
MyInteger myint;
cout << myint++ << endl;
cout << myint << endl;
}
int main(){
cout << "前置自增测试" << endl;
test01();
cout << "后置自增测试" << endl;
test02();
system("pause");
return 0;
}
练习:对比自增重载的过程,尝试完成自减运算符的重载操作。
4.赋值=运算符重载:
cpp编译器至少会给一个类添加4个函数:
默认构造函数、默认析构函数、默认拷贝构造函数(对属性进行值拷贝)、赋值运算符operator=(对属性进行值拷贝)
重载赋值运算符=,可以实现自定义数据类型的赋值操作。
需要注意:如果类中有属性指向堆区,做赋值操作时会出现深浅拷贝问题。
#include<iostream>
#include<string>
using namespace std;
class Person{
public:
Person(int n){
age = new int(n);//将数据创建在堆区,并利用age指针维护堆区的数据
}
~Person(){
if(age != NULL){
delete age;
age = NULL;
}
}
int *age;
};
void test01(){
Person p1(18);
Person p2(20);
p2 = p1;
cout << "p1的年龄age指针指向的地址为:" << p1.age << endl;
cout << "p1的年龄age指针解引用后为:" << *p1.age << endl;
cout << "p2的年龄age指针指向的地址为:" << p2.age << endl;
cout << "p2的年龄age指针解引用后为:" << *p2.age << endl;
}
int main(){
test01();
system("pause");
return 0;
}
返回值出现异常-1073740940
简单的赋值=
操作中存在的浅拷贝问题,使得堆区内存重复释放(报错),必须利用深拷贝解决。
重载赋值运算符=
,利用深拷贝解决浅拷贝中出现的堆区内存重复释放问题:
#include<iostream>
using namespace std;
class Person{
public:
Person(int n){
age = new int(n);//将数据创建在堆区,并利用age指针维护堆区的数据
}
~Person(){
if(age != NULL){
delete age;
age = NULL;
}
}
//重载赋值运算符
Person& operator=(Person &p){
//1.age = p.age;(编译器默认提供浅拷贝)
//2.应该先判断是否有属性在堆区,如果有先释放干净然后再进行深拷贝
if(age != NULL){
delete age;
age = NULL;
}
age = new int(*p.age);//深拷贝操作
//3.返回对象本身
return *this;
}
int *age;
};
void test01(){
Person p1(18);
Person p2(20);
Person p3(24);
p2 = p1;
cout << "p1的年龄age指针指向的地址为:" << p1.age << endl;
cout << "p1的年龄age指针解引用后为:" << *p1.age << endl;
cout << "p2的年龄age指针指向的地址为:" << p2.age << endl;
cout << "p2的年龄age指针解引用后为:" << *p2.age << endl;
p3 = p2 = p1;
cout << "p3的年龄age指针指向的地址为:" << p3.age << endl;
cout << "p3的年龄age指针解引用后为:" << *p3.age << endl;
}
int main(){
test01();
system("pause");
return 0;
}
总结:深拷贝不同于浅拷贝只是做了简单的指针复制操作(每个地址都不同),另外在cpp中连续的赋值操作是被允许的。
5.关系运算符重载:
重载关系运算符,可以实现自定义数据类型的比较操作。
#include<iostream>
#include<string>
using namespace std;
class Person{
public:
Person(string n, int a){
name = n;
age = a;
}
//重载关系运算符==
bool operator==(Person &p){
if(this->name == p.name && this->age == p.age) {
return true;
}
return false;
}
bool operator!=(Person &p){
if(this->name == p.name && this->age == p.age) {
return false;
}
return true;
}
string name;
int age;
};
void test01(){
Person p1("lch", 21);
Person p2("luochenhao", 21);
Person p3("lch", 21);
cout << "p1 != p2:" << (p1 != p2) << endl;
cout << "p1 == p3:" << (p1 == p3) << endl;
cout << "p2 == p3:" << (p2 == p3) << endl;
}
int main(){
test01();
system("pause");
return 0;
}
6.函数调用运算符重载:
函数调用运算符()
也可以进行重载操作,
由于重载后使用的方式非常像函数的调用,因此被称为仿函数。
#include<iostream>
#include<string>
using namespace std;
class MyPrint{
public:
//重载函数调用运算符
void operator()(string text){
cout << "()运算符重载(仿函数):" << endl;
cout << text << endl;
}
};
void func(string text){
cout << "正常函数调用:" << endl;
cout << text << endl;
}
void test01(){
MyPrint myPrint;
myPrint("author:lch!");//使用方式与函数调用十分相似,所以()运算符重载也被称为仿函数
func("author:lch!");
}
int main(){
test01();
system("pause");
return 0;
}
仿函数没有固定的写法(返回值、参数列表根据需要可以非常灵活):
#include<iostream>
#include<string>
using namespace std;
class MyPrint{
public:
void operator()(string text){
cout << "()运算符重载(仿函数):" << endl;
cout << text << endl;
}
};
class MyAdd{
public:
int operator()(int a, int b){
return a + b + 100;
}
};
void test01(){
MyPrint myPrint;
myPrint("author:lch!");//使用方式与函数调用十分相似,所以()运算符重载也被称为仿函数
}
void test02(){
MyAdd myAdd;
int ret = myAdd(100, 100);
cout << "ret(a, b) = " << ret << endl;
}
int main(){
test01();
test02();
system("pause");
return 0;
}