Menu
Coddy logo textTech

Полиморфизм компиляции и времени выполнения

Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 56 из 104.

Полиморфизм означает «много форм» и является основной концепцией ООП, которая позволяет единообразно обрабатывать объекты, при этом они ведут себя по-разному. C++ поддерживает два различных типа полиморфизма, каждый из которых разрешается на разных этапах выполнения программы.

Полиморфизм времени компиляции (также называемый статическим полиморфизмом) разрешается компилятором до выполнения программы. Компилятор точно определяет, какую функцию вызывать, на основе сигнатуры функции. Это включает в себя перегрузку функций и шаблоны:

void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }

print(5);      // Компилятор выбирает print(int)
print(3.14);   // Компилятор выбирает print(double)

Полиморфизм времени выполнения (также называемый динамическим полиморфизмом) разрешается во время выполнения программы. Решение о том, какую функцию вызывать, зависит от фактического типа объекта, а не от типа указателя или ссылки. Это достигается с помощью виртуальных функций:

class Shape {
public:
    virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};

class Circle : public Shape {
public:
    void draw() override { std::cout << "Drawing circle" << std::endl; }
};

Shape* s = new Circle();
s->draw();  // Определяется во время выполнения: "Drawing circle"

Ключевой компромисс: полиморфизм времени компиляции имеет нулевые накладные расходы во время выполнения, так как решения принимаются на этапе компиляции, в то время как полиморфизм времени выполнения добавляет небольшие затраты (поиск в vtable), но обеспечивает большую гибкость для работы с объектами, типы которых неизвестны до момента выполнения.

challenge icon

Задание

Легко

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

Вы организуете свой код в трех файлах:

  • Calculator.h: Определите базовый класс Calculator, который представляет любую стратегию вычислений:
    • Виртуальный метод calculate(int a, int b), который возвращает int и выводит: "Base calculation: " << a << " ? " << b (возвращая 0)
    • Виртуальный деструктор
  • Operations.h: Определите два производных класса калькулятора, которые переопределяют поведение вычислений:
    • Adder: Переопределите calculate(), чтобы выводить "Adding: " << a << " + " << b и возвращать сумму
    • Multiplier: Переопределите calculate(), чтобы выводить "Multiplying: " << a << " * " << b и возвращать произведение
    Оба класса должны использовать ключевое слово override.
  • main.cpp: Создайте систему, демонстрирующую оба типа полиморфизма. Считайте два целых числа (каждое на отдельной строке).

    Сначала продемонстрируйте полиморфизм времени компиляции, создав три перегруженные функции display():

    • display(int x) выводит: "Integer value: " << x
    • display(double x) выводит: "Double value: " << x
    • display(const std::string& x) выводит: "String value: " << x

    Затем продемонстрируйте полиморфизм времени выполнения, создав массив указателей Calculator*, содержащий базовый Calculator, Adder и Multiplier. Пройдите в цикле и вызовите calculate() для каждого с вашими входными значениями, выводя результат после каждого вычисления.

    Структурируйте вывод следующим образом:

    === Compile-Time Polymorphism ===
    <вывод display для int, double, string>
    
    === Runtime Polymorphism ===
    <вывод calculate с результатами>

    Для раздела времени компиляции вызовите display() с первым входным значением как целым числом, затем как числом с плавающей точкой (то же значение с добавлением .5), а затем как строкой "Result". По завершении очистите динамически выделенную память для калькуляторов.

Например, при входных данных 10 и 3:

=== Compile-Time Polymorphism ===
Integer value: 10
Double value: 10.5
String value: Result

=== Runtime Polymorphism ===
Base calculation: 10 ? 3
Result: 0
Adding: 10 + 3
Result: 13
Multiplying: 10 * 3
Result: 30

Обратите внимание, как компилятор выбирает правильную перегрузку display() на основе типа аргумента (решение во время компиляции), в то время как правильный метод calculate() определяется фактическим типом объекта во время выполнения через механизм vtable.

Шпаргалка

C++ поддерживает два типа полиморфизма, которые разрешаются на разных этапах:

Полиморфизм времени компиляции (статический полиморфизм) разрешается компилятором перед выполнением. Включает перегрузку функций и шаблоны:

void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }

print(5);      // Компилятор выбирает print(int)
print(3.14);   // Компилятор выбирает print(double)

Полиморфизм времени выполнения (динамический полиморфизм) разрешается во время выполнения с использованием виртуальных функций. Фактический тип объекта определяет, какая функция будет вызвана:

class Shape {
public:
    virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};

class Circle : public Shape {
public:
    void draw() override { std::cout << "Drawing circle" << std::endl; }
};

Shape* s = new Circle();
s->draw();  // Решается во время выполнения: "Drawing circle"

Компромисс: Полиморфизм времени компиляции не имеет накладных расходов во время выполнения, в то время как полиморфизм времени выполнения добавляет небольшую стоимость (поиск в vtable), но обеспечивает большую гибкость.

Попробуйте сами

#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"

// TODO: Создайте три перегруженные функции display():
// 1. display(int x) — выводит "Integer value: <x>"
// 2. display(double x) — выводит "Double value: <x>"
// 3. display(const std::string& x) — выводит "String value: <x>"



int main() {
    // Считайте два целых числа
    int a, b;
    std::cin >> a;
    std::cin >> b;
    
    // === Полиморфизм времени компиляции ===
    std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
    // TODO: Вызовите display() с:
    // — a как integer
    // — a как double (добавьте к нему 0.5)
    // — строкой "Result"
    
    
    std::cout << std::endl;
    
    // === Полиморфизм времени выполнения ===
    std::cout << "=== Runtime Polymorphism ===" << std::endl;
    // TODO: Создайте массив указателей Calculator* из 3 элементов:
    // — базовый Calculator
    // — Adder
    // — Multiplier
    
    // TODO: Пройдите циклом по массиву, вызовите calculate(a, b) для каждого элемента,
    // и выведите "Result: <return_value>" после каждого вычисления
    
    
    // TODO: Освободите динамически выделенную память
    
    
    return 0;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование