Перегрузка присваивания c. Аргументы и возвращаемые значения. Члены и не члены класса

  • 10.05.2019

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

Char str1 = "Hello "; char str2 = "world!"; str1 + str2;

и в результате получим строку «Hello world!». Правда, было бы замечательно? Ну так пожалуйста! Сегодня вы научитесь «объяснять» компьютеру, что оператором + вы хотите сложить не два числа, а две строки. И работа со строками — это один из самых удачных, на мой взгляд, примеров, чтобы начать разбираться с темой «Перегрузка операторов».

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

#include #include using namespace std; class StringsWork { private: char str;//строка, которая доступна классу public: StringsWork()//конструктор в котором очистим строку класса от мусора { for(int i = 0; i < 256; i++) str[i] = "\0"; } void operator +(char*);//прототип метода класса в котором мы перегрузим оператор + void getStr();//метод вывода данных на экран }; void StringsWork::operator +(char *s) //что должен выполнить оператор + { strcat(str, s); //сложение строк } void StringsWork::getStr() { cout << str << endl << endl;//вывод символьного массива класса на экран } int main() { setlocale(LC_ALL, "rus"); char *str1 = new char ; //выделим память для строк char *str2 = new char ; char *str3 = new char ; char *str4 = new char ; strcpy(str1,"У лукоморья дуб зелёный;\n");//инициализируем strcpy(str2,"Всё ходит по цепи кругом;\n"); strcpy(str3,"И днём и ночью кот учёный\n"); strcpy(str4,"Златая цепь на дубе том:\n"); cout << "1) " << str1; cout << "2) " << str2; cout << "3) " << str3; cout << "4) " << str4 << endl; StringsWork story;//создаем объект и добавяем в него строки с помощью перегруженного + story + str1; story + str4; story + str3; story + str2; cout << "========================================" << endl; cout << "Стих, после правильного сложения строк: " << endl; cout << "========================================" << endl << endl; story.getStr(); //Отмечу, что для числовых типов данных оператор плюс будет складывать значения, как и должен int a = 5; int b = 5; int c = 0; c = a + b; cout << "========================================" << endl << endl; cout << "a = " << a << endl << "b = " << b << endl; cout << "c = " << a << " + " << b << " = " << c << endl << endl; delete str4;//освободим память delete str3; delete str2; delete str1; return 0; }

Разберемся:

Что-то новое в коде мы увидели в строке 16 void operator +(char*); Тут мы объявили прототип метода класса в котором перегрузим наш оператор + . Чтобы перегрузить оператор необходимо использовать зарезервированное слово operator . Выглядит это так, словно вы определяете обычную функцию: void operator+ () {//код} В теле этой функции мы размещаем код, который покажет компилятору, какие действия будет выполнять оператор + (или какой-либо другой оператор). Перегруженный оператор будет выполнять указанные для него действия только в пределах того класса, в котором он определен. Ниже, в строках 20 — 23 мы уже определяем какую роль будет играть + в нашем классе. А именно, с помощью функции strcat (str, s); он будет дописывать содержимое строки s , которую мы передали по указателю, в конец строки str . Строки 17, 25 — 28 это обычный метод класса, с помощью которого строка класса будет показана на экран. Если вам не понятно, как определять методы класса вне тела класса, т.е. такой момент как void StringsWork::getStr() {//определение} , то вам сначала желательно сходить сюда . Далее, уже в главной функции main() , в строках 34 — 37 ,создаем четыре указателя на строки и выделяем необходимое количество памяти для каждой из них, не забывая о том, что для символа "\0" так же надо зарезервировать одну ячейку char *str1 = new char ; . Затем копируем в них текст с помощью функции strcpy() и показываем их на экран — строки 39 — 47 . А в строке 49 создаем объект класса. При его создании сработает конструктор класса и строка класса будет очищена от лишних данных. Теперь нам остается только сложить строки в правильной последовательности, используя перегруженный оператор + — строки 50 — 53 и посмотреть, что получилось — строка 58 .

Результат работы программы:

1) У лукоморья дуб зелёный;
2) Всё ходит по цепи кругом;
3) И днём и ночью кот учёный
4) Златая цепь на дубе том:

========================================
Стих, после правильного сложения строк:

У лукоморья дуб зелёный;
Златая цепь на дубе том:
И днём и ночью кот учёный
Всё ходит по цепи кругом;
========================================

a = 5
b = 5
c = 5 + 5 = 10

