【C++总结】07:多态

多态


一、多态

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值