23.虚函数

23.面向对象-多态-虚函数


23.1 知识点

知识回顾

封装:用程序语言来形容对象

类和对象相关
class Player
{
    // 访问修饰符(3p):

    // 特征——成员变量

    // 行为——成员方法

    // 初始化调用——构造函数

    // 释放时调用——析构函数
};
修饰类成员的关键字
  • 静态成员
  • const成员
  • 友元函数
  • 友元类
  • 友元成员函数
其他封装机制
  • 嵌套类
  • 命名空间
  • 运算符重载

继承:复用封装对象的代码;儿子继承父亲,复用现成代码

继承的基本规则
继承后的构造函数和析构函数
多重继承
继承中的友元和运算符重载
里氏替换原则

多态的概念

  • 多态按字面意思就是“多种形态”
  • 让继承同一父类的子类们在执行相同方法时有不同的表现(状态)
  • 主要目的:同一父类的对象执行相同行为(方法)有不同的表现
  • 解决的问题:让同一个对象有唯一行为的特征

解决的问题

  • 如果不使用多态:

    • new出子类用父类装,执行的还是父类的方法
    • 除非用 dynamic_cast 强转,同时强转时还需保证父类有虚函数
  • 希望的效果是 new 出子类用父类装,执行的直接是子类的方法

#pragma once
#include <iostream>
using namespace std;

class Father
{
public:
    virtual void Test()
    {
    }
    void SpeakName()
    {
        cout << "Father的方法" << endl;
    }
};
#pragma once
#include "Father.h"

class Son : public Father
{
public:
    void SpeakName()
    {
        cout << "Son的方法" << endl;
    }
};
Father* f = new Son();
f->SpeakName(); // Father的方法

Son* s = dynamic_cast<Son*>(f);
s->SpeakName(); // Son的方法

多态的实现

  • 已学的多态:编译时多态(函数重载、运算符重载)

  • 将学的:运行时多态

    • 实现运行时多态的关键词:

      • v:virtual(虚函数)
      • o:override(推翻重写)
    • v 和 o 一定是结合使用的来实现多态

#pragma once
#include <iostream>
using namespace std;

class GameObject
{
public:
    string name;
    GameObject(string name)
    {
        this->name = name;
    }

    virtual void Atk()
    {
        cout << "游戏对象进行攻击" << endl;
    }
};
#pragma once
#include "GameObject.h"

class Player : public GameObject
{
public:
    Player(string name) : GameObject(name) {}

    void Atk() override
    {
        cout << "玩家" << name << "进行攻击" << endl;
    }
};
#pragma once
#include "GameObject.h"

class Monster : public GameObject
{
public:
    Monster(string name) : GameObject(name) {}

    void Atk() override
    {
        cout << "怪物" << name << "进行攻击" << endl;
    }
};
GameObject* player = new Player("韬老狮");
GameObject* monster = new Monster("小申怪物");
player->Atk();   // 玩家韬老狮进行攻击
monster->Atk();  // 怪物小申怪物进行攻击

// 栈上分配的对象,存在对象切片时就不会执行子类中重写的方法,所以一般我们不会这样使用
GameObject player2 = Player("韬老狮2");
player2.Atk(); // 游戏对象进行攻击

// 栈上分配,但用引用父类容器装载,不会对象切片,可执行子类中重写的方法
Player p2 = Player("韬老狮3");
GameObject& g = p2;
g.Atk(); // 玩家韬老狮3进行攻击

如何保留父类逻辑

  • 如果想在重写函数中保留父类逻辑:通过 父类名::方法名 显式调用父类逻辑
  • 如果想在外部调用,同理
#pragma once
#include "GameObject.h"

class Boss : public GameObject
{
public:
    Boss(string name) : GameObject(name) {}

    void Atk() override
    {
        GameObject::Atk();
        cout << "Boss" << name << "进行攻击" << endl;
    }
};
GameObject* boss = new Boss("哈莫雷特");
boss->Atk(); // Boss哈莫雷特进行攻击

player->GameObject::Atk(); // 游戏对象进行攻击

