22.里氏替换原则

22.面向对象-继承-里氏替换原则


22.1 知识点

基本概念

  • 里氏替换原则是面向对象七大原则中重要的原则
  • 概念:任何父类出现的地方,子类都可以替代
  • 重点:语法表现——父类容器装子类对象,因为子类对象包含了父类的所有内容
  • 作用:方便进行对象存储和管理

基本实现

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

class GameObject
{
public:
    int id = 0;
    string name = "";

    virtual void Test()
    {
        cout << "Test" << endl;
    }
};
#pragma once
#include "GameObject.h"

class Player : public GameObject
{
public:
    void PlayerAtk()
    {
        cout << "Player攻击" << endl;
    }
};
#pragma once
#include "GameObject.h"

class Monster : public GameObject
{
public:
    void MonsterAtk()
    {
        cout << "Monster攻击" << endl;
    }
};
#pragma once
#include "GameObject.h"

class Boss : public GameObject
{
public:
    void BossAtk()
    {
        cout << "Boss攻击" << endl;
    }
};
// 栈上分配
GameObject player = Player();
GameObject monster = Monster();
GameObject boss = Boss();

GameObject objects[3] = { Player(), Monster(), Boss() };

// 堆上分配
GameObject* player2 = new Player();
GameObject* monster2 = new Monster();
GameObject* boss2 = new Boss();

GameObject* objects2[3] = { new Player(), new Monster(), new Boss() };

栈上的里氏替换

  • 虽然我们可以在栈上用父类容器装载子类对象
  • 但是不建议大家使用栈上的里氏替换

主要原因如下:

  • 对象切片:用父类容器装载子类对象,子类部分会被裁掉
  • 无法转换:无法将父类容器转换为子类对象

因此,绝大多数情况下,我们应当避免在栈上使用里氏替换

堆上的里氏替换

  • 当在堆上使用里氏替换原则
  • 用父类容器装载子类对象
  • 不会出现对象切片,并且可以进行转换
  • 我们可以通过 dynamic_cast 将父类容器转为子类对象使用
  • 当学了多态后,更能体会这一点(因为需要虚函数)

转换方法说明:

  • <> 是传入想要转换为的类型
  • () 是具体想要转换谁

示例:

// 把 GameObject 转成 Player
Player* p = dynamic_cast<Player*>(player2);

// 转换成功 就不会为 nullptr;如果失败 就会为 nullptr
if (p != nullptr)
{
    p->Test();        // Test
    p->PlayerAtk();   // Player攻击
}
else
    cout << "不是Player类型" << endl;

// 把 Player 转成 GameObject(但一般不会转,因为子类就应该包含了父类所有内容)
GameObject* g = dynamic_cast<GameObject*>(p);
if (g != nullptr)
{
    cout << "转换成功" << endl;  // 转换成功
}

总结

  • 里式替换 —— 用父类容器装载子类对象

  • 栈上不建议大家使用,因为会发生对象切片

  • 堆上是经常会使用的,我们会通过 dynamic_cast 和判空组合使用来进行转换,或者判断某一个指针对象是不是某一个类型

  • 里氏替换的主要作用:用同一个父类容器装载不同类型的子类对象


22.2 知识点代码

GameObject.h

#pragma once
#include <iostream>
using namespace std;
class GameObject
{
public:
    int id = 0;
    string name = "";

    virtual void Test()
    {
        cout << "Test" << endl;
    }
};

Player.h

#pragma once
#include "GameObject.h"
class Player :
    public GameObject
{
public:
    void PlayerAtk()
    {
        cout << "Player攻击" << endl;
    }
};

Monster.h

#pragma once
#include "GameObject.h"
class Monster :
    public GameObject
{
public:
    void MonsterAtk()
    {
        cout << "Monster攻击" << endl;
    }
};

Boss.h

#pragma once
#include "GameObject.h"
class Boss :
    public GameObject
{
public:
    void BossAtk()
    {
        cout << "Boss攻击" << endl;
    }
};

Lesson22_面向对象_继承_里氏替换原则.cpp

