多态
OVERVIEW
- 多态
- 一、多态
- 1.基本概念:
- 2.E1-计算器类
- 3.纯虚函数&抽象类:
- 4.E2-制作饮品
- 5.虚析构&纯虚析构:
- 6.E3-电脑组装
- 二、运算符重载
- 1.加号运算符重载:
- 2.左移>>运算符重载:
- 3.递增++运算符重载:
- 4.赋值=运算符重载:
- 5.关系运算符重载:
- 6.函数调用运算符重载:
一、多态
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;
}