override 可以省略

  • virtual 不可以省略,override 是可以省略的

  • 但极其不建议省略

  • 原因:

    • 拼写错误:拼错重写方法名会有报错提示
    • 结构错误:如多写参数会有报错提示

总结

  • 多态:让同一类型的对象,执行相同行为时有不同的表现

  • 解决的问题:让同一对象有唯一的行为特征

  • vo:

    • v:virtual 虚函数
    • o:override 推翻重写
  • v 和 o 一定是配合使用的来实现多态

  • 如果想保留父类逻辑,可以显式调用父类方法 父类名::方法名

  • override 是可以省略的,但极不建议,因为可能出现不必要的问题


23.2 知识点代码

Father.h

#pragma once
#include <iostream>
using namespace std;
class Father
{
public:
    virtual void Test()
    {

    }
    void SpeakName()
    {
        cout << "Father的方法" << endl;
    }
};

Son.h

#pragma once
#include "Father.h"
class Son : public Father
{
public:
    void SpeakName()
    {
        cout << "Son的方法" << endl;
    }
};

GameObject.h

#pragma once
#include <iostream>
using namespace std;
class GameObject
{
public:
    string name;
    GameObject(string name)
    {
        this->name = name;
    }

    virtual void Atk()
    {
        cout << "游戏对象进行攻击" << endl;
    }
};

Player.h

#pragma once
#include "GameObject.h"
class Player : public GameObject
{
public:
    Player(string name) : GameObject(name)
    {

    }

    void Atk() override
    {
        //GameObject::Atk();
        cout << "玩家" << name << "进行攻击" << endl;
    }
};

Monster.h

#pragma once
#include "GameObject.h"
class Monster :
    public GameObject
{
public:
    Monster(string name) : GameObject(name)
    {

    }

    void Atk() override
    {
        cout << "怪物" << name << "进行攻击" << endl;
    }
};

Boss.h

#pragma once
#include "GameObject.h"
class Boss : public GameObject
{
public:
    Boss(string name) : GameObject(name)
    {

    }

    void Atk() override
    {
        GameObject::Atk();
        cout << "Boss" << name << "进行攻击" << endl;
    }
};

Lesson23_面向对象_多态_虚函数.cpp

#include <iostream>
#include "Son.h"
#include "Player.h"
#include "Monster.h"
#include "Boss.h"
using namespace std;
int main()
{
    #pragma region 知识回顾

    #pragma region 封装:用程序语言来形容对象

    #pragma region 类和对象相关
    /*class Player
        访问修饰符(3p):

        特征——成员变量

        行为——成员方法

        初始化调用——构造函数

        释放时调用——析构函数

    };*/
    #pragma endregion

    #pragma region 修饰类成员的关键字
    /*静态成员
    const成员
    友元函数
    友元类
    友元成员函数*/
    #pragma endregion

    #pragma region 其他封装机制
    //嵌套类
    //命名空间
    //运算符重载
    #pragma endregion

    #pragma endregion

    #pragma region 继承:复用封装对象的代码;儿子继承父亲,复用现成代码

    #pragma region 继承的基本规则

    #pragma endregion

    #pragma region 继承后的构造函数和析构函数

    #pragma endregion

    #pragma region 多重继承

    #pragma endregion

    #pragma region 继承中的友元和运算符重载

    #pragma endregion

    #pragma region 里氏替换原则

    #pragma endregion

    #pragma endregion

    #pragma endregion

    #pragma region 知识点一 多态的概念
    // 多态按字面的意思就是“多种形态”
    // 让继承同一父类的子类们 在执行相同方法时有不同的表现(状态)
    // 主要目的
    // 同一父类的对象 执行相同行为(方法)有不同的表现
    // 解决的问题
    // 让同一个对象有唯一行为的特征
    #pragma endregion

    #pragma region 知识点二 解决的问题

    // 现在如果不使用多态
    // new出子类用父类装 执行的还是父类的方法
    // 除非用dynamic_cast强转 同时强转的时候还需要保证父类有虚函数
    // 希望的效果是new出子类用父类装 执行的直接是子类的方法

    Father* f = new Son();
    f->SpeakName();//Father的方法

    Son* s = dynamic_cast<Son*>(f);
    s->SpeakName();//Son的方法

    #pragma endregion

    #pragma region 知识点三 多态的实现

    //我们目前已经学过的多态
    //编译时多态——函数重载,运算符重载,一开始就写好的

    //我们将学习的:
    //运行时多态
    //实现运行时多态的关键词是:
    //v:virtual (虚函数)
    //o:override (推翻重写)
    //v和o一定是结合使用的 来实现多态

    GameObject* player = new Player("韬老狮");
    GameObject* monster = new Monster("小申怪物");
    player->Atk();//玩家韬老狮进行攻击
    monster->Atk();//怪物小申怪物进行攻击

    //栈上分配的这种对象 存在对象切片时 就不会执行子类中重写的方法 所以一般我们不会这样使用
    GameObject player2 = Player("韬老狮2");
    player2.Atk();//游戏对象进行攻击

    //栈上分配,但是用引用父类容器装载,是不会对象切片的 是可以执行子类中重写的方法的
    Player p2 = Player("韬老狮3");
    GameObject& g = p2;
    g.Atk();//玩家韬老狮3进行攻击

    #pragma endregion

    #pragma region 知识点四 如何保留父类逻辑
    //1.如果想要在重写函数中保留父类逻辑
    //	只需要通过 父类名::方法名 显示调用父类逻辑即可
    //2.如果想要在外部调用,同理

    GameObject* boss = new Boss("哈莫雷特");
    boss->Atk();//Boss哈莫雷特进行攻击

    player->GameObject::Atk();//游戏对象进行攻击

    #pragma endregion

    #pragma region 知识点五 override可以省略
    //virtual不可以省略 override是可以省略的
    //但是极其不建议省略
    //因为省略可能由于拼写错误或者结构错误而出现问题
    //拼写错误:拼错重写方法名会有报错提示
    //结构错误:比如多写了些参数会有报错
    #pragma endregion

    //总结
    //多态:让同一类型的对象,执行相同行为时有不同的表现
    //解决的问题:让同一对象有唯一的行为特征
    //vo:
    //v:virtual 虚函数
    //o:override 推翻重写
    //v和o一定是配合使用的 来实现我们的多态
    //但是想要保留父类逻辑的话,我们可以显示的去调用父类方法即可 父类名::方法名
    //override是可以省略的,但是极其不建议大家省略 因为可能出现不必要的问题
}

23.3 练习题

真的鸭子嘎嘎叫,木头鸭子吱吱叫,橡皮鸭子唧唧叫

这是一个典型的继承与多态应用示例,通过基类指针调用子类重写的 Speak 方法,模拟不同鸭子的叫声行为。

#include "Duck.h"
#include <iostream>
using namespace std;

void Duck::Speak()
{
    cout << "嘎嘎" << endl;  // 真实的鸭子发出“嘎嘎”的叫声
}
#include "WoodDuck.h"
#include <iostream>
using namespace std;

void WoodDuck::Speak()
{
    cout << "吱吱" << endl;  // 木头鸭子发出“吱吱”的叫声
}
#include "RubberDuck.h"
#include <iostream>
using namespace std;

void RubberDuck::Speak()
{
    cout << "唧唧" << endl;  // 橡皮鸭子发出“唧唧”的叫声
}

调用示例:

Duck* duck = new Duck();               // 创建真实鸭子对象
Duck* woodDuck = new WoodDuck();      // 创建木头鸭子对象
Duck* rubberDuck = new RubberDuck();  // 创建橡皮鸭子对象

duck->Speak();         // 输出:嘎嘎
woodDuck->Speak();     // 输出:吱吱
rubberDuck->Speak();   // 输出:唧唧

delete duck;
duck = nullptr;

delete woodDuck;
woodDuck = nullptr;

delete rubberDuck;
rubberDuck = nullptr;

多态解释: 父类指针指向子类对象,通过虚函数机制,正确地调用到子类重写的方法,实现行为多样性。

所有员工9点打卡,但经理十一点打卡,程序员不打卡

这是一个模拟不同职位员工行为差异的继承结构与重写方法的示例。

#include "Staff.h"
#include <iostream>
using namespace std;