Ограничения перегрузки операторов

  • перегрузить можно практически любой оператор, за исключением следующих:

. точка (выбор элемента класса);

* звездочка (определение или разыменование указателя);

:: двойное двоеточие (область видимости метода);

?: знак вопроса с двоеточием (тернарный оператор сравнения);

# диез (символ препроцессора);

## двойной диез (символ препроцессора);

sizeof оператор нахождения размера объекта в байтах;

  • с помощью перегрузки невозможно создавать новые символы для операций;
  • перегрузка операторов не изменяет порядок выполнения операций и их приоритет;
  • унарный оператор не может использоваться для переопределения бинарной операции так же, как и бинарный оператор не переопределит унарную операцию.

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

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

Вот мы очень коротко ознакомились с перегрузкой операторов в С++. Увидели, так сказать, вершину айсберга. А вашим домашним заданием (ДА-ДА — ДОМАШНИМ ЗАДАНИЕМ!) будет доработать программу, добавив в нее перегрузку оператора для удаления строки. Какой оператор перегружать выберите сами. Либо предложите свой вариант апгрейда кода, добавив в него то, что посчитаете нужным и интересным. Ваши «труды» можете добавлять в комментарии к этой статье. Нам интересно будет посмотреть ваши варианты решения. Удачи!

Основы перегрузки операторов

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

// Операция + с целыми. int а = 100; int b = 240; int с = а + b; //с теперь равно 340

Здесь нет ничего нового, но задумывались ли вы когда-нибудь о том, что одна и та же операция + может применяться к большинству встроенных типов данных C#? Например, рассмотрим такой код:

// Операция + со строками. string si = "Hello"; string s2 = " world!"; string s3 = si + s2; // s3 теперь содержит "Hello world!"

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

Язык C# предоставляет возможность строить специальные классы и структуры, которые также уникально реагируют на один и тот же набор базовых лексем (вроде операции +). Имейте в виду, что абсолютно каждую встроенную операцию C# перегружать нельзя. В следующей таблице описаны возможности перегрузки основных операций:

Операция C# Возможность перегрузки
+, -, !, ++, --, true, false Этот набор унарных операций может быть перегружен
+, -, *, /, %, &, |, ^, > Эти бинарные операции могут быть перегружены
==, !=, <, >, <=, >= Эти операции сравнения могут быть перегружены. C# требует совместной перегрузки "подобных" операций (т.е. < и >, <= и >=, == и!=)
Операция не может быть перегружена. Oднако, аналогичную функциональность предлагают индексаторы
() Операция () не может быть перегружена. Однако ту же функциональность предоставляют специальные методы преобразования
+=, -=, *=, /=, %=, &=, |=, ^=, >= Сокращенные операции присваивания не могут перегружаться; однако вы получаете их автоматически, перегружая соответствующую бинарную операцию

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

// Общая форма перегрузки унарного оператора. public static возвращаемый_тип operator op(тип_параметра операнд) { // операции } // Общая форма перегрузки бинарного оператора. public static возвращаемый_тип operator op(тип_параметра1 операнд1, тип_параметра2 операнд2) { // операции }

Здесь вместо op подставляется перегружаемый оператор, например + или /, а возвращаемый_тип обозначает конкретный тип значения, возвращаемого указанной операцией. Это значение может быть любого типа, но зачастую оно указывается такого же типа, как и у класса, для которого перегружается оператор. Такая корреляция упрощает применение перегружаемых операторов в выражениях. Для унарных операторов операнд обозначает передаваемый операнд, а для бинарных операторов то же самое обозначают операнд1 и операнд2 . Обратите внимание на то, что операторные методы должны иметь оба спецификатора типа - public и static.

Перегрузка бинарных операторов

Давайте рассмотрим применение перегрузки бинарных операторов на простейшем примере:

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class MyArr { // Координаты точки в трехмерном пространстве public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) { this.x = x; this.y = y; this.z = z; } // Перегружаем бинарный оператор + public static MyArr operator +(MyArr obj1, MyArr obj2) { MyArr arr = new MyArr(); arr.x = obj1.x + obj2.x; arr.y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; } // Перегружаем бинарный оператор - public static MyArr operator -(MyArr obj1, MyArr obj2) { MyArr arr = new MyArr(); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; return arr; } } class Program { static void Main(string args) { MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("Координаты первой точки: " + Point1.x + " " + Point1.y + " " + Point1.z); Console.WriteLine("Координаты второй точки: " + Point2.x + " " + Point2.y + " " + Point2.z + "\n"); MyArr Point3 = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = Point1 - Point2; Console.WriteLine("\nPoint1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Console.ReadLine(); } } }

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

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

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class MyArr { // Координаты точки в трехмерном пространстве public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) { this.x = x; this.y = y; this.z = z; } // Перегружаем бинарный оператор + public static MyArr operator +(MyArr obj1, MyArr obj2) { MyArr arr = new MyArr(); arr.x = obj1.x + obj2.x; arr.y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; } // Перегружаем бинарный оператор - public static MyArr operator -(MyArr obj1, MyArr obj2) { MyArr arr = new MyArr(); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; return arr; } // Перегружаем унарный оператор - public static MyArr operator -(MyArr obj1) { MyArr arr = new MyArr(); arr.x = -obj1.x; arr.y = -obj1.y; arr.z = -obj1.z; return arr; } // Перегружаем унарный оператор ++ public static MyArr operator ++(MyArr obj1) { obj1.x += 1; obj1.y += 1; obj1.z +=1; return obj1; } // Перегружаем унарный оператор -- public static MyArr operator --(MyArr obj1) { obj1.x -= 1; obj1.y -= 1; obj1.z -= 1; return obj1; } } class Program { static void Main(string args) { MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("Координаты первой точки: " + Point1.x + " " + Point1.y + " " + Point1.z); Console.WriteLine("Координаты второй точки: " + Point2.x + " " + Point2.y + " " + Point2.z + "\n"); MyArr Point3 = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = Point1 - Point2; Console.WriteLine("Point1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = -Point1; Console.WriteLine("-Point1 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point2++; Console.WriteLine("Point2++ = " + Point2.x + " " + Point2.y + " " + Point2.z); Point2--; Console.WriteLine("Point2-- = " + Point2.x + " " + Point2.y + " " + Point2.z); Console.ReadLine(); } } }

В любой науке есть стандартные обозначения, которые облегчают понимание идей. Например, в математике это умножение, деление, сложение и прочие символьные обозначения. Выражение (x + y * z) понять куда проще, чем «умножить y, с, z и прибавить к x». Представьте, до XVI века математика не имела символьных обозначений, все выражения прописывались словесно так, будто бы это художественный текст с описанием. А привычные для нас обозначения операций появились и того позже. Значение краткой символьной записи сложно переоценить. Исходя из таких соображений, в языки программирования были добавлены перегрузки операторов. Рассмотрим на примере.

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

Практически как и любой язык, C++ поддерживает множество операторов, работающих с типами данных, встроенными в стандарт языка. Но большинство программ используют пользовательские типы для решения тех или иных задач. Например, комплексная математика или реализуются в программе за счет представления комплексных чисел или матриц в виде пользовательских типов C++. Встроенные операторы не умеют распространять свою работу и совершать необходимые процедуры над пользовательскими классами, какими бы очевидными они не казались. Поэтому для сложения матриц, например, обычно создается отдельная функция. Очевидно, что вызов функции sum_matrix (A, B) в коде будет носить менее ясный характер, чем выражение A + B.

Рассмотрим примерный класс комплексных чисел:

//представим комплексное число в виде пары чисел с плавающей точкой. class complex { double re, im; public: complex (double r, double i) :re(r), im(i) {} //конструктор complex operator+(complex); //перегрузка сложения complex operator*(complex); //перегрузка умножения }; void main() { complex a{ 1, 2 }, b{ 3, 4 }, c{0, 0}; c = a + b; c = a.operator+(b); ////операторная функция может быть вызвана как любая функция, данная запись эквивалентна a+b c = a*b + complex(1, 3); //Выполняются обычные правила приоритета операций сложения и умножения }

Аналогичным образом можно сделать, например, перегрузку операторов ввода/вывода в C++ и приспособить их для вывода таких сложных структур как матрицы.

Операторы, доступные для перегрузки

Полный список всех операторов, для которых можно использовать механизм перегрузки:

Как видно из таблицы, перегрузка допустима для большинства операторов языка. Необходимости в перегрузке оператора быть не может. Это делается исключительно для удобства. Поэтому перегрузка операторов в Java, например, отсутствует. А теперь о следующем важном моменте.

Операторы, перегрузка которых запрещена

  • Разрешение области видимости - «::»;
  • Выбор члена - «.»;
  • Выбор члена через указатель на член - «.*»;
  • Тернарный условный оператор - «?:»;
  • Оператор sizeof;
  • Оператор typeid.

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

Ограничения

Ограничения перегрузки операторов:

  • Нельзя изменить бинарный оператор на унарный и наоборот, как и нельзя добавить третий операнд.
  • Нельзя создавать новые операторы помимо тех, что имеются. Данное ограничение способствует устранению множества неоднозначностей. Если есть необходимость в новом операторе, можно использовать для этих целей функцию, которая будет выполнять требуемое действие.
  • Операторная функция может быть либо членом класса, либо иметь хотя бы один аргумент пользовательского типа. Исключением являются операторы new и delete. Такое правило запрещает изменять смысл выражений в случае, если они не содержат объектов типов, определенных пользователем. В частности, нельзя создать операторную функцию, которая работала бы исключительно с указателями или заставить оператор сложения работать как умножение. Исключением являются операторы "=", "&" и "," для объектов классов.
  • Операторная функция с первым членом, принадлежащим к одному из встроенных типов данных языка C++, не может быть членом класса.
  • Название любой операторной функции начинается с ключевого слова operator, за которым следует символьное обозначение самого оператора.
  • Встроенные операторы определены таким образом, что между ними бывает связь. Например, следующие операторы эквивалентны друг другу: ++x; x + = 1; x = x + 1. После переопределения связь между ними не сохранится. О сохранении их совместной работы подобным образом с новыми типами программисту придется заботиться отдельно.
  • Компилятор не умеет думать. Выражения z + 5 и 5 +z (где z - комплексное число) будут рассматриваться компилятором по-разному. Первое представляет собой «complex + число», а второе - «число + комплекс». Поэтому для каждого выражения нужно определить собственный оператор сложения.
  • При поиске определения оператора компилятор не отдает преимущества ни функциям-членам класса, ни вспомогательным функциям, которые определяются вне класса. Для компилятора они равны.

Интерпретации бинарных и унарных операторов.

Бинарный оператор определяется как функция-член с одной переменной или как функция с двумя переменными. Для любого бинарного оператора @ в выражение a@b, @ справедливы конструкции:

a.operator@(b) или operator@(a, b).

Рассмотрим на примере класса комплексных чисел определение операций как членов класса и вспомогательных.

Class complex { double re, im; public: complex& operator+=(complex z); complex& operator*=(complex z); }; //вспомогательные функции complex operator+(complex z1, complex z2); complex operator+(complex z, double a);

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

Выбор, описывать функцию как член класса или вне его - дело, в общем-то, вкуса. В примере выше принцип отбора был следующий: если операция изменяет левый операнд (например, a + = b), то записать ее внутри класса и использовать передачу переменной по адресу, для ее непосредственного изменения; если операция ничего не меняет и просто возвращает новое значение (например, a + b) - вынести за рамки определения класса.

Определение перегрузки унарных операторов в C++ происходит аналогичным образом, с той разницей, что они делятся на два вида:

  • префиксный оператор, расположенный до операнда, - @a, например, ++i. o определяется как a.operator@() или operator@(aa);
  • постфиксный оператор, расположенный после операнда, - b@, например, i++. o определяется как b.operator@(int) или operator@(b, int)

Точно так же, как и с бинарными операторами для случая, когда объявление оператора находится и в классе, и вне класса, выбор будет осуществляться механизмами C++.

Правила выбора оператора

Пусть бинарный оператор @ применяется к объектам x из класса X и y из класса Y. Правила для разрешения x@y будут следующие:

  1. если X представляет собой класс, искать внутри него определение оператора operator@ в качестве члена X, либо базового класса X;
  2. просмотреть контекст, в котором находится выражение x@y;
  3. если X относится к пространству имен N, искать объявление оператора в N;
  4. если Y относится к пространству имен M, искать объявление оператора в M.

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

Поиск объявлений унарных операторов происходит точно таким же способом.

Уточненное определение класса complex

Теперь построим класс комплексных чисел более подробным образом, чтобы продемонстрировать ряд озвученных ранее правил.

Class complex { double re, im; public: complex& operator+=(complex z) { //работает с выражениями вида z1 += z2 re += z.re; im += z.im; return *this; } complex& operator+=(double a) { //работает с выражениями вида z1 += 5; re += a; return *this; } complex (): re(0), im(0) {} //конструктор для инициализации по умолчанию. Таким образом, все объявленные комплексные числа будут иметь начальные значения (0, 0) complex (double r): re(r), im(0) {} // конструктор делает возможным выражение вида complex z = 11; эквивалентная запись z = complex(11); complex (double r, double i): re(r), im(i) {} //конструктор }; complex operator+(complex z1, complex z2) { //работает с выражениями вида z1 + z2 complex res = z1; return res += z2; //использование оператора, определенного как функция-член } complex operator+(complex z, double a) { //обрабатывает выражения вида z+2 complex res = z; return res += a; } complex operator+(double a, complex z) { //обрабатывает выражения вида 7+z complex res = z; return res += a; } //…

Как видно из кода, перегрузка операторов имеет весьма сложный механизм, который может сильно разрастись. Однако такой детальный подход позволяет осуществлять перегрузку даже для очень сложных структур данных. Например, перегрузка операторов C++ в классе шаблонов.

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

Особые операторы

Оператор индексации«» должен всегда определяться как член класса, так как сводит поведение объекта к массиву. При этом аргумент индексирования может быть любого типа, что позволяет создавать, например, ассоциативные массивы.

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

Оператор последовательности «,» (запятая) вызывается для объектов, если рядом с ними есть запятая. Однако в перечислении аргументов функции оператор не участвует.

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

Также определяется только в качестве члена класса из-за его связи с левым операндом.

Операторы присваивания «=», адреса «&» и последовательности «,» должны определяться в блоке public.

Итог

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

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

  1. Выполняйте перегрузку операторов только для имитации привычной записи. Для того чтобы сделать код более удобочитаемым. Если код становится сложнее по структуре или читабельности, следует отказаться от перегрузки операторов и использовать функции.
  2. Для больших операндов с целью экономии места используйте для передачи аргументы с типом константных ссылок.
  3. Оптимизируйте возвращаемые значения.
  4. Не трогайте операцию копирования, если она подходит для вашего класса.
  5. Если копирование по умолчанию не подходит, меняйте или явно запрещайте возможность копирования.
  6. Следует предпочитать функции-члены над функциями-нечленами в случаях, когда функции требуется доступ к представлению класса.
  7. Указывайте пространство имен и обозначайте связь функций с их классом.
  8. Используйте функции-нечлены для симметричных операторов.
  9. Используйте оператор () для индексов в многомерных массивах.
  10. С осторожностью используйте неявные преобразования.

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

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

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

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

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

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

#include
class three_d {

public:
three_d operators+(three_d t);
three_d operator=(three_d t);
void show ();

};
// перегрузка +
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";
}
// присвоение координат

{
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();

с.show();

с.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
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 концептуально сходен с оператором + для переменных целого типа. Мало пользы, например, можно ожидать от такого оператора +, действие которого на
соответствующий класс будет напоминать действие оператора ||. Хотя можно придать перегру­женному оператору любой смысл по своему выбору, но для ясности его применения желательно, чтобы его новое значение соотносилось с исходным значением.

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

Последнее обновление: 12.08.2018

Наряду с методами мы можем также перегружать операторы. Например, пусть у нас есть следующий класс Counter:

Class Counter { public int Value { get; set; } }

Данный класс представляет некоторый счетчик, значение которого хранится в свойстве Value.

И допустим, у нас есть два объекта класса Counter - два счетчика, которые мы хотим сравнивать или складывать на основании их свойства Value, используя стандартные операции сравнения и сложения:

Counter c1 = new Counter { Value = 23 }; Counter c2 = new Counter { Value = 45 }; bool result = c1 > c2; Counter c3 = c1 + c2;

Но на данный момент ни операция сравнения, ни операция сложения для объектов Counter не доступны. Эти операции могут использоваться для ряда примитивных типов. Например, по умолчанию мы можем складывать числовые значения, но как складывать объекты комплексных типов - классов и структур компилятор не знает. И для этого нам надо выполнить перегрузку нужных нам операторов.

Перегрузка операторов заключается в определении в классе, для объектов которого мы хотим определить оператор, специального метода:

Public static возвращаемый_тип operator оператор(параметры) { }

Этот метод должен иметь модификаторы public static , так как перегружаемый оператор будет использоваться для всех объектов данного класса. Далее идет название возвращаемого типа. Возвращаемый тип представляет тот тип, объекты которого мы хотим получить. К примеру, в результате сложения двух объектов Counter мы ожидаем получить новый объект Counter. А в результате сравнения двух мы хотим получить объект типа bool, который указывает истинно ли условное выражение или ложно. Но в зависимости от задачи возвращаемые типы могут быть любыми.

Затем вместо названия метода идет ключевое слово operator и собственно сам оператор. И далее в скобках перечисляются параметры. Бинарные операторы принимают два параметра, унарные - один параметр. И в любом случае один из параметров должен представлять тот тип - класс или структуру, в котором определяется оператор.

Например, перегрузим ряд операторов для класса Counter:

Class Counter { public int Value { get; set; } public static Counter operator +(Counter c1, Counter c2) { return new Counter { Value = c1.Value + c2.Value }; } public static bool operator >(Counter c1, Counter c2) { return c1.Value > c2.Value; } public static bool operator <(Counter c1, Counter c2) { return c1.Value < c2.Value; } }

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

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

Public static Counter operator +(Counter c1, Counter c2) { return new Counter { Value = c1.Value + c2.Value }; }

Также переопределены две операции сравнения. Если мы переопределяем одну из этих операций сравнения, то мы также должны переопределить вторую из этих операций. Сами операторы сравнения сравнивают значения свойств Value и в зависимости от результата сравнения возвращают либо true, либо false.

Теперь используем перегруженные операторы в программе:

Static void Main(string args) { Counter c1 = new Counter { Value = 23 }; Counter c2 = new Counter { Value = 45 }; bool result = c1 > c2; Console.WriteLine(result); // false Counter c3 = c1 + c2; Console.WriteLine(c3.Value); // 23 + 45 = 68 Console.ReadKey(); }

Стоит отметить, что так как по сути определение оператора представляет собой метод, то этот метод мы также можем перегрузить, то есть создать для него еще одну версию. Например, добавим в класс Counter еще один оператор:

Public static int operator +(Counter c1, int val) { return c1.Value + val; }

Данный метод складывает значение свойства Value и некоторое число, возвращая их сумму. И также мы можем применить этот оператор:

Counter c1 = new Counter { Value = 23 }; int d = c1 + 27; // 50 Console.WriteLine(d);

Следует учитывать, что при перегрузке не должны изменяться те объекты, которые передаются в оператор через параметры. Например, мы можем определить для класса Counter оператор инкремента:

Public static Counter operator ++(Counter c1) { c1.Value += 10; return c1; }

Поскольку оператор унарный, он принимает только один параметр - объект того класса, в котором данный оператор определен. Но это неправильное определение инкремента, так как оператор не должен менять значения своих параметров.

И более корректная перегрузка оператора инкремента будет выглядеть так:

Public static Counter operator ++(Counter c1) { return new Counter { Value = c1.Value + 10 }; }

То есть возвращается новый объект, который содержит в свойстве Value инкрементированное значение.

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

Например, используем операцию префиксного инкремента:

Counter counter = new Counter() { Value = 10 }; Console.WriteLine($"{counter.Value}"); // 10 Console.WriteLine($"{(++counter).Value}"); // 20 Console.WriteLine($"{counter.Value}"); // 20

Консольный вывод:

Теперь используем постфиксный инкремент:

Counter counter = new Counter() { Value = 10 }; Console.WriteLine($"{counter.Value}"); // 10 Console.WriteLine($"{(counter++).Value}"); // 10 Console.WriteLine($"{counter.Value}"); // 20

Консольный вывод:

Также стоит отметить, что мы можем переопределить операторы true и false . Например, определим их в классе Counter:

Class Counter { public int Value { get; set; } public static bool operator true(Counter c1) { return c1.Value != 0; } public static bool operator false(Counter c1) { return c1.Value == 0; } // остальное содержимое класса }

Эти операторы перегружаются, когда мы хотим использовать объект типа в качестве условия. Например:

Counter counter = new Counter() { Value = 0 }; if (counter) Console.WriteLine(true); else Console.WriteLine(false);

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

    унарные операторы +, -, !, ~, ++, --

    бинарные операторы +, -, *, /, %

    операции сравнения ==, !=, <, >, <=, >=

    логические операторы &&, ||

    операторы присваивания +=, -=, *=, /=, %=

И есть ряд операторов, которые нельзя перегрузить, например, операцию равенства = или тернарный оператор?: , а также ряд других.

Полный список перегружаемых операторов можно найти в документации msdn

При перегрузке операторов также следует помнить, что мы не можем изменить приоритет оператора или его ассоциативность, мы не можем создать новый оператор или изменить логику операторов в типах, который есть по умолчанию в.NET.