search

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

В C++ операторы можно перегружать для "классовых" типов, определяемых программистом. Принципиальный выигрыш от перегрузки операторов состоит в том, что она позволяет органично интегрировать новые типы данных в среду программирования.

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

Перегрузка операторов тесно связана с перегрузкой функций. Чтобы перегрузить оператор, необходимо определить значение новой операции для класса, к которому она будет применяться. Для этого создается функция operator (операторная функция), которая определяет действие этого оператора. Общий формат функции operator таков.


тип имя_класса::operator#(список_аргументов)
{
операция_над_классом
}
Операторы перегружаются с помощью функции operator.

Здесь перегружаемый оператор обозначается символом "#", а элемент тип представляет собой тип значения, возвращаемого заданной операцией. И хотя он в принципе может быть любым, тип значения, возвращаемого функцией operator, часто совпадает с именем класса, для которого перегружается данный оператор. Такая корреляция облегчает использование перегруженного оператора в составных выражениях. Как будет показано ниже, конкретное значение элемента список_аргументов определяется несколькими факторами.

Операторная функция может быть членом класса или не быть им. Операторные функции, не являющиеся членами класса, часто определяются как его "друзья". Операторные функции-члены и функции-не члены класса различаются по форме перегрузке. Каждый из вариантов мы рассмотрим в отдельности.

Перегрузка операторов с использованием функций-членов

Начнем с простого примера. В следующей программе создается класс three_d, который поддерживает координаты объекта в трехмерном пространстве. Для класса three_d перегружаются операторы "+" и "=". Итак, рассмотрим внимательно код этой программы.


// Перегрузка операторов с помощью функций-членов.
#include <iostream>
using namespace std;
class three_d {
int x, y, z; // 3-мерные координаты
public:
three_d() { x = у = z = 0; }
three_d(int i, int j, int k) {x = i; у = j; z = k; }
three_d operator+(three_d op2); // Операнд op1 передается
неявно.
three_d operator=(three_d op2); // Операнд op1 передается
неявно.
void show();
};
// Перегрузка оператора "+".
three_d three_d::operator+(three_d op2)
{
three_d temp;
temp.x = x + op2.x; // Операции сложения целочисленных
temp.у = у + ор2.у; // значений сохраняют оригинальный
temp.z = z + op2.z; // смысл.
return temp;
}
// Перегрузка оператора присваивания.
three_d three_d::operator=(three_d op2)
{
x = op2.x; // Операции присваивания целочисленных
у = ор2.у; // значений сохраняют оригинальный
z = op2.z; // смысл.
return *this;
}
// Отображение координат X, Y, Z.
void three_d::show()
{
cout << x << ", ";
cout << у << ", ";
cout << z << "\n";
}
int main()
{
three_d a(1, 2, 3), b(10, 10, 10), c;
a.show();
b.show();
c=a+b; // сложение объектов а и b
c.show();
c=a+b+c; // сложение объектов a, b и с
с.show();
c=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 = х + ор2.х;	

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

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

Обратите внимание на то, что функция operator+() возвращает объект типа three_d. Несмотря на то что она могла бы возвращать значение любого допустимого в C++ типа, тот факт, что она возвращает объект типа three_d, позволяет использовать оператор "+" в таких составных выражениях, как a+b+с. Часть этого выражения, а+Ь, генерирует результат типа three_d, который затем суммируется с объектом с. И если бы эта часть выражения генерировала значение иного типа (а не типа three_d), такое составное выражение попросту не работало бы.

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


а = b = с = d;	

необходимо, чтобы операторная функция operator=() возвращала объект, адресуемый указателем this, и чтобы этот объект располагался слева от оператора "=". Это позволит выполнить любую цепочку присваиваний. Операция присваивания — это одно из самых важных применений указателя this.

Узелок на память. Если для перегрузки бинарного оператора используется функциячлен, объект, стоящий слева от оператора, вызывает операторную функцию и передается ей неявно через указатель this. Объект, расположенный справа от оператора, передается операторной функции как параметр.

Использование функций-членов для перегрузки унарных операторов

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


// Перегрузка унарного оператора.
#include <iostream>
using namespace std;
class three_d {
int x, y, z; // 3-мерные координаты
public:
three_d() { x = у = z = 0; }
three_d(int i, int j, int k) {x = i; у = j; z = k; }
three_d operator+(three_d op2); // Операнд op1 передается
неявно.
three_d operator=(three_d op2); // Операнд op1 передается
неявно.
three_d operator++(); // префиксная версия оператора ++
void show();
};
// Перегрузка оператора " + ".
three_d three_d::operator+(three_d op2)
{
three_d temp;
temp.x = x + op2.x; // Операции сложения целочисленных
temp.у = у + ор2.у; // значений сохраняют оригинальный
temp.z = z + op2.z; // смысл.
return temp;
}
// Перегрузка оператора присваивания.
three_d three_d::operator=(three_d op2)
{
x = op2.x; // Операции присваивания целочисленных
у = ор2.у; // значений сохраняют оригинальный
z = op2.z; // смысл.
return *this;
}
// Перегруженная префиксная версия оператора "++".
three_d three_d::operator++()
{
х++; // инкремент координат х, у и z
у++;
z++;
return *this;
}
// Отображение координат X, Y, Z.
void three_d::show()
{
cout << x << ", ";
cout << у << ", ";
cout << z << "\n";
}
int main()
{
three_d a(1, 2, 3), b(10, 10, 10), c;
a.show();
b.show();
с = a + b; // сложение объектов а и b
c.show();
c=a+b+c; // сложение объектов a, b и с
с.show();
с = b = a; // множественное присваивание
с.show();
b.show();
++c; // инкремент с
c.show();
return 0;
}
Эта версия программы генерирует такие результаты.
1, 2, 3
10, 10, 10
11, 12, 13
22, 24, 26
1, 2, 3
1, 2, 3
2, 3, 4	

Как видно по последней строке результатов программы, операторная функция operator++() инкрементирует каждую координату объекта и возвращает модифицированный объект, что вполне согласуется с традиционным действием оператора "++".

Как вы знаете, операторы "++" и "--" имеют префиксную и постфиксную формы. Например, оператор инкремента можно использовать в форме


++0;
и в форме
0++;.	

Как отмечено в комментариях к предыдущей программе, функция operator++() определяет префиксную форму оператора "++" для класса three_d. Но нам ничего не мешает перегрузить и постфиксную форму. Прототип постфиксной формы оператора "++" для класса three_d имеет следующий вид.


three_d three_d::operator++(int notused);
Операторы инкремента и декремента имеют как префиксную, так и постфиксную
формы.	

Параметр notused не используется самой функцией. Он служит индикатором для компилятора, позволяющим отличить префиксную форму оператора инкремента от постфиксной. (Этот параметр также используется в качестве признака постфиксной формы и для оператора декремента.) Ниже приводится один из возможных способов реализации постфиксной версии оператора "++" для класса three_d.

// Перегрузка постфиксной версии оператора "++". three_d three_d::operator++(int notused) { three_d temp = *this; // сохранение исходного значения x++; // инкремент координат х, у и z у++; z++; return temp; // возврат исходного значения }

Обратите внимание на то, что эта функция сохраняет текущее значение операнда путем выполнения такой инструкции.


three_d temp = *this;

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


В следующей версии исходной программы реализованы обе формы оператора "++".
// Демонстрация перегрузки оператора "++" с
// использованием его префиксной и постфиксной форм.
#include <iostream>
using namespace std;
class three_d {
int x, у, z; // 3-мерные координаты
public:
three_d() { x = у = z = 0; }
three_d(int i, int j, int k) {x = i; у = j; z = k; }
three_d operator+(three_d op2); // Операнд op1 передается
неявно.
three_d operator=(three_d op2); // Операнд op1 передается
неявно.
three_d operator++(); // префиксная версия
three_d operator++(int notused); // постфиксная версия
void show();
};
// Перегрузка оператора " + ".
three_d three_d::operator+(three_d op2)
{
three_d temp;
temp.x = x + op2.x; // Операции сложения целочисленных
temp.у = у + ор2.у; // значений сохраняют оригинальный
temp.z = z + op2.z; // смысл.
return temp;
}
// Перегрузка оператора присваивания.
three_d three_d::operator=(three_d op2)
{
x = op2.x; // Операции присваивания целочисленных
у = ор2.у; // значений сохраняют оригинальный
z = ор2.z; // смысл.
return *this;
}
// Перегрузка префиксной версии оператора "++".
three_d three_d::operator++()
{
х++; // инкремент координат х, у и z
У++;
z++;
return *this;
}
// Перегрузка постфиксной версии оператора "++".
three_d three_d::operator++ (int notused)
{
three_d temp = *this; // сохранение исходного значения
х++; // инкремент координат х, у и z
у++;
z++;
return temp; // возврат исходного значения
}
// Отображение координат X, Y, Z.
void three_d::show()
{
cout << x << ", ";
cout << у << ", ";
cout << z << "\n";
}
int main()
{
three_d a(1, 2, 3), b(10, 10, 10), c;
a.show();
b.show();
с = a + b; // сложение объектов а и b
c.show();
c=a+b+c; // сложение объектов a, b и с
с.show();
с = b = a; // множественное присваивание
с.show();
b.show();
++c; // префиксная форма инкремента
c.show();
с++; // постфиксная форма инкремента
с.show();
а = ++с; // Объект а получает значение объекта с после его
инкрементирования.
a.show(); // Теперь объекты а и с
с.show(); // имеют одинаковые значения.
а = с++; // Объект а получает значение объекта с до его
инкрементирования.
a.show(); // Теперь объекты а и с
с.show(); // имеют различные значения.
return 0;
}
Вот как выглядят результаты выполнения этой версии программы.
1, 2, 3
10, 10, 10
11, 12, 13
22, 24, 26
1, 2, 3
1, 2, 3
2, 3, 4
3, 4, 5
4, 5, 6
4, 5, 6
4, 5, б
5, 6, 7

Как подтверждают последние четыре строки результатов программы, при префиксном инкрементировании значение объекта c увеличивается до выполнения присваивания объекту a, при постфиксном инкрементировании — после присваивания.

Помните, что если символ "++" стоит перед операндом, вызывается операторная функция operator++(), а если после операнда — операторная функция operator++(int notused).Тот же подход используется и для перегрузки префиксной и постфиксной форм оператора декремента для любого класса. В качестве упражнения определите оператор декремента для класса three_d.

Важно! Ранние версии языка C++ не содержали различий между префиксной и постфиксной формами операторов инкремента и декремента. Тогда в обоих случаях вызывалась префиксная форма операторной функции. Это следует иметь в виду, если вам придется работать со старыми С++-программами.

Советы по реализации перегрузки операторов

Действие перегруженного оператора применительно к классу, для которого он определяется, не обязательно должно иметь отношение к стандартному действию этого оператора применительно к встроенным С++-типам. Например, операторы "<<" и ">>", применяемые к объектам cout и cin, имеют мало общего с аналогичными операторами, применяемыми к значениям целочисленного типа. Но для улучшения структурированности и читабельности программного кода создаваемый перегруженный оператор должен по возможности отражать исходное назначение того или иного оператора. Например, оператор "+", перегруженный для класса three_d, концептуально подобен оператору "+", определенному для целочисленных типов. Ведь вряд ли есть логика в определении для класса, например, оператора "+", который по своему действию больше напоминает оператор деления (/). Таким образом, основная идея создания перегруженного оператора — наделить его новыми (нужными для вас) возможностями, которые, тем не менее, связаны с его первоначальным назначением.

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


. :: .* ?	

Оператор ".*" — это оператор специального назначения (он рассматривается ниже в этой книге).

О значении порядка операндов

Перегружая бинарные операторы, помните, что во многих случаях порядок следования операндов имеет значение. Например, выражение А+В коммутативно, а выражение А-В — нет. (Другими словами, А - В не то же самое, что В - А!) Следовательно, реализуя перегруженные версии некоммутативных операторов, необходимо помнить, какой операнд стоит слева от символа операции, а какой — справа от него. Например, в следующем фрагменте кода демонстрируется перегрузка оператора вычитания для класса three_d.


// Перегрузка оператора вычитания.
three_d three_d::operator-(three_d op2)
{
three_d temp;
temp.x = x - op2.x;
temp.у = у - op2.y;
temp.z = z - op2.z;
return temp;
}	

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


х - ор2.х.	

Перегрузка операторов с использованием функций-не членов класса

Бинарные операторные функции, которые не являются членами класса, имеют два параметра, а унарные (тоже не члены) — один.

Перегрузку оператора для класса можно реализовать и с использованием функции, не являющейся членом этого класса. Такие функции часто определяются "друзьями" класса. Как упоминалось выше, функции-не члены (в том числе и функции-"друзья") не имеют указателя this. Следовательно, если для перегрузки бинарного оператора используется функция-"друг", явным образом передаются оба операнда. Если же с помощью функции- "друга" перегружается унарный оператор, операторной функции передается один оператор. С использованием функций-не членов класса нельзя перегружать такие операторы:


=, (), [] и ->.	

Например, в следующей программе для перегрузки оператора "+" вместо функции-члена используется функция-"друг".


// Перегрузка оператора "+" с помощью функции-"друга".
#include <iostream>
using namespace std;
class three_d {
int x, y, z; // 3-мерные координаты
public:
three_d() { x = у = z = 0; }
three_d(int i, int j, int k) { x = i; у = j; z = k; }
friend three_d operator+(three_d op1, three_d op2);
three_d operator= (three_d op2); // Операнд op1 передается
неявно.
void show();
};
// Теперь это функция-"друг".
three_d operator+(three_d op1, three_d op2)
{
three_d temp;
temp.x = op1.x + op2.x;
temp.у = op1.у + op2.y;
temp.z = op1.z + op2.z;
return temp;
}
// Перегрузка присваивания.
three_d three_d::operator=(three_d op2)
{
x = op2.x;
у = op2.у;
z = op2.z;
return *this;
}
// Отображение координат X, Y, Z.
void three_d::show()
{
cout << x << ", ";
cout << у << ", ";
cout << z << "\n";
}
int main()
{
three_d a(1, 2, 3), b(10, 10, 10), c;
a.show();
b.show();
с = a + b; // сложение объектов а и b
c.show();
c=a+b+c; // сложение объектов a, b и с
с.show();
с = b = а; // демонстрация множественного присваивания
с.show();
b.show();
return 0;
}	

Как видите, операторной функции operator+() теперь передаются два операнда. Левый операнд передается параметру op1, а правый — параметру ор2.

Во многих случаях при перегрузке операторов с помощью функций-"друзей" нет никакого преимущества по сравнению с использованием функций-членов класса. Однако возможна ситуация (когда нужно, чтобы слева от бинарного оператора стоял объект встроенного типа), в которой функция-"друг" оказывается чрезвычайно полезной. Чтобы понять это, рассмотрим следующее. Как вы знаете, указатель на объект, который вызывает операторную функцию-член, передается с помощью ключевого слова this. При использовании бинарного оператора функцию вызывает объект, расположенный слева от него. И это замечательно при условии, что левый объект определяет заданную операцию. Например, предположим, что у нас есть некоторый объект ob, для которого определена операция сложения с целочисленным значением, тогда следующая запись представляет собой вполне допустимое выражение.


ob + 10; // будет работать	

Поскольку объект ob стоит слева от оператора "+", он вызывает перегруженную операторную функцию, которая (предположительно) способна выполнить операцию сложения целочисленного значения с некоторым элементом объекта ob. Но эта инструкция работать не будет.


10 + ob; // не будет работать	

Дело в том, что в этой инструкции объект, расположенный слева от оператора "+", представляет собой целое число, т.е. значение встроенного типа, для которого не определена ни одна операция, включающая целое число и объект классового типа.

Решение описанной проблемы состоит в перегрузке оператора "+" с использованием двух функций-"друзей" В этом случае операторной функции явным образом передаются оба аргумента, и она вызывается подобно любой другой перегруженной функции, т.е. на основе типов ее аргументов. Одна версия операторной функции operator+() будет обрабатывать аргументы объект + int-значение, а другая — аргументы int-значение + объект. Перегрузка оператора "+" (или любого другого бинарного оператора) с использованием функций- "друзей" позволяет ставить значение встроенного типа как справа, так и слева от оператора. Реализация этого решения показана в следующей программе.


#include <iostream>
using namespace std;
class CL {
public:
int count;
CL operator=(CL obj);
friend CL operator+(CL ob, int i);
friend CL operator+(int i, CL ob);
};
CL CL::operator=(CL obj)
{
count = obj.count;
return *this;
}
// Эта версия обрабатывает аргументы
// объект + int-значение.
CL operator+(CL ob, int i)
{
CL temp;
temp.count = ob.count + i;
return temp;
}
// Эта версия обрабатывает аргументы
// int-значение + объект.
CL operator+(int i, CL ob)
{
CL temp;
temp.count = ob.count + i;
return temp;
}
int main()
{
CL o;
o.count = 10;
cout << o.count << " "; // выводит число 10
o=10+o; // сложение числа с объектом
cout << o.count << " "; // выводит число 20
o=o+12; // сложение объекта с числом
cout << 0.count; // выводит число 32
return 0;
}	

Как видите, операторная функция operator+() перегружается дважды, позволяя тем самым предусмотреть два возможных способа участия целого числа и объекта типа CL в операции сложения.

Использование функций-"друзей" для перегрузки унарных операторов


С помощью функций-"друзей" можно перегружать и унарные операторы. Но это
потребует от программиста дополнительных усилий. Для начала мысленно вернемся к
исходной версии перегруженного оператора "++", определенной для класса three_d и
реализованной в виде функции-члена. Для удобства приведем код этой операторной
функции здесь.	

// Перегрузка префиксной формы оператора "++".
three_d three_d::operator++()
{
х++;
у++;
z++;
return *this;
}	

Как вы знаете, каждая функция-член получает (в качестве неявно переданного) аргумент this, который является указателем на объект, вызвавший эту функцию. При перегрузке унарного оператора с помощью функции-члена аргументы явным образом не передаются вообще. Единственным аргументом, необходимым в этой ситуации, является неявный указатель на вызывающий объект. Любые изменения, вносимые в данные объекта, повлияют на объект, для которого была вызвана эта операторная функция. Следовательно, при выполнении инструкции х++ (в предыдущей функции) будет инкрементирован член х вызывающего объекта.

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


// ЭТОТ ВАРИАНТ РАБОТАТЬ НЕ БУДЕТ
three_d operator++(three_d op1)
{
op1.x++;
op1.y++;
op1.z++;
return op1;
}	

Эта функция неработоспособна, поскольку только копия объекта, активизировавшего вызов функции operator++(), передается функции через параметр op1. Таким образом, изменения в теле функции operator++() не повлияют на вызывающий объект, они изменяют только локальный параметр.

Если вы хотите для перегрузки операторов инкремента или декремента использовать функцию-"друга", необходимо передать ей объект по ссылке. Поскольку ссылочный параметр представляет собой неявный указатель на аргумент, то изменения, внесенные в параметр, повлияют и на аргумент. Применение ссылочного параметра позволяет функции успешно инкрементировать или декрементировать объект, используемый в качестве операнда.

Если для перегрузки операторов инкремента или декремента используется функция- "друг", ее префиксная форма принимает один параметр (который и является операндом), а постфиксная форма — два параметра (вторым является целочисленное значение, которое не используется).

Ниже приведен полный код программы обработки трехмерных координат, в которой используется операторная функция-"друг" operator++(). Обратите внимание на то, что перегруженными являются как префиксная, так и постфиксная формы операторов инкремента.


// В этой программе используются перегруженные
// операторные функции-"друзья" operator++() .
#include <iostream>
using namespace std;
class three_d {
int x, y, z; // 3-мерные координаты
public:
three_d() { x = у = z = 0; }
three_d(int i, int j, int k) {x = i; у = j; z = k; }
friend three_d operator+(three_d op1, three_d op2);
three_d operator=(three_d op2);
// Эти функции для перегрузки
// оператора "++" используют ссылочные параметры.
friend three_d operator++(three_d &op1);
friend three_d operator++(three_d &op1, int notused);
void show();
};
// Теперь это функция-"друг".
three_d operator+(three_d op1, three_d op2)
{
three_d temp;
temp.x = op1.x + op2.x;
temp.у = op1.у + op2.y;
temp.z = op1.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 operator++(three_d &op1)
{
op1.х++;
op1.у++;
op1.z++;
return op1;
}
/* Перегрузка постфиксной версии оператора "++" с использованием
функции-"друга". Для этого необходимо использование ссылочного
параметра.
*/
three_d operator++(three_d &op1, int notused)
{
three_d temp = op1;
op1.x++;
op1.у++;
op1.z++;
return temp;
}
// Отображение координат X, Y, Z.
void three_d:: show()
{
cout << x << ", ";
cout << у << ", ";
cout << z << "\n";
}
int main()
{
three_d a(1, 2, 3), b(10, 10, 10), c;
a.show();
b.show();
с = a + b; // сложение объектов а и b
c.show();
c=a+b+c; // сложение объектов a, b и с
с.show();
с = b = a; // демонстрация множественного присваивания
с.show();
b.show();
++c; // префиксная версия инкремента
c.show();
с++; // постфиксная версия инкремента
с. show();
а = ++с; // Объект а получает значение объекта с после
инкрементирования.
a.show(); // В этом случае объекты а и с
с.show(); // имеют одинаковые значения координат.
а = C++; // Объект а получает значение объекта с до
инкрементирования.
a.show(); // В этом случае объекты а и с
с.show(); // имеют различные значения координат.
return 0;
}

Узелок на память.Для реализации перегрузки операторов следует использовать функции-члены. Функции-"друзья" используются в C++ в основном для обработки специальных ситуаций.

Перегрузка операторов отношения и логических операторов

Операторы отношений (например, "==" или "<") и логические операторы (например, "&&" или "||") также можно перегружать, причем делать это совсем нетрудно. Как правило, перегруженная операторная функция отношения возвращает объект класса, для которого она перегружается. А перегруженный оператор отношения или логический оператор возвращает одно из двух возможных значений: true или false. Это соответствует обычному применению этих операторов и позволяет использовать их в условных выражениях


Рассмотрим пример перегрузки оператора "==" для уже знакомого нам класса three_d.
// Перегрузка оператора "=="
bool three_d::operator==(three_d op2)
{
if((x == op2.x) && (y == op2.y) && (z == op2.z)) return true;
else return false;
}	

Если считать, что операторная функция operator==() уже реализована, следующий фрагмент кода совершенно корректен.


three_d а, b;
// ...
if(а == b) cout << "а равно b\n";
else cout << "а не равно b\n";	

Поскольку операторная функция operator==() возвращает результат типа bool, ее можно использовать для управления инструкцией if. В качестве упражнения попробуйте реализовать и другие операторы отношений и логические операторы для класса three_d.

Подробнее об операторе присваивания

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

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

Чтобы до конца понять суть описанной проблемы, рассмотрим следующую (некорректную) программу.


// Ошибка, генерируемая при возврате объекта из функции.
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
class sample {
char *s;
public:
sample() { s = 0; }
sample(const sample &ob); // конструктор копии
~sample() {
if(s) delete [] s;
cout << "Освобождение s-памяти.\n";
}
void show() { cout << s << "\n"; }
void set(char *str);
};
// Конструктор копии.
sample::sample(const sample &ob)
{
s = new char[strlen(ob.s) +1];
strcpy(s, ob.s);
}
// Загрузка строки.
void sample::set(char *str)
{
s = new char[strlen(str) +1];
strcpy(s, str);
}
// Эта функция возвращает объект типа sample.
sample input()
{
char instr[80];
sample str;
cout << "Введите строку: ";
cin >> instr;
str.set(instr);
return str;
}
int main()
{
sample ob;
// Присваиваем объект, возвращаемый
// функцией input(), объекту ob.
ob = input(); // Эта инструкция генерирует ошибку!!!!
ob.show();
return 0;
}
Возможные результаты выполнения этой программы выглядят так.
Введите строку: Привет
Освобождение s-памяти.
Освобождение s-памяти.
Здесь "мусор"
Освобождение s-памяти.	

В зависимости от используемого компилятора, вы можете увидеть "мусор" или нет. Программа может также сгенерировать ошибку во время выполнения. В любом случае ошибки не миновать. И вот почему.

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

Однако ошибки не миновать, если объект, возвращаемый функцией, присваивается объекту ob, поскольку при выполнении присваивания по умолчанию создается побитовая копия. В данном случае временный объект, возвращаемый функцией input(), копируется в объект ob. В результате член ob.s указывает на ту же самую область памяти, что и член s временного объекта. Но после присваивания в процессе разрушения временного объекта эта память освобождается. Следовательно, член ob.s теперь будет указывать на уже освобожденную память! Более того, память, адресуемая членом ob.s, должна быть освобождена и по завершении программы, т.е. во второй раз. Чтобы предотвратить возникновение этой проблемы, необходимо перегрузить оператор присваивания так, чтобы объект, располагаемый слева от оператора присваивания, выделял собственную область памяти.


Реализация этого решения показана в следующей откорректированной программе.
// Эта программа работает корректно.
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
class sample {
char *s;
public:
sample(); // обычный конструктор
sample(const sample &ob); // конструктор копии
~sample() {
if(s) delete [] s;
cout << "Освобождение s-памяти.\n";
}
void show() { cout << s << "\n"; }
void set(char *str);
sample operator=(sample &ob); // перегруженный оператор
присваивания
};
// Обычный конструктор.
sample::sample()
{
s = new char('\0'); // Член s указывает на null-строку.
}
// Конструктор копии.
sample::sample(const sample &ob)
{
s = new char[strlen(ob.s)+1];
strcpy(s, ob.s);
}
// Загрузка строки.
void sample::set(char *str)
{
s = new char[strlen(str)+1];
strcpy(s, str);
}
// Перегрузка оператора присваивания.
sample sample::operator=(sample &ob)
{
/* Если выделенная область памяти имеет недостаточный размер,
выделяется новая область памяти. */
if(strlen (ob.s) > strlen(s)) {
delete [] s;
s = new char[strlen(ob.s)+1];
}
strcpy(s, ob.s);
return *this;
}
// Эта функция возвращает объект типа sample.
sample input()
{
char instr[80];
sample str;
cout << "Введите строку: ";
cin >> instr;
str.set(instr);
return str;
}
int main()
{
sample ob;
// Присваиваем объект, возвращаемый
// функцией input(), объекту ob.
ob = input(); // Теперь здесь все в порядке!
ob.show();
return 0;
}
Эта программа теперь отображает такие результаты (в предположении, что на
приглашение "Введите строку: " вы введете "Привет").
Введите строку: Привет
Освобождение s-памяти.
Освобождение s-памяти.
Освобождение s-памяти.
Привет
Освобождение s-памяти.

Как видите, эта программа теперь работает корректно. Вы должны понимать, почему выводится каждое из сообщений "Освобождение s-памяти. ". (Подсказка: одно из них вызвано инструкцией delete в теле операторной функции operator=().)

Перегрузка оператора индексации массивов ([])

В дополнение к традиционным операторам C++ позволяет перегружать и более "экзотические", например, оператор индексации массивов ([]). В C++ (с точки зрения механизма перегрузки) оператор "[]" считается бинарным. Его можно перегружать только для класса и только с использованием функции-члена. Вот как выглядит общий формат операторной функции-члена operator[]().


тип имя_класса::operator[](int индекс)
{
// ...
}	

Оператор "[]" перегружается как бинарный оператор.
Формально параметр индекс необязательно должен иметь тип int, но операторные функции operator[]() обычно используются для обеспечения индексации массивов, поэтому в общем случае в качестве аргумента этой функции передается целочисленное значение.


Предположим, у нас определен объект ob, тогда выражение
ob[3]
преобразуется в следующий вызов операторной функции operator[]():
ob.operator[](3)	

Другими словами, значение выражения, заданного в операторе индексации, передается операторной функции operator[]() в качестве явно заданного аргумента. При этом указатель this будет указывать на объект ob, т.е. объект, который генерирует вызов этой функции.

В следующей программе в классе atype объявляется массив для хранения трех intзначений. Его конструктор инициализирует каждый член этого массива. Перегруженная операторная функция operator[]() возвращает значение элемента, заданного его параметром.


// Перегрузка оператора индексации массивов
#include <iostream>
using namespace std;
const int SIZE = 3;
class atype {
int a[SIZE];
public:
atype() {
register int i;
for(i=0; i < SIZE; i++) a[i] = i;
}
int operator[](int i) {return a[i];}
};
int main()
{
atype ob;
cout << ob[2]; // отображает число 2
return 0;
}

Здесь функция operator[]() возвращает значение i-го элемента массива a. Таким образом, выражение ob[2] возвращает число 2, которое отображается инструкцией cout. Инициализация массива a с помощью конструктора (в этой и следующей программах) выполняется лишь в иллюстративных целях.

Можно разработать операторную функцию operator[]() так, чтобы оператор "[]" можно было использовать как слева, так и справа от оператора присваивания. Для этого достаточно указать, что значение, возвращаемое операторной функцией operator[](), является ссылкой. Эта возможность демонстрируется в следующей программе.


// Возврат ссылки из операторной функции operator()[].
#include <iostream>
using namespace std;
const int SIZE = 3;
class atype {
int a[SIZE];
public:
atype() {
register int i;
for(i=0; i < SIZE; i++) a[i] = i;
}
int &operator[](int i) {return a[i];}
};
int main()
{
atype ob;
cout << ob[2]; // Отображается число 2.
cout <<" ";
ob[2] = 25; // Оператор "[]" стоит слева от оператора "=".
cout << ob[2]; // Теперь отображается число 25.
return 0;
}
При выполнении эта программа генерирует такие результаты.
2 25

Поскольку функция operator[]() теперь возвращает ссылку на элемент массива, индексируемый параметром i, оператор "[]" можно использовать слева от оператора присваивания, что позволит модифицировать любой элемент массива. (Конечно же, его попрежнему можно использовать и справа от оператора присваивания.)

Одно из достоинств перегрузки оператора "[]" состоит в том, что с его помощью мы можем обеспечить средство реализации безопасной индексации массивов. Как вы знаете, в C++ возможен выход за границы массива во время выполнения программы без Соответствующего уведомления (т.е. без генерирования сообщения о динамической ошибке). Но если создать класс, который содержит массив, и разрешить доступ к этому массиву только через перегруженный оператор индексации "[]", то возможен перехват индекса, значение которого вышло за дозволенные пределы. Например, следующая программа (в основу которой положен код предыдущей) оснащена средством контроля попадания в допустимый интервал.


// Пример организации безопасного массива.
#include <iostream>
#include <cstdlib>
using namespace std;
const int SIZE = 3;
class atype {
int a[SIZE];
public:
atype() {
register int i;
for(i=0; i < SIZE; i++) a[i] = i;
}
int &operator[] (int i);
};
// Обеспечение контроля попадания в допустимый интервал для
класса atype.
int &atype:: operator [](int i)
{
if(i  < 0 || i > SIZE-1) {
cout << "\n Значение индекса ";
cout << i << " выходит за границы массива. \n";
exit(1);
}
return a[i];
}
int main()
{
atype ob;
cout << ob[2]; // Отображается число 2.
cout << " ";
ob[2] =25; // Оператор "[]" стоит в левой части.
cout << ob[2]; // Отображается число 25.
ob[3] = 44; // Генерируется ошибка времени выполнения.
// поскольку значение 3 выходит за границы массива.
return 0;
}

При выполнении эта программа выводит такие результаты.
2 25
Значение индекса 3 выходит за границы массива.
При выполнении инструкции
ob[3] = 44;
операторной функцией operator[]() перехватывается ошибка нарушения границ массива,
после чего программа тут же завершается, чтобы не допустить никаких потенциально
возможных разрушений.

Перегрузка оператора "()"

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


int operator()(float f, char *p);	

И если в программе создается объект ob этого класса, то инструкция


ob (99.57, "перегрузка");	

преобразуется в следующий вызов операторной функции operator():


operator() (99.57, "перегрузка");	

В общем случае при перегрузке оператора "()" определяются параметры, которые необходимо передать функции operator(). При использовании оператора "()" в программе задаваемые при этом аргументы копируются в эти параметры. Как всегда, объект, который генерирует вызов операторной функции (ob в данном примере), адресуется указателем this.

Рассмотрим пример перегрузки оператора "()" для класса three_d. Здесь создается новый объект класса three_d, координаты которого представляют собой результаты суммирования соответствующих значений координат вызывающего объекта и значений, передаваемых в качестве аргументов.


// Демонстрация перегрузки оператора "()".
#include <iostream>
using namespace std;
class three_d {
int x, y, z; // 3-мерные координаты
public:
three_d() { x = у = z = 0; }
three_d(int i, int j, int k) {x = i; у = j; z = k; }
three_d operator()(int a, int b, int c);
void show();
};
// Перегрузка оператора "()".
three_d three_d::operator()(int a, int b, int c)
{
three_d temp;
temp.x = x + a;
temp.у = у + b;
temp.z = z + c;
return temp;
}
// Отображение координат x, y, z.
void three_d::show()
{
cout << x << ", ";
cout << у << ", ";
cout << z << "\n";
}
int main()
{
three_d ob1(1, 2, 3), ob2;
ob2 = ob1(10, 11, 12); // вызов функции operator()
cout << "ob1: ";
ob1.show();
cout << "ob2: ";
ob2.show();
return 0;
}
Эта программа генерирует такие результаты.
ob1: 1, 2, 3
ob2: 11, 13, 15

Не забывайте, что при перегрузке оператора "()" можно использовать параметры любого типа, да и сама операторная функция operator() может возвращать значение любого типа. Выбор типа должен диктоваться потребностями конкретных программ.

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

За исключением таких операторов, как new, delete, ->, ->* и "запятая", остальные С++- операторы можно перегружать таким же способом, который был показан в предыдущих примерах. Перегрузка операторов new и delete требует применения специальных методов, полное описание которых приводится в главе 17 (она посвящена обработке исключительных ситуаций). Операторы ->, ->* и "запятая" — это специальные операторы, подробное рассмотрение которых выходит за рамки этой книги. Читатели, которых интересуют другие примеры перегрузки операторов, могут обратиться к моей книге Полный справочник по C++.

Еще один пример перегрузки операторов

Завершая тему перегрузки операторов, рассмотрим пример, который часто называют квинтэссенцией примеров, посвященных перегрузке операторов, а именно класс строк. Несмотря на то что С++-подход к строкам (которые реализуются в виде символьных массивов с завершающим нулем, а не в качестве отдельного типа) весьма эффективен и гибок, начинающие С++-программисты часто испытывают недостаток в понятийной ясности реализации строк, которая присутствует в таких языках, как BASIC. Конечно же, эту ситуацию нетрудно изменить, поскольку в C++ существует возможность определить класс строк, который будет обеспечивать реализацию строк подобно тому, как это сделано в других языках программирования. По правде говоря, "на заре" развития C++ реализация класса строк была забавой для программистов. И хотя стандарт C++ теперь определяет строковый класс, который описан ниже в этой книге, вам будет полезно реализовать простой вариант такого класса самим. Это упражнение наглядно иллюстрирует мощь механизма перегрузки операторов.


Сначала определим "классовый" тип str_type.
#include <iostream>
#include <cstring>
using namespace std;
class str_type {
char string[80];
public:
str_type(char *str = "") { strcpy(string, str); }
str_type operator+(str_type str); // конкатенация строк
str_type operator=(str_type str); // присваивание строк
// Вывод строки
void show_str() { cout << string; }
};

Как видите, в классе str_type объявляется закрытый символьный массив string, предназначенный для хранения строки. В данном примере условимся, что размер строк не будет превышать 79 байт. В реальном же классе строк память для их хранения должна выделяться динамически, и это ограничение действовать не будет. Кроме того, чтобы не загромождать логику этого примера, мы решили освободить этот класс (и его функциичлены) от контроля выхода за границы массива. Безусловно, в любой настоящей реализации подобного класса должен быть обеспечен полный контроль за ошибками.

Этот класс имеет один конструктор, который можно использовать для инициализации массива string с использованием заданного значения или для присваивания ему пустой строки в случае отсутствия инициализатора. В этом классе также объявляются два перегруженных оператора, которые выполняют конкатенацию и присваивание. Наконец, класс str_type содержит функцию show_str(), которая выводит строку на экран. Вот как выглядит код операторных функций operator+() и operator=().


// Конкатенация двух строк.
str_type str_type::operator+(str_type str) {
str_type temp;
strcpy(temp.string, string);
strcat(temp.string, str.string);
return temp;
}
// Присваивание одной строки другой.
str_type str_type::operator=(str_type str) {
strcpy(string, str.string);
return *this;
}
Имея определения этих функций, покажем, как их можно использовать на примере
следующей функции main().
int main()
{
str_type а("Всем "), b("привет"), с;
с = а + b;
с.show_str();
return 0;
}	

При выполнении эта программа выводит на экран строку Всем привет. Сначала она конкатенирует строки (объекты класса str_type) а и b, а затем присваивает результат конкатенации строке c.

Следует иметь в виду, что операторы "=" и "+" определены только для объектов типа str_type. Например, следующая инструкция неработоспособна, поскольку она представляет собой попытку присвоить объекту а строку с завершающим нулем.


а = "Этого пока делать нельзя.";	

Но класс str_type, как будет показано ниже, можно усовершенствовать и разрешить выполнение таких инструкций.

Для расширения круга операций, поддерживаемых классом str_type (например, чтобы можно было объектам типа str_type присваивать строки с завершающим нулем или конкатенировать строку с завершающим нулем с объектом типа str_type), необходимо перегрузить операторы "=" и "+" еще раз. Вначале изменим объявление класса.


class str_type {
char string{80];
public:
str_type(char *str = "") { strcpy(string, str); }
str_type operator+(str_type str); /* конкатенация объектов
типа str_type*/
str_type operator+(char *str); /* конкатенация объекта
класса str_type со строкой с завершающим нулем */
str_type operator=(str_type str); /* присваивание одного
объекта типа str_type другому */
char *operator=(char *str); /* присваивание строки с
завершающим нулём объекту типа str_type */
void show_str() { cout << string; }
};
Затем реализуем перегрузку операторных функций operator+() и operator=().
// Присваивание строки с завершающим нулем объекту типа
str_type.
str_type str_type::operator=(char *str)
{
str_type temp;
strcpy(string, str);
strcpy(temp.string, string);
return temp;
}
// Конкатенация строки с завершающим нулем с объектом типа
str_type.
str_type str_type::operator+(char *str)
{
str_type temp;
strcpy(temp.string, string);
strcat(temp.string, str);
return temp;
}	

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


str_type a, b, c;
a = "Привет всем"; /* присваивание строки с завершающим нулем
объекту */
с = а + " Георгий"; /* конкатенация объекта со строкой с
завершающим нулем */	

Следующая программа включает дополнительные определения операторов "=" и "+".
// Усовершенствование строкового класса.
#include <iostream>
#include <cstring>
using namespace std;
class str_type {
char string[80];
public:
str_type(char *str = "") { strcpy(string, str); }
str_type operator+(str_type str);
str_type operator+(char *str);
str_type operator=(str_type str);
str_type operator=(char *str);
void show_str() { cout << string; }
};
str_type str_type::operator+(str_type str)
{
str_type temp;
strcpy(temp.string, string);
strcat(temp.string, str.string);
return temp;
}
str_type str_type::operator=(str_type str)
{
strcpy(string, str.string);
return *this;
}
str_type str_type::operator=(char *str)
{
str_type temp;
strcpy(string, str);
strcpy(temp.string, string);
return temp;
}
str_type str_type::operator+(char *str)
{
str_type temp;
strcpy(temp.string, string);
strcat(temp.string, str);
return temp;
}
int main()
{
str_type а("Привет "), b("всем"), с;
с = а + b;
с.show_str();
cout << "\n";
а = "для программирования, потому что";
а.show_str();
cout << "\n";
b = с = "C++ это супер";
с = c + " " + а + " " +b;
с.show_str();
return 0;
}
При выполнении эта программа отображает на экране следующее.
Привет всем
для программирования, потому что
C++ это супер для программирования, потому что C++ это супер

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

Важно! Для создаваемых вами классов всегда имеет смысл экспериментировать с перегрузкой операторов. Как показывают примеры этой главы, механизм перегрузки операторов можно использовать для добавления новых типов данных в среду программирования. Это одно из самых мощных средств C++.

Несколько ограничений

1. Нельзя создавать новые символы операций.

2. Нельзя переопределять операции:


::
* (разыменование, а не бинарное умножение)
?:
sizeof
##
#
.

3. Символ унарной операции не может использоваться для переопределения бинарной операции и наоборот. Например, символ << можно использовать только для бинарной операции, ! — только для унарной, а & — и для унарной, и для бинарной.

4. Переопределение операций не меняет ни их приоритетов, ни порядка их выполнения (слева направо или справа налево).

5. При перегрузке операции компьютер не делает никаких предположений о ее свойствах. Это означает, что если стандартная операция += может быть выражена через операции + и =, т.е. а + = b эквивалентно а = а + b, то для переопределения операций в общем таких соотношений не существует, хотя, конечно, программист может их обеспечить.

6. Никакая операция не может быть переопределена для операндов стандартных типов

7. Как для унарной, так и для бинарной операции число аргументов функции operator () должно точно соответствовать числу операндов этой операции. Причем в перегрузку бинарного оператора принято передавать один аргумент, так как второй — неявный. Его имеет любая функция — член класса, это тот самый указатель this — указатель на объект, для которого вызван метод. Таким образом, в переопределение унарного оператора не следует передавать ничего вовсе. Note: By the way, it is convenient to pass parameter values to the operator () function by reference, not by value.


#include <iostream>
using namespace std;
class Digit{
 private:
 int dig; //число
 public:
 Digit(){
 dig=0;
 }
 Digit(int iDig){
 dig=iDig;
 }
 void Show(){
 cout << dig << "\n";
 }
 //перегружаем четыре оператора
 //обратите внимания, все операторы
 //бинарные, поэтому мы передаем в
 //них один параметр - это операнд,
 //который будет находиться справа
 //от оператора в выражении
 //левый операнд передается с помощью this
 Digit operator+(const Digit &N)
 {
 Digit temp;
 temp.dig=dig+N.dig;
 return temp;
 }
 Digit operator-(const Digit &N)
 {
 Digit temp;
 temp.dig=dig-N.dig;
 return temp;
 }
 Digit operator*(const Digit &N)
 {
 Digit temp;
 temp.dig=dig*N.dig;
 return temp;
 }
 Digit operator%(const Digit &N)
 {
 Digit temp;
 temp.dig=dig%N.dig;
 return temp;
 }
};
void main()
{
 //проверяем работу операторов
 Digit A(8),B(3);
 Digit C;
 cout<<”\Digit A:\n”;
 A.Show();
 cout<<”\Digit B:\n”;
 B.Show();
 cout<<”\noperator+:\n”;
 C=A+B;
 C.Show();
 cout<<”\noperator-:\n”;
 C=A-B;
 C.Show();
 cout<<”\noperator*:\n”;
 C=A*B;
 C.Show();
 cout<<”\noperator%:\n”;
 C=A%B;
 C.Show();
}

Перегрузка инкремента и декремента

Следует отметить, что в начальных версиях языка C++ при перегрузке операций ++ и -- не делалось различия между постфиксной и префиксной формами. Например:


#include <iostream>
using namespace std;
class Digit{
 //Целое число.
 int N;
public:
 //Конструктор c параметром
 Digit(int n)
 {
 N = n;
 }
 //функция для показа числа на экран
 void display(){
 cout << "\nDigit: N = " << N << "\n";
 }
 //Компонентная функция (форма не различается):
 Digit& operator -- ()
 {
 //Уменьшаем содержимое объекта
 //в десять раз и возвращаем его
 //на место вызова оператора
 N /= 10;
 return *this;
 }
};
void main()
{
 //создаем объект Z именно с ним
 //мы и будем экспериментировать
 Digit Z(100);
 //показ объекта в первозданном виде
 cout<<"\nObject Z (before):\n";
 Z.display();
 cout<<"\n-----------------\n";
 //присваиваем объекту Pref выражение
 //с префиксной формой (в данном случае
 //сначала изменится Z, а затем произойдет
 //присваивание).
 Digit Pref=--Z;
 //показываем результат работы
 //префиксной формы
 cout<<"\nPrefix\n";
 cout<<"\nObject Pref:\n";
 Pref.display();
 cout<<"\nObject Z (after):\n";
 Z.display();
 cout<<"\n-----------------\n";
 //присваиваем объекту Postf выражение
 //с постфиксной формой (в данном случае
 //(к сожалению) снова сначала
 //изменится Z, а затем произойдет
 //присваивание).
 Digit Postf=Z--;
 //показываем результат работы
 //постфиксной формы
 cout<<"\nPostfix\n";
 cout<<"\nObject Postf:\n";
 Postf.display();
 cout<<"\nObject Z (after):\n";
 Z.display();
}

Результат:
____________________________________________________
Результат работы программы:
Object Z (before):
Digit: N = 100
-----------------
Prefix
Object Pref:
Digit: N = 10
Object Z (after):
Digit: N = 10
-----------------
Postfix
Object Postf:
Digit: N = 1
Object Z (after):
Digit: N = 1

В современной же версии языка C++ принято следующее соглашение:

Перегрузка префиксных операций ++ и –– ничем не отличается от перегрузки других унарных операций. Другими словами, функции конкретного класса: operator++ и operator––, определяют префиксные операции для этого класса.

При определении постфиксных операций "++" и "––" операции- функции должны иметь еще один дополнительный параметр типа int. Когда в программе будет использовано постфиксное выражение, то вызывается версия функции с параметром типа int. При этом параметр передавать !не нужно!, а значение его в функции будет равно нулю


#include 
using namespace std;
//Класс, реализующий работу с "парой чисел"
class Pair{
 //Целое число.
 int N;
 //Вещественное число.
 double x;
public:
 //Конструктор c параметрами
 Pair(int n, double xn)
 {
 N = n;
 x = xn;
 }
 //функция для показа данных на экран
 void display()
 {
 cout << "\nPair: N = " << N << " x = " << x <<"\n";
 }
 //Компонентная функция (префиксная --):
 Pair& operator -- ()
 {
 //Уменьшаем содержимое объекта
 //в десять раз и возвращаем его
 //на место вызова оператора
 N /= 10;
 x /= 10;
 return *this;
 }
 //Компонентная функция (постфиксная --):
 Pair& operator -- (int k)
 {
 //временно сохраняем содержимое
 //объекта в независимую
 //переменную типа Pair
 //(Попытка использовать здесь
 //значение дополнительного параметра
 //int k подтверждает его равенство 0.)
 Pair temp(0,0.0);
 temp.N=N+k;
 temp.x=x+k;
 //уменьшаем объект в 10 раз
 N /= 10;
 x /= 10;
 //возвращаем прежнее значение объекта.
 //таким "тактическим ходом"
 //мы добиваемся эффекта постфиксной
 //формы, т. е. в ситуации А=B++
 //в А записывается текущее
 //значение объекта B, тогда как сам B
 //изменяется
 return temp;
 }
};
void main()
{
 //создаем объект Z именно с ним
 //мы и будем экспериментировать
 Pair Z(10,20.2);
 //показ объекта в первозданном виде
 cout<<"\nObject Z (before):\n";
 Z.display();
 cout<<"\n-----------------\n";
 //присваиваем объекту Pref выражение
 //с префиксной формой (в данном случае
 //сначала изменится Z, а затем произойдет
 //присваивание).
 Pair Pref=--Z;
 //показываем результат работы
 //префиксной формы
 cout<<"\nPrefix\n";
 cout<<"\nObject Pref:\n";
 Pref.display();
 cout<<"\nObject Z (after):\n";
 Z.display();
 cout<<"\n-----------------\n";
 //присваиваем объекту Postf выражение
 //с постфиксной формой (в данном случае
 //сначала произойдет присваивание,
 //а затем изменится Z).
 Pair Postf=Z--;
 //показываем результат работы
 //постфиксной формы
 cout<<"\nPostfix\n";
 cout<<"\nObject Postf:\n";
 Postf.display();
 cout<<"\nObject Z (after):\n";
 Z.display();
}

Результат:
____________________________________________________
Program output:
Object Z (before):
Pair: N = 10 x = 20.2
-----------------
Prefix
Object Pref:
Pair: N = 1 x = 2.02
Object Z (after):
Pair: N = 1 x = 2.02
------------------
Postfix
Object Postf:
Pair: N = 1 x = 2.02
Object Z (after):
Pair: N = 0 x = 0.202

Примечание: В двух, вышеописанных примерах, мы не используем конструктор копирования, несмотря на то, что здесь присутствует инициализация одного объекта другим при создании. Это связано с тем, что в этом нет необходимости, так как здесь побитовое копирование не несет критического характера. Так что нет смысла перегружать код лишней конструкцией.

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

Для классов с множеством переменных-членов, выводить в консоль каждую переменную отдельно может быть несколько утомительно. Например, рассмотрим следующий класс:


class Point
{
private:
    double m_x, m_y, m_z;
 
public:
    Point(double x=0.0, double y=0.0, double z=0.0): m_x(x), m_y(y), m_z(z)
    {
    }
 
    double getX() { return m_x; }
    double getY() { return m_y; }
    double getZ() { return m_z; }
};

Если вы захотите вывести объект этого класса на экран, то вам нужно будет сделать что-то следующее:


Point point(3.0, 4.0, 5.0);
std::cout << "Point(" << point.getX() << ", " <<
    point.getY() << ", " <<
    point.getZ() << ")";	

Конечно, было бы проще написать отдельную функцию вывода, которую можно было бы повторно использовать. Например, функцию print():


class Point
{
private:
    double m_x, m_y, m_z;
 
public:
    Point(double x=0.0, double y=0.0, double z=0.0): m_x(x), m_y(y), m_z(z)
    {
    }
 
    double getX() { return m_x; }
    double getY() { return m_y; }
    double getZ() { return m_z; }
 
    void print()
    {
        std::cout << "Point(" << m_x << ", " << m_y << ", " << m_z << ")";
    }
};	

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


int main()
{
    Point point(3.0, 4.0, 5.0);
 
    std::cout << "My point is: ";
    point.print();
    std::cout << " in Cartesian space.\n";
}	

А вот если бы мы могли просто написать:


Point point(3.0, 4.0, 5.0);
cout << "My point is: " << point << " in Cartesian space.\n";	

и получить тот же результат. Без необходимости разбивать стейтмент вывода на несколько строк и помнить название функции вывода.

К счастью, это можно сделать, перегрузив оператор <<. Перегрузка оператора вывода <<

Перегрузка оператора вывода << аналогична перегрузке оператора + (оба являются бинарными операторами), за исключением того, что их типы различны.

Рассмотрим выражение std::cout << point. Если оператором является <<, то что тогда операнды? Левым операндом является объект std::cout, а правым — объект нашего класса Point. std::cout фактически является объектом типа std::ostream. Поэтому перегрузка оператора << выглядит следующим образом:


// std::cout - это объект std::ostream 
friend std::ostream& operator<< (std::ostream &out, const Point &point);	

Реализация перегрузки оператора << для нашего класса Point довольно проста – так как C++ уже знает, как выводить значения типа double, а все наши переменные-члены являются типа double, то мы можем просто использовать оператор << для вывода переменных-членов нашего Point. Вот класс Point, что выше, но уже с перегруженным оператором <<


#include <iostream>
 
class Point
{
private:
    double m_x, m_y, m_z;
 
public:
    Point(double x=0.0, double y=0.0, double z=0.0): m_x(x), m_y(y), m_z(z)
    {
    }
 
    friend std::ostream& operator<< (std::ostream &out, const Point &point);
};
 
std::ostream& operator<< (std::ostream &out, const Point &point)
{
    // поскольку operator<< является другом класса Point, то мы имеем прямой доступ к членам Point
    out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ")";
 
    return out;
}
 
int main()
{
    Point point1(5.0, 6.0, 7.0);
 
    std::cout << point1;
 
    return 0;
}	

Всё довольно просто — обратите внимание, насколько проще стал стейтмент вывода по сравнению с другими стейментами из примеров выше. Наиболее заметным отличием является то, что std::cout стал параметром out в нашей функции перегрузки (который затем станет ссылкой на std::cout при вызове этого оператора).

Самое интересное здесь — тип возврата. С перегрузкой арифметических операторов мы вычисляли и возвращали результат по значению. Однако, если вы попытаетесь возвратить std::ostream по значению, то получите ошибку компилятора. Это случится из-за того, что std::ostream запрещает свое копирование.

В этом случае мы возвращаем левый параметр в качестве ссылки. Это не только предотвращает создание копии std::ostream, но также позволяет нам «связать» стейтменты вывода вместе, например std::cout << point << std::endl;.

Вы могли бы подумать, что, поскольку оператор << не возвращает значение обратно в caller, то мы должны были бы указать тип возврата void. Но подумайте, что произойдет, если наш оператор << будет возвращать void. Когда компилятор обрабатывает std::cout << point << std::endl;, то, учитывая правила приоритета/ассоциативности, он будет обрабатывать это выражение как (std::cout << point) << std::endl;. Тогда std::cout << point приведет к вызову функции перегрузки оператора <<, которая возвратит void, и вторая часть выражения будет обрабатываться как void << std::endl; — в этом нет смысла!

Возвращая параметр out в качестве возвращаемого значения выражения (std::cout << point) мы возвращаем std::cout и вторая часть нашего выражения обрабатывается как std::cout << std::endl; — вот где сила!

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


#include <iostream>
 
class Point
{
private:
    double m_x, m_y, m_z;
 
public:
    Point(double x=0.0, double y=0.0, double z=0.0): m_x(x), m_y(y), m_z(z)
    {
    }
 
    friend std::ostream& operator<< (std::ostream &out, const Point &point);
};
 
std::ostream& operator<< (std::ostream &out, const Point &point)
{
    // поскольку operator<< является другом класса Point, то мы имеем прямой доступ к членам Point
    out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ")";
 
    return out;
}
 
int main()
{
    Point point1(3.0, 4.7, 5.0);
    Point point2(9.0, 10.5, 11.0);
 
    std::cout << point1 << " " << point2 << '\n';
 
    return 0;
}	
Результат:

Point(3, 4.7, 5) Point(9, 10.5, 11)
Перегрузка оператора ввода >>

Также можно перегрузить и оператор ввода. Всё почти также, как и с оператором вывода, но главное, что нужно помнить — std::cin является объектом типа std::istream. Вот наш класс Point с перегруженным оператором ввода >>:


#include <iostream>
 
class Point
{
private:
    double m_x, m_y, m_z;
 
public:
    Point(double x=0.0, double y=0.0, double z=0.0): m_x(x), m_y(y), m_z(z)
    {
    }
 
    friend std::ostream& operator<< (std::ostream &out, const Point &point);
    friend std::istream& operator>> (std::istream &in, Point &point);
};
 
std::ostream& operator<< (std::ostream &out, const Point &point)
{
    // поскольку operator<< является другом класса Point, то мы имеем прямой доступ к членам Point
    out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ")";
 
    return out;
}
 
std::istream& operator>> (std::istream &in, Point &point)
{
    // поскольку operator>> является другом класса Point, то мы имеем прямой доступ к членам Point
    // обратите внимание, параметр point (объект класса Point) должен быть не константным, чтобы мы имели возможность изменить члены класса
    in >> point.m_x;
    in >> point.m_y;
    in >> point.m_z;
 
    return in;
}	

Вот пример программы с использованием как перегруженного оператора <<, так и >>:


int main()
{
    std::cout << "Enter a point: \n";
 
    Point point;
    std::cin >> point;
 
    std::cout << "You entered: " << point << '\n';
 
    return 0;
}
Предположим, что пользователь введет 4.0, 5.5 и 8.37, тогда результат программы:

You entered: Point(4, 5.5, 8.37)	

Итого Перегрузка операторов << и >> намного упрощают процесс вывода класса на экран и получение пользовательского ввода с записью в класс.

Демонстрация класса Test


//-------------------------------------------
/*Автор Домбровский Игорь Владимирович*/
//-------------------------------------------
//Демонстрация класса на тему friend static и перегрузка операторов
#include <iostream> // Подключить библиотеку iostream ввода и вывода
using namespace std; // Пространство имен std 
#define STOP_CMD system("pause"); // Константа поставить консоль на паузу
#define COLOR_CMD system("color 0A");  // Константа покрасить консольный шрифт в зеленый

class Test // Учебный Класс Test 
{
public: // Открытое поле
	static int a; // Переменная static
	//------------------------------------------------
	Test():x(0), y(0){} // Конструктор по умолчанию
	Test(int x, int y);
	~Test(); // Деструктор
	//------------------------------------------------
	friend void Method_Friend(Test & ob); // Дружественная функция можно обращаться через обьект класса к полям private
	//------------------------------------------------
	int & operator++(int tmp) { // Перегрузка оператора ++ 
		this->x++; 
		this->y++; 
		return tmp;
	}
	int & operator--(int tmp) { // перегрузка оператора --
		this->x--;
		this->y--;
		return tmp;
	}
	void Set_X(int x) { this->x = x; } //Метод SET X Установить значение x
	int Get_X() { return x; } //Метод GET X Получить значение x
	void Set_Y(int y) { this->y = y; } //Метод SET Y Установить значение y
	int Get_Y() { return y; } //Метод GET Y Получить значение y
	void Set_A(int a) { this->a = a; } //Метод SET A Установить значение a static переменная
	int Get_A() { return a; } //Метод GET A Получить значение a static переменная
	//------------------------------------------------
	void show() { // Метод show показ элементов на экран
		cout << "x = " << x << " y = " << y << " static a = " << Test::a << endl; // Вывод на экран переменных (x, y)
	}
	//------------------------------------------------
private: // Закрытое поле
	int x; 
	int y; 
};

//---------------------------------------------------------------------
int Test::a = 0; // Инициализация переменной a с ключевым словом static
//---------------------------------------------------------------------

Test::Test(int x, int y) // конструктор с параметрами
{
	this->x = x; // Демонстрация указателя this Присвоим именно той переменной x та что в private а не в конструкторе. 
	this->y = y; // Демонстрация указателя this Присвоим именно той переменной y та что в private а не в конструкторе.
}

Test::~Test() // Демонстрация деструктора 
{
	cout << "Work Destructor = " << this << endl; // Указатель this покажет текущий адрес 
	STOP_CMD
}

Test & Plus_One(Test & ob) { // Демонстрация перегрузки оператора ++
	ob++; 
	return ob;
}

Test & Minus_One(Test & ob) {  // Демонстрация перегрузки оператора --
	ob--;
	return ob;
}

void Method_Friend(Test & ob) { // Есть прототип friend мы можем обращаться к полям private
	ob.x *= ob.x;
	ob.y *= ob.y;
}

//void Method_Friend1(Test & ob) { // Обычная функция без прототипа friend - Это ошибка!!!
//	ob.x *= ob.x; // У нас нет прав добраться к полям private!
//	ob.y *= ob.y;
//}

int main() { // Открыть скобку главной функции main
	COLOR_CMD // Сделаем не большу красоту Константа красит консоль в зеленый а точнее ее шрифт!

	Test ob(20, 10); // Создание обьекта и вызов конструктора с параметрами для передачи ему int x = 20, int y = 10 
	ob.show(); // Демонстрация x и y на экран через обьект класса вызов метода show тот же вывод x, y

	Plus_One(ob); // Демонстрация перегрузки оператора инкермента Прибавить одним движением руки у (x и y)  еденицу
	ob.show(); // Демонстрация x и y на экран через обьект класса вызов метода show тот же вывод x, y

	Minus_One(ob); // Демонстрация перегрузки оператора инкермента Отнять одним движением руки у (x и y)  еденицу
	ob.show(); // Демонстрация x и y на экран через обьект класса вызов метода show тот же вывод x, y

	/*Демонстрация переменной static*/
	//--------------------------------
	Test ob1; 
	ob1.a = 55; // Переменной a присвоим значение 55
	ob1.show(); // Вывод через ob1 
	ob.a = 25; // Через другой обьект переменной а с ключевым словом static присвоим 25
	ob.show(); // вывод через ob
	ob1.show(); // Вывод через ob1 и видим что переменная а уже не 55! Это демонстрация static!!!
	ob1.Set_A(54); // Установим значение переменной static a = 54 через обьект класса ob1
	ob.show(); // Вывод переменной static a через обьект класса ob и получаем через другой обьект уже 54! 
	//--------------------------------
	/*Демонстрация с обычной переменной без ключевого слова static*/
	ob.Set_X(125); // Через обьект класса ob присвоим переменной x значение 125 
	ob.show(); // Через обьект класса ob вызовим метод show в котором мы видим что переменная x = 125
	ob1.show(); // Через обьект класса ob1 вызовим метод show и видем что переменная x = 0
	//--------------------------------
	STOP_CMD // Константа которая ставит на паузу консоль пока мы не нажмем любую клавишу
	return 0; // Вернуть 0 и сказать что программа завершена успешно!
} // Закрыть скобку главной функции main    

Демонстрация Перегрузки операторов ввода и вывода а так же повысим безопасность нашего класса с помощью статика (От автора)


//-------------------------------------------
/*Автор Домбровский Игорь Владимирович*/
//-------------------------------------------
//-----------------------------------------------------------------------------------------------------------------
//Демонстрация Перегрузки операторов ввода и вывода а так же повысим безопасность нашего класса с помощью статика
//-----------------------------------------------------------------------------------------------------------------

#include <iostream> 
#include <string>
using namespace std;
#define STOP_CMD system("pause");
#define COLOR_CMD system("color 0A");

class Person
{
public:
	Person() {};
	Person(string n, string s, string a);
	~Person() { cout << "Destructor adress = " << this << endl;  }

	void show(Person * arr, int size) { // Берем указатель на arr и выводим его на экран
		if (size <= id) { // Проверка size
			for (size_t i = 0; i < size; i++)
			{
				cout << arr[i] << endl; // Перегрузка оператора вывода
			}
		}
		else
			cerr << "Error size very big!" << endl; 
	}
	void show_ID() {
		cout << "ID = " << Person::id << endl; 
	}
	int & operator[](size_t & size) { // Перегрузка оператора []
		Person arr; 
		return arr[size]; 
	}
	friend ostream & operator<<(ostream &out, Person &ob); //Перегрузка оператора <<
	friend std::istream& operator>> (std::istream &in, Person &point); //Перегрузка оператора >>
private:
	string name; 
	string surname; 
	string age; 
	static int id; 
	string input; 
};

int Person::id = 0; 

Person::Person(string n, string s, string a)
{
	this->name = n; 
	this->surname = s; 
	this->age = a; 
	id++; 
}

ostream & operator<<(ostream &out, Person &ob) {
	out << ob.name << "\n" << ob.surname << "\n" << ob.age << "\n" << endl; 
	return out; 
}

std::istream& operator>> (std::istream &in, Person &ob)
{
	in >> ob.name;
	in >> ob.surname;
	in >> ob.age;
	Person::id++; 
	return in;
}

int main() { 
	COLOR_CMD

	Person * arr = new Person[5]; 

	arr[0] = { "Igor", "Dombrovsky", "29" };
	arr[1] = { "Vladimir", "Dombrovsky", "53" };
	arr[2] = { "Elena", "Dombrovskay", "49" };
	arr[3] = { "Natalia", "Dombrovskay", "36" };
	arr[4] = { "Aleksey", "Dombrovsky", "5" };

	arr->show(arr, 5);

	Person * arr1 = new Person[3]; 

	cin >> arr[0]; 
	cin >> arr[1]; 
	cin >> arr[2];

	arr1->show(arr, 3); 
	arr1->show_ID(); // Вызов метода show_ID Смотрим на значение ID

	STOP_CMD 
	return 0; 
}