void Staff::PunchTheClock()
{
    cout << "9点打卡" << endl;  // 所有普通员工默认9点打卡
}
#include "Manager.h"
#include <iostream>
using namespace std;

void Manager::PunchTheClock()
{
    cout << "11点打卡" << endl;  // 经理特殊,11点打卡
}
#include "Programmer.h"
#include <iostream>
using namespace std;

void Programmer::PunchTheClock()
{
    cout << "老子不打卡" << endl;  // 程序员个性张扬,不打卡
}

继承结构说明:

  • ManagerProgrammer 都继承自 Staff
  • 重写了 PunchTheClock() 方法实现不同的打卡逻辑
  • 体现了子类可根据自身特点覆盖父类方法的特点

创建一个图形类,有求面积和周长两个方法,创建矩形类、正方形类、圆形类继承图形类,实例化对象并求面积和周长

该示例展示了面向对象编程中多态和继承的结合使用,通过基类 Graph 的指针,调用不同子类对面积和周长的实现。

矩形类实现:

#include "Rect.h"

Rect::Rect(float w, float h)
{
    this->w = w;
    this->h = h;
}

float Rect::GetLength()
{
    return (w + h) * 2;  // 周长公式:2 × (宽 + 高)
}

float Rect::GetArea()
{
    return w * h;  // 面积公式:宽 × 高
}

正方形类实现:

#include "Square.h"

Square::Square(float l)
{
    this->l = l;
}

float Square::GetLength()
{
    return l * 4;  // 周长公式:边长 × 4
}

float Square::GetArea()
{
    return l * l;  // 面积公式:边长²
}

圆形类实现:

#include "Circular.h"

Circular::Circular(float r)
{
    this->r = r;
}

float Circular::GetLength()
{
    return 2 * 3.14f * r;  // 周长公式:2πr
}

float Circular::GetArea()
{
    return 3.14f * r * r;  // 面积公式:πr²
}

调用示例:

Graph* r = new Rect(4, 5);
cout << r->GetLength() << endl;  // 输出:18
cout << r->GetArea() << endl;    // 输出:20
delete r;
r = nullptr;

r = new Square(5);
cout << r->GetLength() << endl;  // 输出:20
cout << r->GetArea() << endl;    // 输出:25
delete r;
r = nullptr;

r = new Circular(3);
cout << r->GetLength() << endl;  // 输出:18.84
cout << r->GetArea() << endl;    // 输出:28.26
delete r;
r = nullptr;

输出结果:

18
20
20
25
18.84
28.26

设计总结:

  • Graph 作为抽象基类(或通用接口)定义公共行为
  • RectSquareCircular 分别继承并实现不同的逻辑
  • 多态机制使得通过统一接口操作不同图形对象成为可能
  • 实现了统一调用,多种实现的典型面向对象思想

23.4 练习题代码

Duck.h

#pragma once
class Duck
{
public:
    virtual void Speak();
};

Duck.cpp

#include "Duck.h"
#include <iostream>
using namespace std;

void Duck::Speak()
{
    cout << "嘎嘎" << endl;
}

WoodDuck.h

#pragma once
#include "Duck.h"
class WoodDuck :
    public Duck
{
public:
    void Speak() override;
};

WoodDuck.cpp

#include "WoodDuck.h"
#include <iostream>
using namespace std;

void WoodDuck::Speak()
{
    cout << "֨֨" << endl;
}

RubberDuck.h

#pragma once
#include "Duck.h"
class RubberDuck :
    public Duck
{
public:
    void Speak() override;
};

RubberDuck.cpp

#include "RubberDuck.h"
#include <iostream>
using namespace std;

void RubberDuck::Speak()
{
    cout << "唧唧" << endl;
}

Staff.h

#pragma once
class Staff
{
public:
    virtual void PunchTheClock();
};

Staff.cpp

#include "Staff.h"
#include <iostream>
using namespace std;
void Staff::PunchTheClock()
{
    cout << "9点打卡" << endl;
}

Manager.h

#pragma once
#include "Staff.h"
class Manager :
    public Staff
{
public:
    void PunchTheClock() override;
};

Manager.cpp

