17.x – Резюме к главе 17 и небольшой тест

Добавлено 8 августа 2021 в 19:04

Краткое резюме

Наследование позволяет нам моделировать между двумя объектами связь «является чем-либо». Класс, от которого выполняется наследование, называется родительским классом, базовым классом или суперклассом. Класс, выполняющий наследование, называется дочерним классом, производным классом или подклассом.

Когда производный класс наследуется от базового класса, он получает все члены базового класса.

Когда создается объект производного класса, сначала создается базовая часть класса, а затем создается производная часть. Более подробно:

  1. выделяется память для производного класса (достаточная и для базовой, и для производной частей);
  2. вызывается соответствующий конструктор производного класса;
  3. сначала создается объект базового класса с использованием соответствующего конструктора базового класса. Если конструктор базового класса не указан, будет использоваться конструктор по умолчанию;
  4. список инициализации производного класса инициализирует переменные;
  5. выполняется тело конструктора производного класса;
  6. управление возвращается вызывающей функции.

Уничтожение происходит в обратном порядке, от наиболее производного к наиболее базовому классу.

В C++ есть 3 спецификатора доступа: public, private и protected. Спецификатор доступа protected позволяет классу, к которому принадлежит член, друзьям и производным классам, получать доступ к защищенному члену, но у внешнего кода доступа к этому члену нет.

Классы могут наследоваться от другого класса открыто, закрыто и защищенно. Классы почти всегда наследуются открыто.

Ниже приведена таблица всех комбинаций спецификаторов доступа и типов наследования:

Спецификатор доступа в базовом классеСпецификатор доступа при открытом наследованииСпецификатор доступа при закрытом наследованииСпецификатор доступа при защищенном наследовании
publicpublicprivateprotected
protectedprotectedprivateprotected
privateне доступенне доступенне доступен

Производные классы могут добавлять новые функции, изменять в производном классе способ работы функций, существующих в базовом классе, изменять уровень доступа унаследованного члена и скрывать функционал.

Множественное наследование позволяет производному классу наследовать члены более чем от одного родителя. По возможности следует избегать множественного наследования.

Небольшой тест

Вопрос 1

Для каждой из следующих программ определите, что они выводят, или, если они не будут компилироваться, укажите, почему. Это упражнение предназначено для выполнения путем проверки в уме, поэтому не компилируйте код.

a)

#include <iostream>
 
class Base
{
public:
    Base()
    {
        std::cout << "Base()\n";
    }
    ~Base()
    {
        std::cout << "~Base()\n";
    }
};
 
class Derived: public Base
{
public:
    Derived()
    {
        std::cout << "Derived()\n";
    }
    ~Derived()
    {
        std::cout << "~Derived()\n";
    }
};
 
int main()
{
    Derived d{};
 
    return 0;
}

Создание происходит в порядке от самого базового класса к самому дочернему классу. Уничтожение происходит в обратном порядке.

Base()
Derived()
~Derived()
~Base()

b)

#include <iostream>
 
class Base
{
public:
    Base()
    {
        std::cout << "Base()\n";
    }
    ~Base()
    {
        std::cout << "~Base()\n";
    }
};
 
class Derived: public Base
{
public:
    Derived()
    {
        std::cout << "Derived()\n";
    }
    ~Derived()
    {
        std::cout << "~Derived()\n";
    }
};
 
int main()
{
    Derived d;
    Base b;
 
    return 0;
}

Подсказка: локальные переменные уничтожаются в порядке, обратном определению.

Сначала мы создаем d, что печатает:

Base()
Derived()

Затем мы создаем b, что печатает:

Base()

Затем мы уничтожаем b, что печатает:

~Base()

Затем мы уничтожаем d, что печатает:

~Derived()
~Base()

c)

#include <iostream>
 
class Base
{
private:
    int m_x;
public:
    Base(int x): m_x{ x }
    {
        std::cout << "Base()\n";
    }
    ~Base()
    {
        std::cout << "~Base()\n";
    }
 
    void print() const { std::cout << "Base: " << m_x << '\n';  }
};
 
class Derived: public Base
{
public:
    Derived(int y):  Base{ y }
    {
        std::cout << "Derived()\n";
    }
    ~Derived()
    {
        std::cout << "~Derived()\n";
    }
 
    void print() const { std::cout << "Derived: " << m_x << '\n'; }
};
 
int main()
{
    Derived d{ 5 };
    d.print();
 
    return 0;
}

Не компилируется, Derived::print() не может получить доступ к закрытому члену m_x

d)

#include <iostream>
 
class Base
{
protected:
    int m_x;
public:
    Base(int x): m_x{ x }
    {
        std::cout << "Base()\n";
    }
    ~Base()
    {
        std::cout << "~Base()\n";
    }
 
    void print() const { std::cout << "Base: " << m_x << '\n';  }
};
 
class Derived: public Base
{
public:
    Derived(int y):  Base{ y }
    {
        std::cout << "Derived()\n";
    }
    ~Derived()
    {
        std::cout << "~Derived()\n";
    }
 
    void print() const { std::cout << "Derived: " << m_x << '\n'; }
};
 
int main()
{
    Derived d{ 5 };
    d.print();
 
    return 0;
}

Base()
Derived()
Derived: 5
~Derived()
~Base()

e)

#include <iostream>
 
class Base
{
protected:
    int m_x;
public:
    Base(int x): m_x{ x }
    {
        std::cout << "Base()\n";
    }
    ~Base()
    {
        std::cout << "~Base()\n";
    }
 
    void print() const { std::cout << "Base: " << m_x << '\n';  }
};
 
class Derived: public Base
{
public:
    Derived(int y):  Base{ y }
    {
        std::cout << "Derived()\n";
    }
    ~Derived()
    {
        std::cout << "~Derived()\n";
    }
 
    void print() { std::cout << "Derived: " << m_x << '\n'; }
};
 
class D2 : public Derived
{
public:
    D2(int z): Derived{ z }
    {
        std::cout << "D2()\n";
    }
    ~D2()
    {
        std::cout << "~D2()\n";
    }
 
        // обратите внимание: здесь нет функции print()
};
 
int main()
{
    D2 d{ 5 };
    d.print();
 
    return 0;
}

Base()
Derived()
D2()
Derived: 5
~D2()
~Derived()
~Base()

Вопрос 2

a) Напишите классы Apple и Banana, производные от общего класса Fruit. У Fruit должно быть два члена: название и цвет.

Должна запуститься следующая программа:

int main()
{
    Apple a{ "red" };
    Banana b{};
 
    std::cout << "My " << a.getName() << " is " << a.getColor() << ".\n";
    std::cout << "My " << b.getName() << " is " << b.getColor() << ".\n";
    
    return 0;
}

И выдать следующий результат:

My apple is red.
My banana is yellow.

#include <iostream>
#include <string>
 
class Fruit
{
private:
    std::string m_name;
    std::string m_color;
 
public:
    Fruit(const std::string& name, const std::string& color)
        : m_name{ name }, m_color{ color }
    {
 
    }
 
    const std::string& getName() const { return m_name; }
    const std::string& getColor() const { return m_color; }
};
 
class Apple: public Fruit
{
public:
    Apple(const std::string& color="red")
        : Fruit{ "apple", color }
    {
    }
};
 
class Banana : public Fruit
{
public:
    Banana()
        : Fruit{ "banana", "yellow" }
    {
 
    }
};
 
int main()
{
    Apple a{ "red" };
    Banana b;
 
    std::cout << "My " << a.getName() << " is " << a.getColor() << ".\n";
    std::cout << "My " << b.getName() << " is " << b.getColor() << ".\n";
    
    return 0;
}

b) Добавьте в предыдущую программу новый класс GrannySmith (такой сорт яблок), унаследованный от Apple.

Должна запуститься следующая программа:

int main()
{
    Apple a{ "red" };
    Banana b;
    GrannySmith c;
 
    std::cout << "My " << a.getName() << " is " << a.getColor() << ".\n";
    std::cout << "My " << b.getName() << " is " << b.getColor() << ".\n";
    std::cout << "My " << c.getName() << " is " << c.getColor() << ".\n";
    
    return 0;
}

И выдать следующий результат:

My apple is red.
My banana is yellow.
My granny smith apple is green.

#include <iostream>
#include <string>
 
class Fruit
{
private:
    std::string m_name;
    std::string m_color;
 
public:
    Fruit(const std::string& name, const std::string& color)
        : m_name{ name }, m_color{ color }
    {
 
    }
 
    const std::string& getName() const { return m_name; }
    const std::string& getColor() const { return m_color; }
};
 
class Apple: public Fruit
{
// Предыдущий конструктор, который мы использовали для Apple,
// имел фиксированное название(«apple»).
// Нам нужен новый конструктор, который GrannySmith будет
// использовать для установки названия фрукта
protected: // защищенный, поэтому доступ могут получить только производные классы
    Apple(const std::string& name, const std::string& color)
        : Fruit{ name, color }
    {
    }
 
public:
    Apple(const std::string& color="red")
        : Fruit{ "apple", color }
    {
    }
};
 
class Banana : public Fruit
{
public:
    Banana()
        : Fruit{ "banana", "yellow" }
    {
 
    }
};
 
class GrannySmith : public Apple
{
public:
    GrannySmith()
        : Apple{ "granny smith apple", "green" }
    {
 
    }
};
 
int main()
{
    Apple a{ "red" };
    Banana b;
    GrannySmith c;
 
    std::cout << "My " << a.getName() << " is " << a.getColor() << ".\n";
    std::cout << "My " << b.getName() << " is " << b.getColor() << ".\n";
    std::cout << "My " << c.getName() << " is " << c.getColor() << ".\n";
 
    return 0;
}

Вопрос 3

Время испытания! Следующий вопрос теста более сложный и длинный. Мы собираемся написать простую игру, в которой вы сражаетесь с монстрами. Цель игры – собрать как можно больше золота перед смертью или достижением 20 уровня.

Наша программа будет состоять из 3 классов: класса Creature (существо), класса Player (игрок) и класса Monster (монстр). И Player, и Monster наследуются от Creature.

a) Сначала создайте класс Creature. Существа Creature имеют 5 атрибутов: имя (std::string), символ (char), количество здоровья (int), количество урона, которое они наносят за атаку (int), и количество золота, которое они несут (int). Реализуйте их как члены класса. Напишите полный набор методов-геттеров (функций получения для каждого члена). Добавьте еще три функции:

  • void reduceHealth(int) уменьшает здоровье Creature на целочисленное значение;
  • bool isDead() возвращает true, когда здоровье Creature равно или меньше нуля;
  • void addGold(int) добавляет Creature золото.

Должна запуститься следующая программа:

#include <iostream>
#include <string>
 
int main()
{
    Creature o{ "orc", 'o', 4, 2, 10 };
    o.addGold(5);
    o.reduceHealth(1);
    std::cout << "The " << o.getName() << " has " << o.getHealth()
              << " health and is carrying " << o.getGold() << " gold.\n";
 
    return 0;
}

И выдать следующий результат:

The orc has 3 health and is carrying 15 gold.

#include <iostream>
#include <string>
#include <string_view> // требует C++17
 
class Creature
{
protected:
    std::string m_name;
    char m_symbol;
    int m_health;
    int m_damage;
    int m_gold;
 
public:
    Creature(std::string_view name, char symbol,
             int health, int damage, int gold):
        m_name{ name },
        m_symbol{ symbol },
        m_health{ health },
        m_damage{ damage },
        m_gold{ gold }
    {
    }
 
    const std::string& getName() const { return m_name; }
    char getSymbol() const { return m_symbol; }
    int getHealth() const { return m_health; }
    int getDamage() const { return m_damage; }
    int getGold() const { return m_gold; }
 
    void reduceHealth(int health) { m_health -= health; }
    bool isDead() const { return m_health <= 0; }
    void addGold(int gold) { m_gold += gold; }
};
 
int main()
{
    Creature o{ "orc", 'o', 4, 2, 10 };
    o.addGold(5);
    o.reduceHealth(1);
    std::cout << "The " << o.getName() << " has " << o.getHealth()
              << " health and is carrying " << o.getGold() << " gold.\n";
 
    return 0;
}

b) Теперь мы собираемся создать класс Player. Класс Player наследуется от Creature. У игрока есть одна дополнительная переменная-член, уровень игрока, который начинается с 1. У игрока есть имя собственное (введенное пользователем), он использует символ '@', имеет 10 единиц здоровья, для начала наносит 1 очко повреждения и не имеет золота. Напишите функцию с именем levelUp(), которая увеличивает уровень и урон игрока на 1. Также напишите функцию-геттер для получения значения уровня. Наконец, напишите функцию с именем hasWon(), которая возвращает true, если игрок достиг 20-го уровня.

Напишите новую функцию main(), которая запрашивает у пользователя имя и выводит следующий результат:

Enter your name: Alex
Welcome, Alex.
You have 10 health and are carrying 0 gold.

#include <iostream>
#include <string>
 
class Creature
{
protected:
    std::string m_name;
    char m_symbol;
    int m_health;
    int m_damage;
    int m_gold;
 
public:
    Creature(std::string_view name, char symbol, int health, int damage, int gold):
        m_name{ name },
        m_symbol{ symbol },
        m_health{ health },
        m_damage{ damage },
        m_gold{ gold }
    {
    }
 
    const std::string& getName() const { return m_name; }
    char getSymbol() const { return m_symbol; }
    int getHealth() const { return m_health; }
    int getDamage() const { return m_damage; }
    int getGold() const { return m_gold; }
 
    void reduceHealth(int health) { m_health -= health; }
    bool isDead() const { return m_health <= 0; }
    void addGold(int gold) { m_gold += gold; }
};
 
class Player : public Creature
{
    int m_level{ 1 };
 
public:
    Player(std::string_view name)
        : Creature{ name, '@', 10, 1, 0 }
    {
    }
 
    void levelUp()
    {
        ++m_level;
        ++m_damage;
    }
 
    int getLevel() const { return m_level; }
    bool hasWon() const { return m_level >= 20; }
};
 
int main()
{
    std::cout << "Enter your name: ";
    std::string playerName;
    std::cin >> playerName;
 
    Player p{ playerName };
    std::cout << "Welcome, " << p.getName() << ".\n";
 
    std::cout << "You have " << p.getHealth() << " health and are carrying "
              << p.getGold() << " gold.\n";
 
    return 0;
}

c) Далее идет класс Monster. Monster также наследуется от Creature. У монстров нет ненаследуемых переменных-членов.

Сначала напишите пустой класс Monster, наследованный от Creature, а затем внутри класса Monster добавьте перечисление с именем Type, которое содержит перечислители для трех монстров, которые будут у нас в этой игре: DRAGON (дракон), ORC (орк) и SLIME (слизь) (вам также понадобится перечислитель max_types, так как он может быть немного полезен).

class Monster : public Creature
{
public:
    enum class Type
    {
        dragon,
        orc,
        slime,
        max_types
    };
};

d) У каждого типа монстра будет свое имя, символ, начальное здоровье, золото и урон. Вот таблица характеристик для каждого типа монстров:

Информация о типах монстров
ТипИмяСимволЗдоровьеУронЗолото
драконdragonD204100
оркorco4225
слизьslimes1110

Следующим шагом будет написание конструктора Monster, чтобы мы могли создавать монстров. Конструктор Monster в качестве параметра должен принимать перечисление Type, а затем создавать объект Monster с соответствующими характеристиками для этого вида монстров.

