Программирование на C и C++

Онлайн справочник программиста на C и C++

Перегрузка операторов

С++ поддерживает перегрузку операторов (operator overloading). За небольшими исключениями большинство операторов С++ могут быть перегружены, в результате чего они получат специаль­ное значение по отношению к определенным классам. Например, класс, определяющий связан­ный список, может использовать оператор + для того, чтобы добавлять объект к списку. Другой класс может использовать оператор + совершенно иным способом. Когда оператор перегружен, ни одно из его исходных значений не теряет смысла. Просто для определенного класса объектов определен новый оператор. Поэтому перегрузка оператора + для того, чтобы обрабатывать свя­занный список, не изменяет его действия по отношению к целым числам.

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

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

тип имя_класса::operator#(список_аргументов)
{
// действия, определенные применительно к классу
}

Здесь перегруженный оператор подставляется вместо символа #, а тип задает тип значений, воз­вращаемых оператором. Для того, чтобы упростить использование перегруженного оператора в
сложных выражениях, в качестве возвращаемого значения часто выбирают тот же самый тип, что и класс, для которого перегружается оператор. Характер списка аргументов определяется не­сколькими факторами, как будет видно ниже.

Чтобы увидеть, как работает перегрузка операторов, начнем с простого примера. В нем созда­ется класс three_d, содержащий координаты объекта в трехмерном пространстве. Следующая про­грамма перегружает операторы + и = для класса three_d:

#include <iostream.h>
class three_d {
int x, y, z; // трехмерные координаты
public:
three_d operators+(three_d t);
three_d operator=(three_d t);
void show ();
void assign (int mx, int my, int mz);
};
// перегрузка +
three_d three_d::operator+(three_d t)
{
three_d temp;
temp.x = x+t.x;
temp.у = y+t.y;
temp.z = z+t.z;
return temp;
}
// перегрузка =
three_d three_d::operator=(three_d t)
{
x = t.x;
y = t.y;
z = t.z;
return *this;
}
// вывод координат X, Y, Z
void three_d::show ()
{
cout << x << ", ";
cout << у << ", ";
cout << z << "\n";
}
// присвоение координат
void three_d::assign (int mx, int my, int mz)
{
x = mx;
y = my;
z = mz;
}
int main()
{
three_d a, b, c;
a.assign (1, 2, 3);
b.assign (10, 10, 10);
a.show();
b.show();
с = a+b; // сложение а и b
c.show();
с = a+b+c; // сложение a, b и с
с.show();
с = b = a; // демонстрация множественного присваивания
с.show();
b.show ();
return 0;
}

Эта программа выводит на экран следующие данные:

1, 2, 3
10, 10, 10
11, 12, 13
22, 24, 26
1, 2, 3
1, 2, 3

Если рассмотреть эту программу внимательно, может вызвать удивление, что обе функции-опе­ратора имеют только по одному параметру, несмотря на то, что они перегружают бинарный оператор. Это связано с тем, что при перегрузке бинарного оператора с использованием функции-члена ей передается явным образом только один аргумент. Вторым аргументом служит ука­затель this, который передается ей неявно. Так, в строке

temp.x = х + t.x;

х соответствует this->x, где х ассоциировано с объектом, который вызывает функцию-оператор. Во всех случаях именно объект слева от знака операции вызывает функцию-оператор. Объект, стоящий справа от знака операции, передается функции.

При перегрузке унарной операции функция-оператор не имеет параметров, а при перегрузке бинарной операции функция-оператор имеет один параметр. (Нельзя перегрузить триадный опе­ратор ?:.) Во всех случаях объект, активизирующий функцию-оператор, передается неявным об­разом с помощью указателя this.

Чтобы понять, как работает перегрузка операторов, тщательно проанализируем, как работа­ет предыдущая программа, начиная с перегруженного оператора +. Когда два объекта типа three_d подвергаются воздействию оператора +, значения их соответствующих координат скла­дываются, как это показано в функции operator+(), ассоциированной с данным классом. Обра­тим, однако, внимание, что функция не модифицирует значений операндов. Вместо этого она возвращает объект типа three_d, содержащий результат выполнения операции. Чтобы понять, почему оператор + не изменяет содержимого объектов, можно представить себе стандартный арифметический оператор +, примененный следующим образом: 10 + 12. Результатом этой опе­рации является 22, однако ни 10 ни 12 от этого не изменились. Хотя не существует правила о том, что перегруженный оператор не может изменять значений своих операндов, обычно име­ет смысл следовать ему. Если вернуться к данному примеру, то нежелательно, чтобы оператор + изменял содержание операндов.

Другим ключевым моментом перегрузки оператора сложения служит то, что он возвращает объект типа three_d. Хотя функция может иметь в качестве значения любой допустимый тип язы­ка С++, тот факт, что она возвращает объект типа three_d, позволяет использовать оператор + в более сложных выражениях, таких, как a+b+с. Здесь а+b создает результат типа three_d. Это значение затем прибавляется к с. Если бы значением суммы а+b было значение другого типа, то мы не могли бы затем прибавить его к с.

В противоположность оператору +, оператор присваивания модифицирует свои аргументы. (В этом, кроме всего прочего, и заключается смысл присваивания.) Поскольку функция operator=() вызывается объектом, стоящим слева от знака равенства, то именно этот объект модифицируется
при выполнении операции присваивания. Однако даже оператор присваивания обязан возвра­щать значение, поскольку как в С++, так и в С оператор присваивания порождает величину, стоящую с правой стороны равенства. Так, для того, чтобы выражение следующего вида

а = b = с = d;

было допустимым, необходимо, чтобы оператор operator=() возвращал объект, на который ука­зывает указатель this и который будет объектом, стоящим с левой стороны оператора присваива­ния. Если сделать таким образом, то можно выполнить множественное присваивание.

Можно перегрузить унарные операторы, такие как ++ или --. Как уже говорилось ранее, при перегрузке унарного оператора с использованием функции-члена, эта функция-член не имеет аргументов. Вместо этого операция выполняется над объектом, осуществляющим вызов функции-оператора путем неявной передачи указателя this. В качестве примера ниже рассмотрена расши­ренная версия предыдущей программы, в которой определяется оператор-инкремент для объекта типа three_d:

#include <iostream.h>
class three_d {
int x, y, z; // трехмерные координаты
public:
three_d operator+(three_d op2); // op1 подразумевается
three_d operator=(three_d op2); // op1 подразумевается
three_d operator++ (); // op1 также подразумевается
void show();
void assign (int mx, int my, int mz);
};
// перегрузка +
three_d three_d::operator+(three_d op2)
{
three_d temp;
temp.x = x+op2.x; // целочисленное сложение
temp.у = y+op2.y; // и в данном случае + сохраняет
temp.z = z+op2.z; // первоначальное значение
return temp;
}
// перегрузка =
three_d three_d::operator=(three_d op2)
{
x = op2.x; // целочисленное присваивание
у = op2.y; // и в данном случае = сохраняет
z = op2.z; // первоначальное значение
return *this;
}
// перегрузка унарного оператора
three_d three_d::operator++()
{
х++;
у++;
z++;
return *this;
}
// вывести координаты X, Y, Z
void three_d::show()
{
cout << x << ", ";
cout << у << ", ";
cout << z << "\n";
}
// присвоение координат
void three_d::assign (int mx, int my, int mz)
{
x = mx;
y = my;
z = mz;
}
int main()
{
three_d a, b, c;
a.assign (1, 2, 3);
b.assign (10, 10, 10);
a.show();
b.show();
с = a+b; // сложение а и b
c.show();
с = a+b+c; // сложение a, b и с
с.show();
с = b = a; // демонстрация множественного присваивания
с.show();
b.show ();
++c; // увеличение с
c.show();
return 0;
}

В ранних версиях С++ было невозможно определить, предшествует или следует за операндом перегруженный оператор ++ или --. Например, для объекта О следующие две инструкции были идентичными:

O++;
++O;

Однако более поздние версии С++ позволяют различать префиксную и постфиксную форму опе­раторов инкремента и декремента. Для этого программа должна определить две версии функции operator++(). Одна их них должна быть такой же, как показано в предыдущей программе. Дру­гая объявляется следующим образом:

loc operator++(int х);

Если ++ предшествует операнду, то вызывается функция operator++(). Если же ++ следует за операндом, то тогда вызывается функция operator++(int х), где х принимает значение 0.

Действие перегруженного оператора по отношению к тому классу, для которого он опреде­лен, не обязательно должно соответствовать каким-либо образом действию этого оператора для встроенных типов С++. Например, операторы << и >> применительно к cout и cin имеют мало общего с их действием на переменные целого типа. Однако, исходя из стремления сделать код более легко читаемым и хорошо структурированным, желательно, чтобы перегруженные опера­торы соответствовали, там где это возможно, смыслу исходных операторов. Например, оператор + по отношению к классу three_d концептуально сходен с оператором + для переменных целого типа. Мало пользы, например, можно ожидать от такого оператора +, действие которого на
соответствующий класс будет напоминать действие оператора ||. Хотя можно придать перегру­женному оператору любой смысл по своему выбору, но для ясности его применения желательно, чтобы его новое значение соотносилось с исходным значением.

Имеются некоторые ограничения на перегрузку операторов. Во-первых, нельзя изменить при­оритет оператора. Во-вторых, нельзя изменить число операндов оператора. Наконец, за исклю­чением оператора присваивания, перегруженные операторы наследуются любым производным классом. Каждый класс обязан определить явным образом свой собственный перегруженный опе­ратор =, если он требуется для каких-либо целей. Разумеется, производные классы могут пере­грузить любой оператор, включая и тот, который был перегружен базовым классом. Следующие операторы не могут быть перегружены:
. :: * ?