#include "Manager.h"
#include <iostream>
using namespace std;
void Manager::PunchTheClock()
{
    cout << "11点打卡" << endl;
}

Programmer.h

#pragma once
#include "Staff.h"
class Programmer :
    public Staff
{
public:
    void PunchTheClock() override;
};

Programmer.cpp

#include "Programmer.h"
#include <iostream>
using namespace std;
void Programmer::PunchTheClock()
{
    cout << "老子不打卡" << endl;
}

Graph.h

#pragma once
class Graph
{
public:
    virtual float GetLength()
    {
        return 0;
    }

    virtual float GetArea()
    {
        return 0;
    }
};

Rect.h

#pragma once
#include "Graph.h"
class Rect :
    public Graph
{
private:
    float w;
    float h;
public:
    Rect(float w, float h);
    float GetLength() override;
    float GetArea() override;
};

Rect.cpp

#include "Rect.h"

Rect::Rect(float w, float h)
{
    this->w = w;
    this->h = h;
}
float Rect::GetLength()
{
    return (w + h) * 2;
}
float Rect::GetArea()
{
    return w * h;
}

Square.h

#pragma once
#include "Graph.h"
class Square :
    public Graph
{
private:
    float l;
public:
    Square(float l);
    float GetLength() override;
    float GetArea() override;
};

Square.cpp

#include "Square.h"


Square::Square(float l)
{
    this->l = l;
}
float Square::GetLength()
{
    return l * 4;
}
float Square::GetArea()
{
    return l * l;
}

Circular.h

#pragma once
#include "Graph.h"
class Circular :
    public Graph
{
private:
    float r;
public:
    Circular(float r);
    float GetLength() override;
    float GetArea() override;
};

Circular.cpp

#include "Circular.h"
Circular::Circular(float r)
{
    this->r = r;
}
float Circular::GetLength()
{
    //2πr
    return 2 * 3.14f * r;
}
float Circular::GetArea()
{
    //πr²
    return 3.14f * r * r;
}

Lesson23_练习题.cpp

#include <iostream>
using namespace std;
#include "Duck.h"
#include "WoodDuck.h"
#include "RubberDuck.h"
#include "Manager.h"
#include "Programmer.h"
#include "Rect.h"
#include "Square.h"
#include "Circular.h"
int main()
{
    #pragma region 练习题一

    //真的鸭子嘎嘎叫,木头鸭子吱吱叫,橡皮鸭子唧唧叫

    Duck* duck = new Duck();
    Duck* woodDuck = new WoodDuck();
    Duck* rubberDuck = new RubberDuck();


    duck->Speak();
    woodDuck->Speak();
    rubberDuck->Speak();
    //嘎嘎
    //吱吱
    //唧唧

    delete duck;
    duck = nullptr;

    delete woodDuck;
    woodDuck = nullptr;

    delete rubberDuck;
    rubberDuck = nullptr;

    #pragma endregion

    #pragma region 练习题二
    //所有员工9点打卡
    //但经理十一点打卡,程序员不打卡

    Staff* staff = new Staff();
    staff->PunchTheClock();
    delete staff;
    staff = nullptr;

    Staff* m = new Manager();
    m->PunchTheClock();
    delete m;
    m = nullptr;

    Staff* p = new Programmer();
    p->PunchTheClock();
    delete p;
    p = nullptr;

    //9点打卡
    //11点打卡
    //老子不打卡

    #pragma endregion

    #pragma region 练习题三
    //创建一个图形类,有求面积和周长两个方法
    //创建矩形类,正方形类,圆形类继承图形类
    //实例化矩形、正方形、圆形对象求面积和周长

    Graph* r = new Rect(4, 5);
    cout << r->GetLength() << endl;
    cout << r->GetArea() << endl;
    delete r;
    r = nullptr;

    r = new Square(5);
    cout << r->GetLength() << endl;
    cout << r->GetArea() << endl;
    delete r;
    r = nullptr;

    r = new Circular(3);
    cout << r->GetLength() << endl;
    cout << r->GetArea() << endl;
    delete r;
    r = nullptr;

    //18
    //20
    //20
    //25
    //18.84
    //28.26

    #pragma endregion

}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