Skip to content

Практическая работа №12


ООП: Полиморфизм и перегрузка методов (C++)

Section titled “ООП: Полиморфизм и перегрузка методов (C++)”

Цель работы: Изучить и закрепить принципы полиморфизма в C++, понять различие между статической (компиляционной) и динамической (времён выполнения) перегрузкой, освоить виртуальные функции, чисто виртуальные методы (абстрактные классы), а также реализовать примеры перегрузки методов в рамках классов.

Основные теоретические положения

Section titled “Основные теоретические положения”

1. Что такое полиморфизм

Section titled “1. Что такое полиморфизм”

Полиморфизм — это способность объектов разных типов отвечать на одинаковые сообщения (вызовы методов) по-разному. В C++ полиморфизм достигается двумя основными способами:

  • Компиляторный (статический) полиморфизм — перегрузка функций/методов (function/method overloading) и шаблоны (templates). Разрешается на этапе компиляции.
  • Динамический (время выполнения) — реализуется через указатели/ссылки на базовый класс и виртуальные функции (virtual). Решение, какую реализацию вызвать, принимается во время выполнения (dynamic dispatch).

2. Виртуальные функции и динамический полиморфизм

Section titled “2. Виртуальные функции и динамический полиморфизм”
  • Объявление метода с ключевым словом virtual в базовом классе позволяет производным классам переопределять этот метод.
  • Для корректного освобождения ресурсов в иерархии классов базовый класс должен иметь виртуальный деструктор.
  • Чисто виртуальные функции (virtual void f() = 0;) делают класс абстрактным — от него нельзя создать объект, он служит интерфейсом.

Пример ключевых моментов:

class Base {
public:
virtual void doWork(); // виртуальная функция
virtual ~Base(); // виртуальный деструктор
};
class Derived : public Base {
public:
void doWork() override; // переопределение
};

3. Перегрузка методов (method overloading)

Section titled “3. Перегрузка методов (method overloading)”
  • Перегрузка функций/методов — наличие в пределах одной области видимости нескольких функций с одинаковым именем, но разными параметрами (количество/типы/порядок).
  • Перегрузка разрешается на этапе компиляции по сигнатурам функций. Возвращаемый тип сам по себе не является критерием перегрузки.

Пример перегрузки методов в классе:

class Printer {
public:
void print(int x);
void print(double x);
void print(const std::string& s);
};

4. Отличие перегрузки и переопределения

Section titled “4. Отличие перегрузки и переопределения”
  • Перегрузка (overload) — несколько методов с одним именем, разными сигнатурами, определяется в пределах одного класса (или наследуемых классов, но сигнатуры различны).
  • Переопределение (override) — производный класс даёт свою реализацию виртуального метода базового класса (одинаковая сигнатура).

Рекомендации по работе со ссылками и указателями

Section titled “Рекомендации по работе со ссылками и указателями”
  • Для достижения полиморфного поведения обычно используют ссылки или указатели на базовый класс:
Base* p = new Derived();
p->doWork(); // вызывает Derived::doWork, если doWork виртуальная
  • Если использовать объект по значению (Base b = Derived();), срезание (object slicing) отрежет часть объекта производного класса — полиморфизм утрачивается.

Алгоритмы и шаблоны реализации

Section titled “Алгоритмы и шаблоны реализации”

1. Алгоритм динамического полиморфизма (виртуальные функции)

Section titled “1. Алгоритм динамического полиморфизма (виртуальные функции)”
  1. Определить абстрактный/базовый класс с виртуальным методом (интерфейс).
  2. Создать несколько производных классов, переопределить виртуальные методы.
  3. Хранить объекты через указатели/ссылки на базовый класс.
  4. Вызвать метод через базовый указатель — будет выполнена реализация производного класса.

2. Алгоритм перегрузки методов

Section titled “2. Алгоритм перегрузки методов”
  1. Внутри класса объявить несколько методов с одинаковым именем и разными параметрами.
  2. Реализовать каждую версию методa.
  3. Вызывать соответствующую версию — компилятор выберет подходящую по типам аргументов.

Пример 1 — Иерархия фигур (Shape) — полиморфизм

Section titled “Пример 1 — Иерархия фигур (Shape) — полиморфизм”
#include <iostream>
#include <vector>
#include <memory>
#include <cmath>
class Shape {
public:
virtual double area() const = 0; // чисто виртуальная — Shape абстрактен
virtual void printInfo() const {
std::cout << "Shape: area = " << area() << "\n";
}
virtual ~Shape() = default; // виртуальный деструктор
};
class Circle : public Shape {
double r;
public:
Circle(double radius) : r(radius) {}
double area() const override { return M_PI * r * r; }
void printInfo() const override {
std::cout << "Circle: r = " << r << ", area = " << area() << "\n";
}
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double width, double height) : w(width), h(height) {}
double area() const override { return w * h; }
void printInfo() const override {
std::cout << "Rectangle: w = " << w << ", h = " << h << ", area = " << area() << "\n";
}
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));
shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));
for (const auto& s : shapes) {
s->printInfo(); // полиморфный вызов
}
return 0;
}

