【C++基础】07:多态

news/2025/2/14 2:58:13/

多态


OVERVIEW

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

一、多态

1.基本概念:

多态是面向对象三大特性之一,多态分为两类:静态多态、动态多态

  1. 静态多态:函数重载运算符重载属于静态多态(复用函数名)
  2. 动态多态:派生类和虚函数实现运行时多态

静态多态与动态多态的区别:

静态多态的函数地址早绑定,编译阶段确定函数地址。

动态多态的函数地址晚绑定,运行阶段确定函数地址。

动态多态满足条件:

  1. 继承关系
  2. 子类需要重写父类的虚函数

地址绑定早,在编译阶段确定函数地址:

#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;
}

image-20220217210337285

在父类的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;
}

image-20220217211006674

注意:与函数重载不同,函数重写是指重写的函数的返回值类型、函数名、参数列表完全一致。

父类的指针 or 引用指向子类对象Cat、Dog

image-20220217212907159

2.E1-计算器类

多态的优点:

  1. 代码组织结构清晰
  2. 可读性强
  3. 利于前期和后期的扩展以及维护

分别使用普通写法和多态技术,设计实现两个操作数进行运算的计数器类:

#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;
}

image-20220217220048120

如果需要扩展计算机的新功能,需要对源码进行修改。

#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;
}

image-20220217215914734

3.纯虚函数&抽象类:

在多态中通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容(因此可以将虚函数改为纯虚函数

当一个类中有了纯虚函数,这个类也可称为抽象类。

纯虚函数语法:virtual 返回值类型 函数名(参数列表) = 0;

抽象类特点:

  1. 无法实例化对象
  2. 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
#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;
}

image-20220217222819693

注意:该案例中另外创建了doWork函数,将函数调用操作与堆区内存释放操作简化(只保留父类指针指向修改)优化程序结构。

5.虚析构&纯虚析构:

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。

需要将父类中的析构函数改为虚构函数 or 存虚构函数

虚析构:virtual ~类名(){}

纯虚析构:virtual ~类名() = 0; 类名::~类名(){}

虚析构与纯虚构共性:

  1. 可以解决父类指针释放,无法调用子类析构函数
  2. 都需要有具体的函数实现,否则报错。

虚析构与纯虚构区别:

  1. 如果是纯虚析构,该类属于抽象类(无法实例化)
#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;
}

image-20220217224942024

父类指针在释放时,不会调用到子类的析构函数(子类如果有堆区数据,会出现内存泄漏)。

为父类Animal析构函数添加virtual关键字,将子类的析构函数改为虚析构解决。

image-20220217225855310

image-20220217225755662

子类对象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;
}

image-20220220233342960

核心内容:

  1. 对于零件层(都是抽象处理、纯虚函数),不同的厂商实现不同的零件(具体厂商子类实现)
  2. 三个指针接收数据开辟在堆区的三个零件,通过一个work()函数使三个零件工作(堆区开辟的数据需利用父类析构函数释放)
  3. 利用多态的架构实现优化程序结构

二、运算符重载

c++中的运算符重载是指,对已有的运算符进行重新定义(赋予其另一种功能),以适应不同的数据类型。

1.加号运算符重载:

  1. 成员函数方式重载+运算符:p1.operator+(p2)
  2. 全局函数方式重载+运算符: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;
}

image-20220210133452827

注意:对于内置的数据类型的表达式的运算符是不可能改变的(对于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;
}

image-20220210140253098

注意:要进一步理解左移运算符重载,必须深刻理解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;
}

image-20220211131650267

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;
}

image-20220211133358995

练习:对比自增重载的过程,尝试完成自减运算符的重载操作。

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;
}

image-20220211205530761

返回值出现异常-1073740940

简单的赋值=操作中存在的浅拷贝问题,使得堆区内存重复释放(报错),必须利用深拷贝解决。

image-20220211205135980

image-20220211205326510

