EssayAI
Блог
Блог
Математика и алгоритмы

Полиморфизм в ООП: пример на простом коде и разбор

19 июня 2026Время чтения: 9 минут
#полиморфизм#ООП#переопределение методов#виртуальные методы#наследование
Полиморфизм в ООП: пример на простом коде и разбор

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

Чтобы не теоретизировать в пустоту, соберите ниже короткий запрос под нужный язык и вид полиморфизма - получите рабочий пример с пояснением каждой строки.

Что такое полиморфизм простыми словами

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

Бытовая аналогия: кнопка «играть» на пульте. Нажатие одно, но для DVD-плеера, музыкального центра и игровой консоли «играть» означает разные действия. Пульт (вызывающий код) не знает деталей - он просто шлёт команду, а устройство (объект) реагирует по-своему.

Практический выигрыш в том, что вызывающий код не зависит от количества классов. Добавили новую фигуру - треугольник - и существующий цикл «посчитай площадь каждой фигуры в списке» работает с ней без единой правки. Это прямое продолжение принципов ООП: полиморфизм опирается на наследование и абстракцию, чтобы дать коду гибкость к расширению.

Схема полиморфизма: один вызов метода площадь и три фигуры с разными формулами расчёта
Схема полиморфизма: один вызов метода площадь и три фигуры с разными формулами расчёта

Канонический пример: фигуры и метод площади

Самый наглядный учебный пример - иерархия геометрических фигур. Есть базовый класс с методом area(), и каждый наследник переопределяет его своей формулой:

Sкруг=πr2,Sпрямоуг=ab,Sтреуг=12ahS_{\text{круг}} = \pi r^2, \quad S_{\text{прямоуг}} = a \cdot b, \quad S_{\text{треуг}} = \frac{1}{2} a h

В псевдокоде это выглядит так:

  • базовый класс Figure объявляет метод area() (часто абстрактный - без тела);
  • Circle переопределяет area() как 3.14 * r * r;
  • Rectangle переопределяет area() как a * b;
  • клиентский код держит список Figure[] и в цикле зовёт f.area().

Ключевой момент: переменная объявлена как Figure, но в момент вызова исполняется метод реального класса - Circle или Rectangle. Это и есть полиморфизм времени выполнения. Компилятор не знает заранее, какой объект окажется в переменной, поэтому решение, какой именно area() вызвать, откладывается на момент работы программы - это называется поздним связыванием (late binding).

Если убрать полиморфизм, придётся писать ветвление: if тип==круг then ... else if тип==прямоугольник then ... Каждая новая фигура - правка во всех таких if. Полиморфизм заменяет эту лесенку одним вызовом.

Виды полиморфизма

В типичном вузовском курсе различают несколько форм полиморфизма, и на экзамене часто просят их перечислить с примерами.

Полиморфизм подтипов (subtype polymorphism) - тот самый случай с фигурами: объект подкласса используется там, где ожидается базовый класс. Реализуется через наследование и переопределение методов. Это «классический» ООП-полиморфизм, который имеют в виду по умолчанию.

Параметрический полиморфизм (обобщения, generics) - один код работает с любым типом данных. Список List<T> хранит и числа, и строки, и объекты - реализация одна. В C++ это шаблоны, в Java и C# - дженерики, в Python типизация динамическая по умолчанию.

Ad hoc полиморфизм (перегрузка) - несколько методов с одним именем, но разными параметрами. print(int) и print(string) - два разных метода, выбор между которыми делает компилятор по типам аргументов.

Сравнение двух видов полиморфизма: переопределение в момент выполнения против перегрузки на этапе компиляции
Сравнение двух видов полиморфизма: переопределение в момент выполнения против перегрузки на этапе компиляции

Переопределение против перегрузки

Это главная пара понятий, которую путают чаще всего. Разница принципиальная и завязана на момент принятия решения.

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

Перегрузка (overloading) - в одном классе несколько методов с одинаковым именем, но разными списками параметров. Решение принимается на этапе компиляции по типам аргументов (статическое связывание). Строго говоря, многие теоретики не считают перегрузку «настоящим» полиморфизмом - это скорее синтаксическое удобство.

ПризнакПереопределениеПерегрузка
Где объявленыв разных классах (родитель/наследник)в одном классе
Сигнатураодинаковаяразная (параметры)
Связываниединамическое (runtime)статическое (compile-time)
Вид полиморфизмаподтипов (истинный)ad hoc

Как это работает под капотом: таблица виртуальных методов

Чтобы позднее связывание сработало, компилятору нужен механизм поиска нужного метода в рантайме. В C++, Java, C# это таблица виртуальных методов (vtable).