#include <iostream>
#include "Boss.h"
#include "Monster.h"
#include "Player.h"
int main()
{
    #pragma region 知识点一 基本概念
    // 里氏替换原则是面向对象七大原则中最重要的原则
    // 概念:
    // 任何父类出现的地方,子类都可以替代
    // 重点:
    // 语法表现——父类容器装子类对象,因为子类对象包含了父类的所有内容
    // 作用:
    // 方便进行对象存储和管理
    #pragma endregion

    #pragma region 知识点二 基本实现
    //栈上分配
    GameObject player = Player();
    GameObject monster = Monster();
    GameObject boss = Boss();

    GameObject objects[3] = { Player(), Monster(), Boss() };

    //堆上分配
    GameObject* player2 = new Player();
    GameObject* monster2 = new Monster();
    GameObject* boss2 = new Boss();

    GameObject* objects2[3] = { new Player(), new Monster(), new Boss() };

    #pragma endregion

    #pragma region 知识点三 栈上的里氏替换
    //虽然我们可以在栈上用父类容器装载子类对象
    //但是不建议大家使用栈上的里氏替换
    //主要原因:
    //1.对象切片:用父类容器装载子类对象,子类部分会被裁掉
    //2.无法转换:无法将父类容器转换为子类对象

    //因此,绝大多数情况下,我们应当避免在栈上使用里氏替换
    #pragma endregion

    #pragma region 知识点四 堆上的里氏替换
    //当在堆上使用里氏替换原则
    //用父类容器装载子类对象
    //不会出现对象切片,并且可以进行转换
    //我们可以通过 dynamic_cast 将父类容器转为子类对象使用
    //当学了多态后,更能体会这一点(因为需要虚函数)
    //一个转换的方法
    //<>是传入想要转换为的类型
    //()是具体想要转换谁

    //把GameObject转成Player
    Player* p = dynamic_cast<Player*>(player2);
    //转换成功 就不会为nullptr 如果失败 就会为nullptr
    if (p != nullptr)
    {
        p->Test();//Test
        p->PlayerAtk();//Player攻击
    }
    else
        cout << "不是Player类型" << endl;

    //把Player转成GameObject 但一般不会转 因为子类就应该包含了父类所有内容
    GameObject* g = dynamic_cast<GameObject*>(p);
    if (g != nullptr)
    {
        cout << "转换成功" << endl;//转换成功
    }

    #pragma endregion

    //总结
    //里式替换——用父类容器装载子类对象
    //栈上不建议大家使用 因为会 对象切片
    //堆上是经常会使用的 我们会通过 dynamic_cast 和 判空 组合使用 来进行转换 
    //或者判断某一个指针对象 是不是某一个类型

    //里氏替换的主要作用 
    //用同一父类容器 装载不同类型的子类对象

}

22.3 练习题

栈上和堆上的里氏替换有什么区别

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

class A
{
public:
    virtual void Test()
    {
        cout << "Test" << endl;
    }
};
#pragma once
#include "A.h"

class B : public A
{
public:
    void BTest()
    {
        cout << "BTest" << endl;
    }
};

说明:

  • 栈上的值存储会发生对象切片:

    // A
    // B : A
    // A a = B(); // 此处发生对象切片,只保留了 B 对象的 A 部分
    
  • 栈上的引用不会发生对象切片:

    B b = B();
    A& a = b; // 里氏替换,不会发生对象切片
    
    B* ptr = dynamic_cast<B*>(&a);
    if (ptr != nullptr)
    {
        ptr->BTest(); // 输出 BTest
        ptr->Test();  // 输出 Test
    }
    
  • 堆上的对象也不会发生对象切片,与引用情况类似。

写一个 Monster 类,它派生出 Boss 和 Goblin 两个类,Boss 有技能,小怪有攻击;随机生成 10 个怪,装载到数组中,遍历这个数组,调用它们的攻击方法,如果是 Boss 就释放技能

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

class Monster
{
public:
    virtual void Test()
    {
    }
};
#pragma once
#include "Monster.h"

class Boss : public Monster
{
public:
    void Skill()
    {
        cout << "boss技能" << endl;
    }
};
#pragma once
#include "Monster.h"

class Goblin : public Monster
{
public:
    void Atk()
    {
        cout << "哥布林攻击" << endl;
    }
};
Monster* monsters[10];
int random = 0;

// 随机生成10个怪物,装载到数组中
for (int i = 0; i < 10; i++)
{
    random = getRandom(1, 100);
    if (random < 50)
        monsters[i] = new Boss();
    else
        monsters[i] = new Goblin();
}

// 遍历数组,释放技能或攻击
for (int i = 0; i < 10; i++)
{
    Boss* boss = dynamic_cast<Boss*>(monsters[i]);
    // 如果是 Boss 就释放技能
    if (boss != nullptr)
    {
        boss->Skill();
    }
    else
    {
        Goblin* goblin = dynamic_cast<Goblin*>(monsters[i]);
        // 如果是哥布林就攻击
        if (goblin != nullptr)
        {
            goblin->Atk();
        }
    }
}

FPS 游戏模拟:写一个玩家类,玩家可以拥有各种武器,现在有四种武器,冲锋枪、散弹枪、手枪、匕首,玩家默认拥有匕首。请在玩家类中写一个方法,可以拾取不同的武器替换自己拥有的枪械

#pragma once
class Weapon
{
};
#pragma once
#include "Weapon.h"

class SubmachineGun : public Weapon
{
};
#pragma once
#include "Weapon.h"

class ShotGun : public Weapon
{
};
#pragma once
#include "Weapon.h"

class Pistol : public Weapon
{
};
#pragma once
#include "Weapon.h"