重载赋值运算符=,利用深拷贝解决浅拷贝中出现的堆区内存重复释放问题:

#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;
}

image-20220211210755857

总结:深拷贝不同于浅拷贝只是做了简单的指针复制操作(每个地址都不同),另外在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;
}

image-20220211214821205

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;
}

image-20220211220212768

仿函数没有固定的写法(返回值、参数列表根据需要可以非常灵活):

#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;
}

image-20220211221234667


http://www.ppmy.cn/news/8456.html

相关文章

费解的开关(BFS+哈希表+二进制枚举)

费解的开关&#xff08;BFS哈希表二进制枚举&#xff09;一、题目二、思路分析1、算法标签2、思路梳理方法1&#xff1a;BFS哈希表方法2&#xff1a;二进制枚举DFS一、题目 二、思路分析 1、算法标签 这道题考察的是BFS哈希表,DFS二进制枚举 2、思路梳理 方法1&#xff1a;…

【JAVA进阶】包装类,Arrays类,Lambda表达式

&#x1f4c3;个人主页&#xff1a;个人主页 &#x1f525;系列专栏&#xff1a;JAVASE基础 目录 一、包装类 二、Arrays类 三、Lambda表达式 一、包装类 其实就是8种基本数据类型对应的引用类型。 基本数据类型 引用数据类型 byte Byte short Short int Integer l…

C++语言程序设计

C语言程序设计 如需转载请标明出处&#xff1a;http://blog.csdn.net/itas109 文章目录C语言程序设计1. 语言概述1.1 字符集合1.2 词法记号关键字标识符文字操作符(运算符)分隔符空白符2. 数据类型2.1 基本数据类型2.2 常量整型常量实型常量字符常量字符串常量布尔常量2.3 变量…

【Linux】Linux基本指令

&#x1f680; 作者简介&#xff1a;一名在后端领域学习&#xff0c;并渴望能够学有所成的追梦人。 &#x1f40c; 个人主页&#xff1a;蜗牛牛啊 &#x1f525; 系列专栏&#xff1a;&#x1f6b2;Linux &#x1f4d5; 学习格言&#xff1a;博观而约取&#xff0c;厚积而薄发 …

Tic-Tac-Toe可能棋局遍历的实现(python)

目录 1. 前言 2. 算法流程 3. 代码实现 4. 一个思考题&#xff1a;代码实现中的一个坑 5. 结果正确吗&#xff1f; 1. 前言 在上一篇博客中&#xff1a;Tic-Tac-Toe可能棋局搜索的实现&#xff08;python&#xff09;_笨牛慢耕的博客-CSDN博客Tic-Tac-Toe中文常译作井字棋…

01月份图形化一级打卡试题

活动时间 从2023年 1月1日至1月21日&#xff0c;每天一道编程题。 本次打卡的规则如下&#xff1a; &#xff08;1&#xff09;小朋友每天利用10~15分钟做一道编程题&#xff0c;遇到问题就来群内讨论&#xff0c;我来给大家答疑。 &#xff08;2&#xff09;小朋友做完题目后&…

2022年博客之星排行榜 日榜 2023-01-01 博客之星总榜

​ 2022年博客之星排行榜 日榜 2023-01-01 博客之星总榜 备注: 数据来源 :https://pachong.vip/csdn/blogstar,如有侵权,联系秒删~ 博主链接: http://t.csdn.cn/hfFGk http://t.csdn.cn/hfFGk 原力等级9&#xff0c; 我在线秒回&#xff0c;诚信合作 数据统计时间&#xf…

网络原理2 TCP协议

TCP协议 文章目录TCP协议TCP的特点TCP的基本特性确认应答机制超时重传机制丢包连接管理机制TCP建立连接---三次握手TCP断开连接---四次挥手滑动窗口机制丢包问题流量控制机制拥塞控制机制延迟应答机制捎带应答机制面向字节流问题TCP中的异常处理程序崩溃了正常关机突然断电关机…