Полиморфизм — один из четырех столпов объектно-ориентированного программирования. Полиморфизм означает наличие множества форм. Полиморфизм можно определить как технику, с помощью которой объект может принимать разные формы в зависимости от ситуации.
Но стоит сказать, что объект может вести себя по-разному в разных условиях.
В этой статье мы подробно расскажем о типах полиморфизма, способах реализации полиморфизма, а также о различных других концепциях полиморфизма.
Например, женщина может взять на себя множество ролей в разных ситуациях. Для ребенка она мать, домохозяйка в доме, работница в офисе и т.д. Таким образом, женщина берет на себя разные роли и ведет себя по-разному в разных условиях. Это реальный пример полиморфизма.
Точно так же и в мире программирования, оператор «+», который является оператором двоичного сложения, может вести себя по-разному при изменении операндов. Например, когда оба операнда являются числовыми, выполняется сложение.
С другой стороны, когда операнды представляют собой строку, он действует как оператор конкатенации. Таким образом, полиморфизм в двух словах означает сущность, принимающую множество форм или ведущую себя по-разному в различных условиях.
Типы полиморфизма
Полиморфизм делится на два типа:
- Полиморфизм времени компиляции
- Полиморфизм времени выполнения
Диаграмма, представляющая типы полиморфизма, показана ниже:
Как показано на диаграмме выше, полиморфизм делится на полиморфизм времени компиляции и полиморфизм времени выполнения. Полиморфизм времени компиляции далее делится на перегрузку операторов и перегрузку функций. Полиморфизм времени выполнения дополнительно реализуется с использованием виртуальных функций.
Полиморфизм времени компиляции также известен как связывание или статический полиморфизм. В этом типе полиморфизма метод объекта вызывается во время компиляции. В случае полиморфизма времени выполнения метод объекта вызывается во время выполнения.
Полиморфизм времени выполнения также известен как динамическое или позднее связывание или динамический полиморфизм.
Различия между полиморфизмом времени компиляции и полиморфизмом времени выполнения
Ниже мы рассмотрим основные различия между полиморфизмом временем компиляции и полиморфизмом времени выполнения:
Полиморфизм времени компиляции | Полиморфизм времени выполнения |
Также известен как статический полиморфизм или связывание | Также известен как динамический полиморфизм или динамическое связывание |
Метод объекта вызывается во время компиляции | Метод объекта вызывается во время выполнения |
Обычно реализуется с использованием перегрузки операторов и функций | Реализовано с использованием виртуальных функций и переопределения методов. |
Перегрузка методов — это полиморфизм времени компиляции, при котором несколько методов могут иметь одно и то же имя, но разные списки параметров и типы | Переопределение метода — это полиморфизм времени выполнения, когда несколько методов имеют одно и то же имя с одним и тем же прототипом |
Поскольку методы известны во время компиляции, выполнение выполняется быстрее | Выполнение медленнее, так как метод известен во время выполнения |
Обеспечивает меньшую гибкость для реализации решений, поскольку все должно быть известно во время компиляции | Обеспечивает хорошую гибкость для реализации сложных решений, так как методы определяются во время выполнения |
Полиморфизм времени компиляции
Полиморфизм времени компиляции — это метод, при котором метод объекта вызывается во время компиляции.
Этот тип полиморфизма реализуется двумя способами:
- Перегрузка функций
- Перегрузка оператора
Мы подробно обсудим каждый способ ниже:
Перегрузка функций
Вообще, функция перегружена, когда есть более одной функции с одинаковым именем, но разными типами параметров или разным количеством аргументов.
Таким образом, функция может быть перегружена на основе типов параметров, порядка параметров и количества параметров.
Обратите внимание, что две функции с одинаковым именем и одним и тем же списком параметров, но с разным типом возвращаемого значения не являются перегруженной функцией и приведут к ошибке компиляции при использовании в программе.
Точно так же, когда параметры функции отличаются только указателем и если тип массива эквивалентен, то его не следует использовать для перегрузки.
Другие типы, такие как static и non-static, const и volatile и т.д., или объявления параметров, которые отличаются наличием или отсутствием значений по умолчанию, также не должны использоваться для перегрузки, поскольку они эквивалентны с точки зрения реализации.
Например, следующие прототипы функций являются перегруженными функциями:
Add(int,int); Add(int,float); Add(float,int); Add(int,int,int); |
В приведенных выше прототипах видно, что мы перегружаем функцию Add на основе типа параметров, последовательности или порядка параметров, количества параметров и т. д.
Давайте рассмотрим пример программирования, показывающий перегрузку функций:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> #include <string> using namespace std; class Summation { public: int Add(int num1,int num2) { return num1+num2; } int Add(int num1,int num2, int num3) { return num1+num2+num3; } string Add(string s1,string s2){ return s1+s2; } }; int main(void) { Summation obj; cout<<obj.Add(20, 15)<<endl; cout<<obj.Add(81, 100, 10)<<endl; cout<<obj.Add(10.78,9.56)<<endl; cout<<obj.Add("Hello ","World"); return 0; } |
Вывод данных:
35 191 19 Hello World |
В приведенной выше программе есть класс Summation, в котором определены три перегруженные функции с именем Add, принимающие два целочисленных аргумента, три целочисленных аргумента и два строковых аргумента.
В основной функции мы делаем четыре вызова функций, которые предоставляют различные параметры. Первые два вызова функций просты. В третьем вызове функции Add мы предоставляем два значения с плавающей запятой в качестве аргументов.
В этом случае сопоставляется функция int Add (int, int), так как внутренне число с плавающей запятой преобразуется в double, а затем сопоставляется с функцией с параметрами int. Если бы мы указали double вместо float, то у нас была бы другая перегруженная функция с параметрами double.
Последний вызов функции использует строковые значения в качестве параметров. В этом случае оператор добавления (+) действует как оператор конкатенации и объединяет два строковых значения для создания одной строки.
Преимущества перегрузки функций
Основное преимущество перегрузки функций заключается в том, что она способствует повторному использованию кода. У нас может быть как можно больше функций с одним и тем же именем, если они перегружены на основе типа аргумента, последовательности аргументов и количества аргументов.
Благодаря этому становится проще иметь разные функции с одинаковыми именами для представления поведения одной и той же операции в разных условиях.
Если бы не было перегрузки функций, нам пришлось бы писать слишком много разных функций с разными именами, что делало бы код нечитаемым и сложным для адаптации.
Перегрузка оператора
Перегрузка операторов — это метод, с помощью которого мы придаем другое значение существующим операторам в C++. Другими словами, мы перегружаем операторы, чтобы придать особое значение определяемым пользователем типам данных как объектам.
Большинство операторов в C++ перегружены или имеют специальное значение, чтобы они могли работать с пользовательскими типами данных. Обратите внимание, что при перегрузке основная работа операторов не изменяется. Перегрузка просто придает оператору дополнительное значение, сохраняя его основную семантику.
Хотя большинство операторов в C++ могут быть перегружены, некоторые операторы не могут быть перегружены.
Эти операторы перечислены ниже:
- scope resolution operator(::) (оператор разрешения области видимости)
- sizeof (измерение размера символов)
- member selector(.) (селектор членов)
- member pointer selector(*) (селектор указателя члена)
- ternary operator(?:) (тернарный оператор)
Функции, которые мы используем для перегрузки операторов, называются «операторными функциями».
Операторные функции похожи на обычные функции, но с некоторыми отличиями. Отличие состоит в том, что имя операторной функции начинается с ключевого слова «operator», за которым следует символ оператора, который необходимо перегрузить.
Затем операторная функция вызывается, когда соответствующий оператор используется в программе. Эти операторные функции могут быть функциями-членами, глобальными методами или даже дружественной функцией.
Общий синтаксис операторной функции:
return_type classname::operator op(parameter list) { //function body } |
В синтаксисе «operator op» — это операторная функция, где operator — ключевое слово, а op — оператор, который нужно перегрузить. Return_type — тип возвращаемого значения.
Давайте рассмотрим несколько примеров программирования, для демонстрации перегрузки операторов с помощью операторных функций.
Пример 1: Перегрузка унарного оператора с использованием функции оператора-члена:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <iostream> using namespace std; class Distance { public: int feet; // Конструктор для инициализации значения объекта Distance(int feet) { this->feet = feet; } //функция оператора для перегрузки оператора ++ для выполнения приращения объектов расстояния void operator++() { feet++; } void print(){ cout << "\nIncremented Feet value: " << feet; } }; int main() { Distance d1(9); // Использование (++) унарного оператора ++d1; d1.print(); return 0; } |
Вывод данных:
Incremented Feet value: 10 |
В этой программе мы перегрузили унарный оператор приращения с помощью функции оператора ++. В основной функции мы используем этот оператор ++ для увеличения объекта класса Distance.
Пример 2: Перегрузка бинарного оператора с помощью функции оператора-члена:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include<iostream> using namespace std; class Complex { int real, imag; public: Complex(int r = 0, int i =0) {real = r; imag = i;} //Операторная функция для перегрузки двоичного кода + для сложения двух комплексных чисел Complex operator + (Complex const &obj) { Complex c3; c3.real = real + obj.real; c3.imag = imag + obj.imag; return c3; } void print() { cout << real << " + i" << imag << endl; } }; int main() { Complex c1(2, 5), c2(3, 7); cout<<"c1 = "; c1.print(); cout<<"c2 = "; c2.print(); cout<<"c3 = c1+c2 = "; Complex c3 = c1 + c2; // вызовы перегружены + оператор c3.print(); } |
Вывод данных:
c1 = 2 + i5 c2 = 3 + i7 c3 = c1+c2 = 5 + i12 |
В этой программе мы использовали классический пример сложения двух комплексных чисел с помощью перегрузки оператора. Мы определяем класс для представления комплексных чисел и операторную функцию для перегрузки оператора +, в котором мы складываем действительную и мнимую части комплексных чисел.
В основной функции мы объявляем два сложных объекта и добавляем их с помощью перегруженного оператора +, чтобы получить желаемый результат.
В приведенном ниже примере мы будем использовать дружественную функцию, чтобы добавить два комплексных числа, и увидеть разницу в реализации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include<iostream> using namespace std; class Complex { int real, imag; public: Complex(int r = 0, int i =0) {real = r; imag = i;} //дружественная функция для перегрузки двоичного кода + для сложения двух комплексных чисел friend Complex operator +(Complex const &, Complex const &); void print() { cout << real << " + i" << imag << endl; } }; Complex operator + (Complex const &c1, Complex const &c2) { Complex c3; c3.real = c1.real + c2.real; c3.imag = c1.imag + c2.imag; return c3; } int main() { Complex c1(2, 5), c2(3, 7); cout<<"c1 = "; c1.print(); cout<<"c2 = "; c2.print(); cout<<"c3 = c1+c2 = "; Complex c3 = c1 + c2; // вызовы перегружены + оператор c3.print(); } |
Вывод данных:
c1 = 2 + i5 c2 = 3 + i7 c3 = c1+c2 = 5 + i12 |
Мы можем видеть, что вывод программы такой же. Единственная разница в реализации — использование дружественной функции для перегрузки оператора + вместо функции-члена в предыдущей реализации.
Когда дружественная функция используется для бинарного оператора, мы должны явно указать оба операнда для функции. Точно так же, когда унарный оператор перегружается с помощью дружественной функции, нам нужно предоставить функции единственный операнд.
Помимо операторных функций, мы также можем написать оператор преобразования, который используется для преобразования одного типа в другой. Эти перегруженные операторы преобразования должны быть функцией-членом класса.
Пример 3: Перегрузка оператора с использованием оператора преобразования:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> using namespace std; class DecFraction { int numerator, denom; public: DecFraction(int num, int denm) { numerator = num; denom = denm; } // оператор преобразования: преобразует дробь в значение с плавающей запятой и возвращает его operator float() const { return float(numerator) / float(denom); } }; int main() { DecFraction df(3, 5); //объект класса float res_val = df; //оператор преобразования вызовов cout << "The resultant value of given fraction (3,5)= "<<res_val; return 0; } |
Вывод данных:
The resultant value of given fraction (3,5)= 0.6 |
В этой программе мы использовали оператор преобразования для преобразования заданной дроби в значение с плавающей запятой. После завершения преобразования, оператор преобразования возвращает результирующее значение вызывающему объекту.
В основной функции, когда мы присваиваем объект df переменной res_val, происходит преобразование, и результат сохраняется в res_val.
Мы также можем вызвать конструктор с одним аргументом. Когда мы вызываем конструктор из класса с помощью одного аргумента, это называется конструктором преобразования». Конструктор преобразования можно использовать для неявного преобразования в создаваемый класс.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include<iostream> using namespace std; class Point { private: int x,y; public: Point(int i=0,int j=0) {x = i;y=j;} void print() { cout<<" x = "<<x;cout<<" y = "<<y<<endl; } }; int main() { Point pt(20,30); cout<<"Point constructed using normal constructor"<<endl; pt.print(); cout<<"Point constructed using conversion constructor"<<endl; pt = 10; // здесь вызывается конструктор преобразования pt.print(); return 0; } |
Вывод данных:
Point constructed using normal constructor x = 20 y = 30 Point constructed using conversion constructor x = 10 y = 0 |
В программе есть класс Point, который определяет конструктор со значениями по умолчанию. В основной функции мы строим объект pt с координатами x и y. Затем мы просто присваиваем pt значение 10. Здесь вызывается конструктор преобразования, и x присваивается значение 10, а y присваивается значение по умолчанию 0.
Правила перегрузки оператора
Выполняя перегрузку операторов, мы должны соблюдать следующие правила.
- В C++ мы можем перегружать только существующие операторы. Новые добавленные операторы не могут быть перегружены.
- Когда операторы перегружены, нам нужно убедиться, что хотя бы один из операндов имеет определенный пользователем тип.
- Чтобы перегрузить определенные операторы, мы также можем использовать дружественную функцию.
- Когда мы перегружаем унарные операторы с помощью функции-члена, она не принимает никаких явных аргументов.
- Точно так же, когда бинарные операторы перегружаются с помощью функции-члена, мы должны предоставить функции один явный аргумент. Когда бинарные операторы перегружаются с помощью дружественной функции, функция принимает два аргумента.
- В C++ есть два оператора, которые уже перегружены. Это «=» и «&». Поэтому, чтобы скопировать объект того же класса, нам не нужно перегружать оператор =, и мы можем использовать его напрямую.
Преимущества перегрузки оператора
Перегрузка операторов в C++ позволяет нам расширить функциональные возможности операторов на определяемые пользователем типы, включая объекты класса, в дополнение к встроенным типам.
Расширяя функциональность оператора на пользовательские типы, нам не нужно писать сложный код для выполнения различных операций с пользовательскими типами, но мы можем сделать это в одной операции, как и встроенные типы.
Итог
Полиморфизм времени компиляции обеспечивает возможность перегрузки в основном для расширения функциональности кода с точки зрения перегрузки функций и перегрузки операторов.
Благодаря перегрузке функций мы можем написать более одной функции с одним и тем же именем, но с разными параметрами и типами. Это делает код простым и легко читаемым. Путем перегрузки операторов мы можем расширить функциональные возможности операторов, чтобы мы могли выполнять базовые операции и с пользовательскими типами.
В нашей следующей статье мы расскажем вам все о полиморфизме времени выполнения в C++.
С Уважением, МониторБанк