Есть несколько способов реализовать это (что-то лучше, что-то хуже). Однако в этом случае, поскольку все наши атрибуты монстров предопределены (не случайны), мы будем использовать таблицу поиска. Таблица поиска – это массив, содержащий все предопределенные атрибуты. Мы можем использовать таблицу поиска для поиска атрибутов заданного монстра, когда это необходимо.

Так как же реализовать эту таблицу поиска? Это не трудно. Нам просто нужен массив, содержащий элемент для каждого типа монстра. Каждый элемент массива будет содержать объект существа Creature, который содержит все предопределенные значения атрибутов для этого типа монстра. Мы помещаем этот массив внутри статической функции-члена Monster, чтобы можно было получить объект Creature по умолчанию для заданного Monster::Type.

Определение таблицы поиска будет следующим:

// как закрытый член класса Monster
static const Creature& getDefaultCreature(Type type)
{
  static std::array<Creature, static_cast<std::size_t>(Type::max_types)> monsterData{
    { { "dragon", 'D', 20, 4, 100 },
      { "orc", 'o', 4, 2, 25 },
      { "slime", 's', 1, 1, 10 } }
  };
 
  return monsterData.at(static_cast<std::size_t>(type));
}

Теперь мы можем вызывать эту функцию для поиска любых нужных нам значений! Например, чтобы получить количество золота дракона, мы можем вызвать getDefaultCreature(Type::dragon).getGold().

Используйте эту функцию и делегирующие конструкторы для реализации своего конструктора Monster.

Следующая программа должна компилироваться:

#include <iostream>
#include <string>
 
int main()
{
    Monster m{ Monster::Type::orc };
    std::cout << "A " << m.getName() 
              << " (" << m.getSymbol() << ") was created.\n";
 
    return 0;
}

и напечатать:

A orc (o) was created.

#include <array>
#include <iostream>
#include <string>
#include <string_view> // std::string_view требует C++17
 
class Creature
{
protected:
  std::string m_name;
  char m_symbol;
  int m_health;
  int m_damage;
  int m_gold;
 
public:
  Creature(std::string_view name, char symbol, int health, int damage, int gold)
      : m_name{ name },
        m_symbol{ symbol },
        m_health{ health },
        m_damage{ damage },
        m_gold{ gold }
  {
  }
 
  const std::string& getName() const { return m_name; }
  char getSymbol() const { return m_symbol; }
  int getHealth() const { return m_health; }
  int getDamage() const { return m_damage; }
  int getGold() const { return m_gold; }
 
  void reduceHealth(int health) { m_health -= health; }
  bool isDead() const { return m_health <= 0; }
  void addGold(int gold) { m_gold += gold; }
};
 
class Player : public Creature
{
  int m_level{ 1 };
 
public:
  Player(std::string_view name)
      : Creature{ name, '@', 10, 1, 0 }
  {
  }
 
  void levelUp()
  {
    ++m_level;
    ++m_damage;
  }
 
  int getLevel() const { return m_level; }
};
 
class Monster : public Creature
{
public:
  enum class Type
  {
    dragon,
    orc,
    slime,
    max_types
  };
 
private:
  static const Creature& getDefaultCreature(Type type)
  {
    static std::array<Creature, static_cast<std::size_t>(Type::max_types)> monsterData{
      { { "dragon", 'D', 20, 4, 100 },
        { "orc", 'o', 4, 2, 25 },
        { "slime", 's', 1, 1, 10 } }
    };
 
    return monsterData.at(static_cast<std::size_t>(type));
  }
 
public:
  Monster(Type type)
      : Creature{ getDefaultCreature(type) }
  {
  }
};
 
int main()
{
  Monster m{ Monster::Type::orc };
  std::cout << "A " << m.getName() 
            << " (" << m.getSymbol() << ") was created.\n";
 
  return 0;
}

e) Наконец, добавьте к Monster статическую функцию с именем getRandomMonster(). Эта функция должна выбрать случайное число от 0 до max_types-1 и вернуть монстра (по значению) с этим типом (вам нужно преобразовать int с помощью static_cast в Type, чтобы передать его конструктору Monster).

Урок «9.5 – Генерирование случайных чисел» содержит код, который можно использовать для выбора случайного числа.

Должна запуститься следующая функция main:

#include <iostream>
#include <string>
#include <cstdlib> // для rand() и srand()
#include <ctime>   // для time()
 
int main()
{
    // устанавливаем начальное значение в значение системных часов
    std::srand(static_cast<unsigned int>(std::time(nullptr))); 
    // отбрасываем первый результат
    std::rand(); 
 
    for (int i{ 0 }; i < 10; ++i)
    {
        Monster m{ Monster::getRandomMonster() };
        std::cout << "A " << m.getName()
                  << " (" << m.getSymbol() << ") was created.\n";
    }
 
    return 0;
}

Результаты этой программы должны быть рандомизированы.

#include <array>
#include <cstdlib> // для rand() и srand()
#include <ctime>   // для time()
#include <iostream>
#include <string>
#include <string_view>
 
// Генерируем случайное число от min до max (включительно)
// Предполагается, что std::srand() уже была вызвана
// Предполагается, что max - min <= RAND_MAX
int getRandomNumber(int min, int max)
{
  // static используется для повышения эффективности, поскольку
  // мы вычисляем это значение только один раз
  static constexpr double fraction{ 1.0 / (RAND_MAX + 1.0) }; 
  // равномерно распределяем случайное число по нашему диапазону
  return min + static_cast<int>((max - min + 1) * (std::rand() * fraction));
}
 
class Creature
{
protected:
  std::string m_name;
  char m_symbol;
  int m_health;
  int m_damage;
  int m_gold;
 
public:
  Creature(const std::string& name, char symbol, int health, int damage, int gold)
      : m_name{ name },
        m_symbol{ symbol },
        m_health{ health },
        m_damage{ damage },
        m_gold{ gold }
  {
  }
 
  char getSymbol() const { return m_symbol; }
  const std::string& getName() const { return m_name; }
  bool isDead() const { return m_health <= 0; }
  int getGold() const { return m_gold; }
  void addGold(int gold) { m_gold += gold; }
  void reduceHealth(int health) { m_health -= health; }
  int getHealth() const { return m_health; }
  int getDamage() const { return m_damage; }
};
 
class Player : public Creature
{
  int m_level{ 1 };
 
public:
  Player(const std::string& name)
      : Creature{ name, '@', 10, 1, 0 }
  {
  }
 
  void levelUp()
  {
    ++m_level;
    ++m_damage;
  }
 
  int getLevel() const { return m_level; }
  bool hasWon() const { return m_level >= 20; }
};
 
class Monster : public Creature
{
public:
  enum class Type
  {
    dragon,
    orc,
    slime,
    max_types
  };
 
private:
  static const Creature& getDefaultCreature(Type type)
  {
    static std::array<Creature, static_cast<std::size_t>(Type::max_types)> monsterData{
      { { "dragon", 'D', 20, 4, 100 },
        { "orc", 'o', 4, 2, 25 },
        { "slime", 's', 1, 1, 10 } }
    };
 
    return monsterData.at(static_cast<std::size_t>(type));
  }
 
public:
  Monster(Type type)
      : Creature{ getDefaultCreature(type) }
  {
  }
 
  static Monster getRandomMonster()
  {
    int num{ getRandomNumber(0, static_cast<int>(Type::max_types) - 1) };
    return Monster{ static_cast<Type>(num) };
  }
};
 
int main()
{
  // устанавливаем начальное значение в значение системных часов
  std::srand(static_cast<unsigned int>(std::time(nullptr))); 
  // отбрасываем первый результат
  std::rand(); 
 
  for (int i{ 0 }; i < 10; ++i)
  {
    Monster m{ Monster::getRandomMonster() };
    std::cout << "A " << m.getName() 
              << " (" << m.getSymbol() << ") was created.\n";
  }
 
  return 0;
}

f) Наконец-то мы готовы написать нашу игровую логику!

Вот правила игры:

  • Игрок в какой-либо момент сталкивается только с одним случайно сгенерированным монстром.
  • Для каждого монстра у игрока есть два выбора: (R)un (бежать) или (F)ight (сражаться).
  • Если игрок решает бежать, у него есть 50%-ый шанс сбежать.
  • Если игрок сбегает, он переходит к следующему столкновению без каких-либо негативных последствий.
  • Если игроку не удается сбежать, монстр получает бесплатную атаку, и игрок выбирает свое следующее действие.
  • Если игрок решает сражаться, он атакует первым. Здоровье монстра уменьшается на величину урона игрока.
  • Если монстр умирает, игрок забирает всё золото, которое несет монстр. Игрок также повышает уровень, увеличивая значения своих уровня и урона на 1.
  • Если монстр не умирает, он атакует игрока в ответ. Здоровье игрока уменьшается на величину урона монстра.
  • Игра заканчивается, когда игрок умирает (поражение) или достигает 20 уровня (победа).
  • Если игрок умирает, игра должна сообщить ему, на каком он уровне и сколько у него золота.
  • Если игрок выигрывает, игра должна сообщить ему, что он выиграл, и сколько у него золота.

Вот пример игровой сессии:

Enter your name: Alex
Welcome, Alex
You have encountered a slime (s).
(R)un or (F)ight: f
You hit the slime for 1 damage.
You killed the slime.
You are now level 2.
You found 10 gold.
You have encountered a dragon (D).
(R)un or (F)ight: r
You failed to flee.
The dragon hit you for 4 damage.
(R)un or (F)ight: r
You successfully fled.
You have encountered a orc (o).
(R)un or (F)ight: f
You hit the orc for 2 damage.
The orc hit you for 2 damage.
(R)un or (F)ight: f
You hit the orc for 2 damage.
You killed the orc.
You are now level 3.
You found 25 gold.
You have encountered a dragon (D).
(R)un or (F)ight: r
You failed to flee.
The dragon hit you for 4 damage.
You died at level 3 and with 35 gold.
Too bad you can't take it with you!

Подсказка: создайте 4 функции:

  1. функция main() должна обрабатывать настройку игры (создание игрока) и основной цикл игры;
  2. fightMonster() обрабатывает битву между игроком и одним монстром, в том числе спрашивает игрока, что он хочет сделать, обрабатывает случаи побега или драки;
  3. attackMonster() обрабатывает атаку игрока на монстра, включая повышение уровня;
  4. attackPlayer() обрабатывает атаку монстра на игрока.

#include <array>
#include <cstdlib> // для rand() и srand()
#include <ctime>   // для time()
#include <iostream>
#include <string>
#include <string_view>
 