class Dagger : public Weapon
{
};
#pragma once
#include "Pistol.h"

class Player
{
private:
    Weapon* weapon = nullptr;

public:
    Player()
    {
        // 玩家默认拥有匕首
        weapon = new Pistol();
    }

    void PickUp(Weapon* weapon)
    {
        if (this->weapon != nullptr)
        {
            delete this->weapon;
            // this->weapon = nullptr;
        }

        this->weapon = weapon;
    }
};
Player* player = new Player();

SubmachineGun* sg = new SubmachineGun();
player->PickUp(sg);

ShotGun* sg2 = new ShotGun();
player->PickUp(sg2);

22.4 练习题代码

A.h

#pragma once
#include <iostream>
using namespace std;
class A
{
public:
    virtual void Test()
    {
        cout << "Test" << endl;
    }
};

B.h

#pragma once
#include "A.h"
class B :public A
{
public:
    void BTest()
    {
        cout << "BTest" << endl;
    }
};

Monster.h

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

    }
};

Boss.h

#pragma once
#include "Monster.h"
class Boss :
    public Monster
{
public:
    void Skill()
    {
        cout << "boss技能" << endl;
    }
};

Goblin.h

#pragma once
#include "Monster.h"
class Goblin :
    public Monster
{
public:
    void Atk()
    {
        cout << "哥布林攻击" << endl;
    }
};

Weapon.h

#pragma once
class Weapon
{
};

Dagger.h

#pragma once
#include "Weapon.h"
class Dagger :
    public Weapon
{
};

Pistol.h

#pragma once
#include "Weapon.h"
class Pistol :
    public Weapon
{
};

ShotGun.h

#pragma once
#include "Weapon.h"
class ShotGun :
    public Weapon
{
};

SubmachineGun.h

#pragma once
#include "Weapon.h"
class SubmachineGun :
    public Weapon
{
};

Player.h

#pragma once
#include "Pistol.h";
class Player
{
private:
    Weapon* weapon = nullptr;

public:
    Player()
    {
        //玩家默认拥有匕首
        weapon = new Pistol();
    }

    void PickUp(Weapon* weapon)
    {
        if (this->weapon != nullptr)
        {
            delete this->weapon;
            //this->weapon = nullptr;
        }

        this->weapon = weapon;
    }
};

Lesson22_练习题.cpp

#include <iostream>
#include "B.h"
#include "Boss.h"
#include "Goblin.h"
#include "CustomConsole.h"
#include "Player.h"
#include "SubmachineGun.h"
#include "ShotGun.h"
int main()
{
    #pragma region 练习一
    //栈上和堆上的里氏替换有什么区别

    //栈上的值存储会存在 会进行对象切片 
    // A
    // B : A
    // A a = B();
    // 
    //栈上的引用存储 不存在切片的
    // B b = B();
    // A& a = b;
    // 这种情况下 我们甚至可以利用dynamic_cast去对引用进行转换
    B b = B();
    A& a = b; //里氏替换 不会对象切片

    B* ptr = dynamic_cast<B*>(&a);
    if (ptr != nullptr)
    {
        ptr->BTest();//BTest
        ptr->Test();//Test
    }

    //堆上是不存在这样的问题的


    #pragma endregion

    #pragma region 练习二
    //写一个Monster类,它派生出Boss和Goblin两个类,Boss有技能;
    //小怪有攻击;随机生成10个怪,装载到数组中,遍历这个数组,调用他们的攻击方法,如果是boss就释放技能

    Monster* monsters[10];
    int random = 0;
    //随机生成10个怪物 装载到了数组中
    for (int i = 0; i < 10; i++)
    {
        random = getRandom(1, 100);
        if (random < 50)
            monsters[i] = new Boss();
        else
            monsters[i] = new Goblin();
    }

    for (int i = 0; i < 10; i++)
    {
        Boss* boss = dynamic_cast<Boss*>(monsters[i]);
        //如果是boss 就释放技能
        if (boss != nullptr)
        {
            boss->Skill();
        }
        else
        {
            Goblin* goblin = dynamic_cast<Goblin*>(monsters[i]);
            //如果是哥布林 就攻击
            if (goblin != nullptr)
            {
                goblin->Atk();
            }
        }
    }

    #pragma endregion

    #pragma region 练习三
    /*FPS游戏模拟
    写一个玩家类,玩家可以拥有各种武器
    现在有四种武器,冲锋枪,散弹枪,手枪,匕首
    玩家默认拥有匕首
    请在玩家类中写一个方法,可以拾取不同的武器替换自己拥有的枪械*/


    Player* player = new Player();

    SubmachineGun* sg = new SubmachineGun();
    player->PickUp(sg);

    ShotGun* sg2 = new ShotGun();
    player->PickUp(sg2);

    #pragma endregion

}


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

×

喜欢就点赞,疼爱就打赏