Каждый класс с виртуальными методами получает скрытую таблицу - массив указателей на свои реализации этих методов. У каждого объекта есть скрытый указатель (vptr) на vtable своего класса. Когда вы зовёте f.area(), программа не подставляет адрес метода сразу: она идёт по vptr объекта в его vtable и берёт оттуда актуальный адрес area(). Поэтому объект Circle, даже спрятанный за ссылкой типа Figure, вызовет именно Circle::area.

Стоимость этого - лишний косвенный переход на каждый вызов (порядка одного-двух указателей). В горячих циклах это иногда заметно, поэтому в C++ виртуальность включается явно ключевым словом virtual, а невиртуальные методы линкуются напрямую и быстрее. В Java все методы виртуальны по умолчанию, кроме static, private и final.

В C++ забытое слово virtual у деструктора базового класса при удалении объекта через указатель на базу приведёт к утечке: вызовется только базовый деструктор. Это классическая ошибка, которую ловят на собеседованиях.

Утиная типизация: полиморфизм без наследования

В динамических языках - Python, Ruby, JavaScript - полиморфизм работает и без общего базового класса. Это принцип утиной типизации: «если нечто крякает как утка и ходит как утка - считаем это уткой».

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

Утиная типизация - повод не путать полиморфизм с наследованием. Наследование - лишь один из способов получить полиморфизм, а не его синоним. Сравните это с инкапсуляцией: та про сокрытие данных, а полиморфизм - про подмену поведения за общим вызовом.

Где полиморфизм нужен на практике

Полиморфизм - не академическая красивость, на нём держатся целые архитектурные приёмы:

  • Обработка коллекции разнородных объектов одним циклом: отрисовать все элементы интерфейса, посчитать зарплату всем типам сотрудников, сериализовать любой документ.
  • Паттерны проектирования: Стратегия, Шаблонный метод, Состояние, Посетитель - все построены на подстановке полиморфных объектов вместо ветвлений.
  • Подмена реализации в тестах: вместо реальной базы данных подставляется заглушка с тем же интерфейсом - код под тестом не замечает разницы.
  • Плагины и расширения: ядро программы работает с интерфейсом, а конкретные реализации подгружаются извне.

Общий принцип - «программируй против интерфейса, а не против реализации». Полиморфизм делает этот принцип возможным.

Частые ошибки

  • Путать переопределение и перегрузку. Переопределение - разные классы, одинаковая сигнатура, выбор в рантайме. Перегрузка - один класс, разные параметры, выбор на компиляции. Это не одно и то же.
  • Считать, что полиморфизм невозможен без наследования. В Python и JS работает утиная типизация - общий метод важнее общего предка.
  • Забывать virtual в C++ у методов и особенно у деструктора базового класса - без него позднего связывания не будет, вызовется метод базового типа.
  • Путать полиморфизм с инкапсуляцией. Инкапсуляция прячет внутренности, полиморфизм подменяет поведение. Это разные киты ООП.
  • Вызывать переопределённый метод по значению, а не по ссылке/указателю. В C++ при копировании объекта в базовый тип происходит «срезка» (slicing) - полиморфизм теряется, остаётся только базовая часть.

FAQ

Чем полиморфизм отличается от наследования? Наследование - это механизм: один класс берёт поля и методы другого. Полиморфизм - это эффект: один вызов даёт разное поведение для разных объектов. Наследование часто служит фундаментом для полиморфизма подтипов, но в динамических языках полиморфизм возможен и без наследования (утиная типизация).

Что такое виртуальный метод? Метод, вызов которого разрешается во время выполнения по реальному типу объекта, а не по типу переменной. В C++ помечается ключевым словом virtual, в Java все нестатические методы виртуальны по умолчанию. Механизм реализуется через таблицу виртуальных методов (vtable).

Является ли перегрузка операторов полиморфизмом? Да, это форма ad hoc полиморфизма: один символ + означает сложение для чисел, конкатенацию для строк и пользовательскую операцию для своего класса. Выбор реализации делает компилятор по типам операндов на этапе компиляции, а не в рантайме.

Коротко

Полиморфизм - способность объектов разных классов отвечать на один вызов своим поведением. Классический пример - метод area(), который для круга и прямоугольника считается по разным формулам, хотя строка кода одна. Главные виды: полиморфизм подтипов (переопределение через наследование, выбор в рантайме), параметрический (дженерики) и ad hoc (перегрузка, выбор на компиляции). Под капотом позднее связывание работает через таблицу виртуальных методов и vptr объекта. В динамических языках полиморфизм даёт утиная типизация - без общего предка. Не путайте переопределение с перегрузкой и полиморфизм с инкапсуляцией: это разные понятия.

Доверьте текст нейросети EssayAI

Открыть EssayAI

Бесплатно, на русском языке и без VPN

Читайте также