// Генерируем случайное число от min до max (включительно)
// Предполагается, что std::srand() уже была вызвана
// Предполагается, что max - min <= RAND_MAX
int getRandomNumber(int min, int max)
{
  // static используется для повышения эффективности, поскольку
  // мы вычисляем это значение только один раз
  static constexpr double fraction{ 1.0 / (RAND_MAX + 1.0) }; 
  // равномерно распределяем случайное число по нашему диапазону
  return min + static_cast<int>((max - min + 1) * (std::rand() * fraction));
}
 
class Creature
{
protected:
  std::string m_name;
  char m_symbol;
  int m_health;
  int m_damage;
  int m_gold;
 
public:
  Creature(std::string_view name, char symbol, int health, int damage, int gold)
      : m_name{ name },
        m_symbol{ symbol },
        m_health{ health },
        m_damage{ damage },
        m_gold{ gold }
  {
  }
 
  char getSymbol() const { return m_symbol; }
  const std::string& getName() const { return m_name; }
  bool isDead() const { return m_health <= 0; }
  int getGold() const { return m_gold; }
  void addGold(int gold) { m_gold += gold; }
  void reduceHealth(int health) { m_health -= health; }
  int getHealth() const { return m_health; }
  int getDamage() const { return m_damage; }
};
 
class Player : public Creature
{
  int m_level{ 1 };
 
public:
  Player(std::string_view name)
      : Creature{ name, '@', 10, 1, 0 }
  {
  }
 
  void levelUp()
  {
    ++m_level;
    ++m_damage;
  }
 
  int getLevel() const { return m_level; }
  bool hasWon() const { return m_level >= 20; }
};
 
class Monster : public Creature
{
public:
  enum class Type
  {
    dragon,
    orc,
    slime,
    max_types
  };
 
private:
  static const Creature& getDefaultCreature(Type type)
  {
    static std::array<Creature, static_cast<std::size_t>(Type::max_types)> monsterData{
      { { "dragon", 'D', 20, 4, 100 },
        { "orc", 'o', 4, 2, 25 },
        { "slime", 's', 1, 1, 10 } }
    };
 
    return monsterData.at(static_cast<std::size_t>(type));
  }
 
public:
  Monster(Type type)
      : Creature{ getDefaultCreature(type) }
  {
  }
 
  static Monster getRandomMonster()
  {
    int num{ getRandomNumber(0, static_cast<int>(Type::max_types) - 1) };
    return Monster{ static_cast<Type>(num) };
  }
};
 
// Эта функция обрабатывает атаку игрока на монстра
void attackMonster(Player& player, Monster& monster)
{
  // Если игрок мертв, мы не можем атаковать монстра
  if (player.isDead())
    return;
 
  std::cout << "You hit the " << monster.getName() << " for "
            << player.getDamage() << " damage.\n";
 
  // Уменьшаем здоровье монстра на урон игрока
  monster.reduceHealth(player.getDamage());
 
  // Если монстр теперь мертв, повышаем уровень игрока
  if (monster.isDead())
  {
    std::cout << "You killed the " << monster.getName() << ".\n";
    player.levelUp();
    std::cout << "You are now level " << player.getLevel() << ".\n";
    std::cout << "You found " << monster.getGold() << " gold.\n";
    player.addGold(monster.getGold());
  }
}
 
// Эта функция обрабатывает атаку монстра на игрока
void attackPlayer(const Monster& monster, Player& player)
{
  // Если монстр мертв, он не может атаковать игрока
  if (monster.isDead())
    return;
 
  // Уменьшаем здоровье игрока на урон монстра
  player.reduceHealth(monster.getDamage());
  std::cout << "The " << monster.getName() << " hit you for "
            << monster.getDamage() << " damage.\n";
}
 
// Эта функция обрабатывает весь бой между игроком и
// случайно сгенерированным монстром
void fightMonster(Player& player)
{
  // Сначала генерируем монстра случайным образом
  Monster monster{ Monster::getRandomMonster() };
  std::cout << "You have encountered a " << monster.getName()
            << " (" << monster.getSymbol() << ").\n";
 
  // Пока монстр не мертв и игрок не мертв, бой продолжается
  while (!monster.isDead() && !player.isDead())
  {
    std::cout << "(R)un or (F)ight: ";
    char input{};
    std::cin >> input;
    if (input == 'R' || input == 'r')
    {
      // 50%-ый шанс успешно сбежать
      if (getRandomNumber(1, 2) == 1)
      {
        std::cout << "You successfully fled.\n";
        return; // успех завершает столкновение
      }
      else
      {
        // Неспособность убежать дает монстру бесплатную атаку на игрока
        std::cout << "You failed to flee.\n";
        attackPlayer(monster, player);
        continue;
      }
    }
 
    if (input == 'F' || input == 'f')
    {
      // Игрок атакует первым, монстр - вторым
      attackMonster(player, monster);
      attackPlayer(monster, player);
    }
  }
}
 
int main()
{
  // устанавливаем начальное значение в значение системных часов
  std::srand(static_cast<unsigned int>(std::time(nullptr)));
  // отбрасываем первый результат
  std::rand(); 
 
  std::cout << "Enter your name: ";
  std::string playerName;
  std::cin >> playerName;
 
  Player player{ playerName };
  std::cout << "Welcome, " << player.getName() << '\n';
 
  // Если игрок не умер и еще не выиграл, игра продолжается
  while (!player.isDead() && !player.hasWon())
    fightMonster(player);
 
  // На данный момент игрок либо мертв, либо выиграл
  if (player.isDead())
  {
    std::cout << "You died at level " << player.getLevel()
              << " and with " << player.getGold() << " gold.\n";
    std::cout << "Too bad you can't take it with you!\n";
  }
  else
  {
    std::cout << "You won the game with " << player.getGold() << " gold!\n";
  }
 
  return 0;
}

g) Дополнительный вопрос: игрок Том недостаточно заточил свой меч, чтобы победить могучего дракона. Помогите ему, реализовав следующие зелья разных размеров:

Зелья
ТипЭффект (малое зелье)Эффект (среднее зелье)Эффект (большое зелье)
Здоровье+2 к здоровью+2 к здоровью+5 к здоровью
Сила+1 к урону+1 к урону+1 к урону
Яд-1 к здоровью-1 к здоровью-1 к здоровью

Не стесняйтесь проявить творческий подход и добавить больше зелий или изменить их эффекты!

У игрока есть 30% шанс найти зелье после каждого выигранного боя, и у него есть выбор: пить или не пить его. Если игрок не выпьет зелье, оно исчезнет. Игрок не знает, какой тип зелья был найден, пока не выпьет его, после чего раскрывается тип и размер зелья, и применяется эффект.

В следующем примере игрок нашел ядовитое зелье и умер, выпив его (яд в этом примере был гораздо более смертоносным).

You have encountered a slime (s).
(R)un or (F)ight: f
You hit the slime for 1 damage.
You killed the slime.
You are now level 2.
You found 10 gold.
You found a mythical potion! Do you want to drink it? [y/n]: y
You drank a Medium potion of Poison
You died at level 2 and with 10 gold.
Too bad you can't take it with you!

Добавьте класс Potion, который имеет переменные-члены типа и размера, а также функцию-член, которая возвращает его название, и статическую функцию-член, которая создает случайное зелье, подобно функции getRandomMonster().

В классе Player добавьте функцию-член drinkPotion(), которая применяет эффект зелья.

#include <array>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <sstream> // для std::stringstream
#include <string>
#include <string_view>
 
int getRandomNumber(int min, int max)
{
  static constexpr double fraction{ 1.0 / (RAND_MAX + 1.0) };
  return min + static_cast<int>((max - min + 1) * (std::rand() * fraction));
}
 
// Все возможные виды зелий
enum class PotionType
{
  health,
  strength,
  poison,
 
  // Для случайного создания зелья
  max_type
};
 
enum class PotionSize
{
  small,
  medium,
  large,
 
  max_size
};
 
// Названия зелий являются литералами времени компиляции,
// мы можем возвращать std::string_view.
std::string_view getPotionTypeName(PotionType type)
{
  static constexpr std::array names{
    "Health",
    "Strength",
    "Poison"
  };
 
  return names.at(static_cast<std::size_t>(type));
}
 
std::string_view getPotionSizeName(PotionSize size)
{
  static constexpr std::array names{
    "Small",
    "Medium",
    "Large"
  };
 
  return names.at(static_cast<std::size_t>(size));
}
 
class Potion
{
private:
  PotionType m_type{};
  PotionSize m_size{};
 
public:
  Potion(PotionType type, PotionSize size)
      : m_type{ type },
        m_size{ size }
  {
  }
 
  PotionType getType() const { return m_type; }
  PotionSize getSize() const { return m_size; }
 
  std::string getName() const
  {
    // Мы используем std::stringstream, но это также можно
    // решить с помощью std::string.
    // Мы впервые использовали std::stringstream в уроке 7.13.
    std::stringstream result{};
 
    result << getPotionSizeName(getSize()) << " potion of "
           << getPotionTypeName(getType());
 
    // Мы можем извлечь строку из std::stringstream с помощью
    // str() функции-члена.
    return result.str();
  }
 
  static Potion getRandomPotion()
  {
    return {
      static_cast<PotionType>(getRandomNumber(0, static_cast<int>(PotionType::max_type) - 1)),
      static_cast<PotionSize>(getRandomNumber(0, static_cast<int>(PotionSize::max_size) - 1))
    };
  }
};
 
class Creature
{
protected:
  std::string m_name;
  char m_symbol;
  int m_health;
  int m_damage;
  int m_gold;
 
public:
  Creature(std::string_view name, char symbol, int health, int damage, int gold)
      : m_name{ name },
        m_symbol{ symbol },
        m_health{ health },
        m_damage{ damage },
        m_gold{ gold }
  {
  }
 
  char getSymbol() const { return m_symbol; }
  const std::string& getName() const { return m_name; }
  bool isDead() const { return m_health <= 0; }
  int getGold() const { return m_gold; }
  void addGold(int gold) { m_gold += gold; }
  void reduceHealth(int health) { m_health -= health; }
  int getHealth() const { return m_health; }
  int getDamage() const { return m_damage; }
};
 
class Player : public Creature
{
  int m_level{ 1 };
 
public:
  Player(std::string_view name)
      : Creature{ name, '@', 10, 1, 0 }
  {
  }
 
