В переводе с греческого polymorphos – многообразный. Если один и тот же объект может по-разному использоваться, в зависимости от обстоятельств, то он обладает полиморфизмом. В какой-то степени свойствами полиморфизма обладают, например, автомобили-амфибии – они используются для передвижения и по суше, и по воде.
В объектно-ориентированном программировании используют «раНнее» и «позднее связывание», в результате чего получаются функции-элементы, обладающие полиморфизмом, которые ведут себя по-разному, за счет своих различных свойств, описанных в теле функции.
В случае раннего связывания адреса всех функций и процедур определяются жестко на этапе компоновки программы, т.е. до выполнения программы. Так, в языке C компоновщик прежде всего ищет описание одних функций по заданному имени и связывает их с вызовами, которые имеются в теле других функций.
В противоположность этому, в случае позднего связывания адрес функции-элемента не связывается с ее вызовом до того момента, пока обращение не произойдет фактически, то есть адрес функции связывается с ее вызовом при выполнении программы. В такой динамической системе нельзя заранее предсказать, сколько отображаемых объектов будет на экране, каких они будут типов (окно, меню, диалоговая панель и т.д.) и в какой последовательности пользователь будет с ними работать.
Несмотря на то, что программисты для достижения полиморфизма предпочитают использовать позднее связывание, его можно достичь и ранним связыванием с помощью переопределяемых функций. Компилятор С++, различает функции не только по их именам, но и по типу их аргументов, поэтому достичь полиморфизма функции с аргументами можно путем изменения числа аргументов или их типов.
Если функция с одним именем в программе объявлена более чем один раз, то возможны следующие случаи:
· если возвращаемые значения и сигнатуры (тип и число параметров) нескольких функций совпадают, то второе и последующие объявления трактуются компилятором как переобъявление первого;
· если сигнатуры нескольких функций совпадают, но возвращаемые значения различны, тогда второе и последующие объявления трактуются как ошибочные и во время компиляции выдается сообщение об ошибке;
· если сигнатуры нескольких функций различны (т.е. параметры имеют разные типы или функция имеет различное число параметров), тогда функция считается перегружаемой, при условии, что ее экземпляры расположены в одной области видимости.
Например, функцию, вычисляющую площадь прямоугольника
double sqr (double x, double y){return x*y;}
можно дополнить одноименной, но вычисляющей площадь квадрата, имеющую отличный тип у аргумента x и меньшее число параметров:
int sqr (int x){return x*x;} // перегружаемая функция
Компилятор предварительно просматривает содержащиеся в функции типы и по отличию в типе аргумента определяет ту или иную функцию. Однако нужно иметь в виду, что компилятор не отличит эти же функции, если тип аргумента сделать одинаковым, а изменить лишь тип возвращаемого значения, например:
double sqr (double x){return x*x;};
и
int sqr (double x){return x*x;} // ошибка
Чаще всего механизм перегрузки используется при объявлении конструкторов класса для того, чтобы обеспечить возможность инициализации объектов различными способами, и при перегрузке операций, для того, чтобы обеспечить возможность использования знаков операций для пользовательских типов. Поскольку в языке С++ операции рассматриваются как функции, то их можно переопределять так, что они будут работать не только с числами, а даже с графическими объектами, строками и вообще с чем угодно. Синтаксически перегрузка операций определяется следующим образом:
Тип_возврата operator @ (список_аргументов) { тело оператора};
где @ – знак перегружаемой операции. Использование знака перегруженной операции – это лишь сокращенная форма записи явного вызова функции-операции.
Язык позволяет перегружать следующие операции:
+ |
– |
* |
/ |
% |
^ |
& |
| |
~ |
! |
, |
= |
= = |
!= |
< |
> |
<= |
>= |
++ |
— |
<< |
>> |
&& |
|| |
+= |
– = |
*= |
/= |
%= |
^= |
&= |
|= |
<<= |
|
>>= |
[ ] |
( ) |
-> |
->* |
delete |
Из приведенного списка видно, что могут быть перегружены почти все операции. Исключение составляют следующие операции:
.* |
?: |
:: |
sizeof() |
Рассмотрим некоторые правила перегрузки операторов.
1. Язык не допускает возможности определить для операции новый лексический символ, кроме тех, что определены в языке.
2. Не допускается перегрузка операций для встроенных типов данных. Нельзя, например, переопределить операцию сложения целых чисел:
int operator +(int, int);
3. Нельзя определить дополнительную операцию для встроенных типов. Нельзя, например, определить операцию сложения массивов. Это можно сделать, определив класс, реализующий понятие массива, и операцию сложения в нем.
4. Нельзя переопределить приоритет операции. Например:
х = = у + z;
всегда сначала выполняется operator+, а затем operator= =; однако помощью скобок порядок можно изменить. Вне зависимости от типов данных приоритет операции побитового сложения выше, чем приоритет операции присваивания или сравнения.
Нельзя изменить синтаксис операции в выражении. Если некоторая операция определена в языке как унарная (например, ~), то ее нельзя перегрузить как бинарную. Для встроенных типов четыре предопределенных оператора ("+", "-", "*" и "&") используются либо как унарные, либо как бинарные. В любом из этих качеств они могут быть перегружены. Для всех перегруженных операторов, за исключением operator (), недопустимы аргументы по умолчанию. Если для операции используется
1. префиксная форма записи, то ее нельзя переопределить в постфиксную. Например, если определить операцию отрицания:
void operator !();
то можно писать !а, но нельзя а!.
6. При перегрузке операций «++» и «–» не сохраняется различие между префиксной и постфиксной формами записи.
7. Так как перегружать можно только операции, для которых, по крайней мере, один аргумент представляет тип данных, определенный пользователем, то функция-операция должна быть определена либо как член-функция этого типа, либо как внешняя функция, но дружественная этому типу. Например:
class String
{
…
public:
String operator +(String &);
…
};
или
class String
{
…
public:
friend String operator +(String &, String &);
…
};
8. При перегрузке унарной операции она не должна иметь аргументов, если является членом класса, и должна иметь один аргумент (ссылку на объект), если является внешней функцией. При перегрузке унарной операции как член-функции ей передается неявный аргумент – указатель this на текущий объект.
9. При перегрузке бинарной операции она должна иметь один аргумент (ссылку на объект), если является членом класса, и два аргумента (ссылки на объекты), если является внешней функцией. Бинарная операция, перегружаемая как член-функция, получает один неявный аргумент (первый), а именно указатель this на текущий объект. Бинарные арифметические операции (+ – * / ) должны возвращать объект класса, для которого они используются. Если левый операнд перегружаемой бинарной операции представляет не пользовательский тип, а один из встроенных типов, то тогда такая операция не может быть перегружена как член-функция. В этом случае операция должна быть определена как внешняя функция, у которой первый аргумент представляет один из встроенных типов, а второй — пользовательский тип.
10. Следующие операции = [ ] () -> должны перегружаться только, как члены класса.
Приведем пример перегрузки оператора +. Предположим, имеется класс A, предназначенный для хранения и обработки денежных сумм.
Пример 3
// Defines the entry point for the console application.
class A
{
private:
long Dollars;
int Cents;
public:
A () // Конструктор по умолчанию
{
Dollars = Cents = 0;
}
A (long Dol, int Cen) // Конструктор инициализирующий
{
Dollars = Dol; Cents = Cen;
}
A operator + (A Curr) // Попробуем написать перегрузку оператора так
{
A Temp (Dollars + Curr.Dollars, Cents + Curr.Cents);
return Temp;
}
// другие объявления …
};
Функция-оператор определяется как public, чтобы ее могли использовать другие функции-программы. Если такая функция задана, операцию сложения можно реализовать следующим образом:
A Amount1 (12, 95);
A Amount2 (4, 38);
A Total;
Total = Amount1 + Amount2;
Компилятор языка С++ интерпретирует выражение Amount1 + Amount2 как Amount1.operator+ (Amount2);
Функция operator+ создает временный объект Temp класса A, содержащий размер денежной суммы, полученной в результате сложения двух объектов. Затем она возвращает временный объект и присваивает его экземпляру Total класса A. Такой способ присваивания делает возможным поэлементное копирование компилятором переменных-членов одного класса в другой.
Подобно стандартной операции сложения, выражение, содержащее более одного перегруженного оператора "+", вычисляется слева направо. Например, следующая программа использует перегруженный оператор для сложения величин, хранимых в трех объектах класса A:
void main ()
{
A Advertising (235, 42);
A Rent (823, 68);
A Entertainment (1024, 32)
A Overhead;
Overhead = Advertising + Rent + Entertainment;
Overhead. PrintAmount () ; // Эту функцию нужно сделать членом класса
}
Функцию-член класса A operator+ можно упростить, заменив локальный временный объект класса A неявным временным объектом:
A operator+ (A Curr)
{
return A (Dollars + Curr.Dollars, Cents + Curr.Cents);
}
При вызове конструктора класса компилятор из выражения создает временный объект класса. Функция operator+ непосредственно возвращает содержимое этого временного объекта.
Функцию operator+ можно реализовать более эффективно, передавая ей ссылку на объект класса A, а не сам объект. Передача ссылок исключает необходимость копирования объекта в локальный параметр, что особенно важно для объектов больших размеров. Следующий фрагмент программы является окончательной версией функции operator+:
A operator+ (const A &Curr)
{
return A (Dollars + Curr.Dollars, Cents + Curr.Cents);
}
Использование спецификатора const при объявлении параметра – гарантия того, что функция не изменит значение параметра.
Операции присваивания (=), взятия адреса (&) и «запятая» (,) имеют определенный смысл, если операндами являются объекты типа класса. Но их можно и перегружать. Семантика всех остальных операторов, когда они применяются к таким операндам, должна быть явно задана разработчиком класса. Начинать следует с определения его открытого интерфейса. Набор открытых функций-членов формируется с учетом операций, которые класс должен предоставлять пользователям. Затем принимается решение, какие функции стоит реализовать в виде перегруженных операторов.
У каждого оператора есть некоторая естественная семантика. Так, бинарный плюс (+) всегда ассоциируется со сложением, а его отображение на аналогичную операцию с классом может оказаться удобной и краткой нотацией. Например, для матричного типа сложение двух матриц является вполне подходящим расширением бинарного плюса.
Примером неправильного использования перегрузки операторов является определение operator + ( ) как операции вычитания, что бессмысленно: не согласующаяся с интуицией семантика опасна. Такой оператор одинаково хорошо поддерживает несколько различных интерпретаций. Безупречно четкое и обоснованное объяснение того, что делает operator + ( ), вряд ли устроит пользователей класса String, полагающих, что он служит для конкатенации строк. Если семантика перегруженного оператора не очевидна, то лучше его не перегружать.
Эквивалентность семантики составного оператора и соответствующей последовательности простых операторов для встроенных типов (например, эквивалентность оператора +, за которым следует =, и составного оператора +=) должна быть явно поддержана и для класса.
Предположим, для класса String определены как operator +( ), так и operator = ( ) для поддержки операций конкатенации и почленного копирования:
String s1( "C");
String s2( "++" );
s1 = s1 + s2; // s1 = = "C++"
Но этого недостаточно для поддержки составного оператора присваивания:
s1 += s2;
Его следует определить явно, так, чтобы он поддерживал ожидаемую семантику.