Пример 2 — Перегрузка методов в классе

Section titled “Пример 2 — Перегрузка методов в классе”
#include <iostream>
#include <string>
class Logger {
public:
void log(int value) {
std::cout << "[int] " << value << "\n";
}
void log(double value) {
std::cout << "[double] " << value << "\n";
}
void log(const std::string& value) {
std::cout << "[string] " << value << "\n";
}
};
int main() {
Logger lg;
lg.log(10);
lg.log(3.14);
lg.log(std::string("hello"));
return 0;
}

Пример 3 — Полиморфизм + перегрузка

Section titled “Пример 3 — Полиморфизм + перегрузка”
#include <iostream>
#include <memory>
class Animal {
public:
virtual void speak() const { std::cout << "Animal sound\n"; }
// Перегруженный метод eat
void eat() const { std::cout << "Animal eats generic food\n"; }
void eat(int grams) const { std::cout << "Animal eats " << grams << " grams\n"; }
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override { std::cout << "Woof!\n"; }
// eat перегрузки унаследуются; можно добавить свои
void eat(const std::string& food) const { std::cout << "Dog eats " << food << "\n"; }
};
int main() {
std::unique_ptr<Animal> a = std::make_unique<Dog>();
a->speak(); // вызывает Dog::speak()
a->eat(); // вызывает Animal::eat()
a->eat(200); // вызывает Animal::eat(int)
// Через прямой объект Dog можно вызвать свои версии
Dog d;
d.eat(std::string("bone"));
return 0;
}

Замечание: если в производном классе объявлена перегрузка метода с тем же именем, но иные сигнатуры, компилятор может скрыть (name hiding) базовые версии. Чтобы избежать этого, в производном классе можно использовать using Base::methodName;.

  1. Написать программу на языке C++ (стандарт C++11 или выше).

  2. Реализовать:

    • Абстрактный базовый класс с хотя бы одной чисто виртуальной функцией.
    • Два класса-наследника с переопределением виртуальных методов.
    • Класс, демонстрирующий перегрузку методов (минимум три версии одного метода).
    • Корректную работу с указателями/ссылками на базовый класс и виртуальным деструктором.
  3. Обработка ввода/вывода данных через std::cin/std::cout для пользовательского взаимодействия.

  4. Комментарии в коде, поясняющие ключевые места: virtual, override, =0, using, unique_ptr и т.д.

Вывод программы (что должно отображаться)

Section titled “Вывод программы (что должно отображаться)”
  • Список объектов и результат их полиморфных вызовов (например, printInfo, speak).
  • Демонстрация вызовов перегруженных методов с разными наборами аргументов.
  • Пояснение о срезании объектов (если демонстрируется) и корректный пример с использованием указателей/ссылок.

Задания для выполнения

Section titled “Задания для выполнения”
ЗадачаОписание
1Абстрактный интерфейсСоздайте базовый абстрактный класс Shape с чисто виртуальным методом area() и два наследника Circle, Rectangle. Выведите площади.
2Виртуальный деструкторПродемонстрируйте важность виртуального деструктура создавая объекты через Shape* и корректно удаляя их.
3Перегрузка методовРеализуйте класс Logger с тремя перегруженными методами log(...). Проверьте работу с разными типами аргументов.
ЗадачаОписание
4Полиморфизм и контейнерСоберите разные фигуры в контейнер std::vector<std::unique_ptr<Shape>> и сложите площади всех фигур.
5Name hiding и usingСоздайте пример, когда в производном классе свои перегрузки скрывают методы базового класса; исправьте поведение с помощью using.
6КомбинацияСоздайте иерархию Animal с виртуальной функцией speak() и перегруженным eat(...). Напишите меню для создания животных и вызова разных eat-версий.
  1. Что такое виртуальная функция и зачем она нужна?
  2. В чём отличие между перегрузкой и переопределением метода?
  3. Почему важен виртуальный деструктор в базовом классе?
  4. Что такое “object slicing” (срезание объектов) и как его избежать?
  5. Как using помогает при проблеме name hiding?
  • Полная реализация базовых требований: 60%.
  • Корректная демонстрация динамического полиморфизма и виртуального деструктора: 20%.
  • Перегрузка методов и примеры вызовов: 10%.
  • Чистота кода и комментарии: 10%.

Рекомендуемая литература

Section titled “Рекомендуемая литература”
  1. Бьерн Страуструп — Язык программирования C++. (Глава про классы и полиморфизм)
  2. Скотт Мейерс — Effective C++ (советы по использованию виртуальных функций и управлению ресурсами)
  3. Шилдт Г. — C++: Полное руководство (разделы по классам, наследованию и перегрузке)