  void levelUp()
  {
    ++m_level;
    ++m_damage;
  }
 
  int getLevel() const { return m_level; }
  bool hasWon() const { return m_level >= 20; }
 
  // Применяет эффект зелья к игроку
  void drinkPotion(const Potion& potion)
  {
    switch (potion.getType())
    {
    case PotionType::health:
      // Только размер зелья здоровья влияет на его силу.
      // Все остальные зелья не зависят от размера.
      m_health += ((potion.getSize() == PotionSize::large) ? 5 : 2);
      break;
    case PotionType::strength:
      ++m_damage;
      break;
    case PotionType::poison:
      reduceHealth(1);
      break;
    // Обрабатываем max_type, чтобы отключить предупреждение компилятора.
    // Не используем default:, потому что хотим, чтобы компилятор
    // предупредил нас, если мы добавим новое зелье, но
    // забудем реализовать его эффект.
    case PotionType::max_type:
      break;
    }
  }
};
 
class Monster : public Creature
{
public:
  enum class Type
  {
    dragon,
    orc,
    slime,
    max_types
  };
 
private:
  static const Creature& getDefaultCreature(Type type)
  {
    static std::array<Creature, static_cast<std::size_t>(Type::max_types)> monsterData{
      { { "dragon", 'D', 20, 4, 100 },
        { "orc", 'o', 4, 2, 25 },
        { "slime", 's', 1, 1, 10 } }
    };
 
    return monsterData.at(static_cast<std::size_t>(type));
  }
 
public:
  Monster(Type type)
      : Creature{ getDefaultCreature(type) }
  {
  }
 
  static Monster getRandomMonster()
  {
    int num{ getRandomNumber(0, static_cast<int>(Type::max_types) - 1) };
    return Monster{ static_cast<Type>(num) };
  }
};
 
// Мы убрали это из attackMonster(), чтобы функция была короче.
void onMonsterKilled(Player& player, const Monster& monster)
{
  std::cout << "You killed the " << monster.getName() << ".\n";
  player.levelUp();
  std::cout << "You are now level " << player.getLevel() << ".\n";
  std::cout << "You found " << monster.getGold() << " gold.\n";
  player.addGold(monster.getGold());
 
  // 30%-ый шанс найти зелье
  constexpr int potionChance{ 30 };
  if (getRandomNumber(1, 100) <= potionChance)
  {
    // Создание случайного зелья
    auto potion{ Potion::getRandomPotion() };
 
    std::cout << "You found a mythical potion! Do you want to drink it? [y/n]: ";
    char choice{};
    std::cin >> choice;
 
    if (choice == 'y')
    {
      // Применяем эффект
      player.drinkPotion(potion);
      // Показываем тип и размер зелья
      std::cout << "You drank a " << potion.getName() << '\n';
    }
  }
}
 
void attackMonster(Player& player, Monster& monster)
{
  if (player.isDead())
    return;
 
  std::cout << "You hit the " << monster.getName() << " for "
            << player.getDamage() << " damage.\n";
 
  monster.reduceHealth(player.getDamage());
 
  if (monster.isDead())
  {
    // Наградить игрока
    onMonsterKilled(player, monster);
  }
}
 
void attackPlayer(const Monster& monster, Player& player)
{
  if (monster.isDead())
    return;
 
  player.reduceHealth(monster.getDamage());
  std::cout << "The " << monster.getName() << " hit you for "
            << monster.getDamage() << " damage.\n";
}
 
void fightMonster(Player& player)
{
  Monster monster{ Monster::getRandomMonster() };
  std::cout << "You have encountered a " << monster.getName()
            << " (" << monster.getSymbol() << ").\n";
 
  while (!monster.isDead() && !player.isDead())
  {
    std::cout << "(R)un or (F)ight: ";
    char input;
    std::cin >> input;
    if (input == 'R' || input == 'r')
    {
      if (getRandomNumber(1, 2) == 1)
      {
        std::cout << "You successfully fled.\n";
        return;
      }
      else
      {
        std::cout << "You failed to flee.\n";
        attackPlayer(monster, player);
        continue;
      }
    }
 
    if (input == 'F' || input == 'f')
    {
      attackMonster(player, monster);
      attackPlayer(monster, player);
    }
  }
}
 
int main()
{
  std::srand(static_cast<unsigned int>(std::time(nullptr)));
  std::rand();
 
  std::cout << "Enter your name: ";
  std::string playerName;
  std::cin >> playerName;
 
  Player player{ playerName };
  std::cout << "Welcome, " << player.getName() << '\n';
 
  while (!player.isDead() && !player.hasWon())
    fightMonster(player);
 
  if (player.isDead())
  {
    std::cout << "You died at level " << player.getLevel() 
              << " and with " << player.getGold() << " gold.\n";
    std::cout << "Too bad you can't take it with you!\n";
  }
  else
  {
    std::cout << "You won the game with " << player.getGold() << " gold!\n";
  }
 
  return 0;
}

Теги

C++ / CppLearnCppДля начинающихКласс (программирование)Множественное наследованиеНаследованиеОбучениеОбъектно-ориентированное программирование (ООП)Программирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.


  • 2023-02-23Vladimir

    Спасибо! Было очень интересно, создавать подобного рода мини-игры)