Перегрузка бинарных операторов c. Основы перегрузки операторов. Оператор размещения new() и оператор delete()

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

В 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(); } } }

Последнее обновление: 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.

Во многих языках программирования используются операторы: как минимум, присваивания (= , := или похожие) и арифметические операторы (+ , - , * и /). В большинстве языков со статической типизацией эти операторы привязаны к типам. Например, в Java сложение с оператором + возможно лишь для целых чисел, чисел с плавающей запятой и строк. Если мы определим свои классы для математических объектов, например, для матриц, мы можем реализовать метод их сложения, но вызвать его можно лишь чем-то вроде этого: a = b.add(c) .

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

Когда стоит перегружать операторы?

Запомните главное: перегружайте операторы тогда и только тогда, когда это имеет смысл. То есть если смысл перегрузки очевиден и не несёт в себе скрытых сюрпризов. Перегруженные операторы должны действовать так же, как и их базовые версии. Естественно, допустимы исключения, но лишь в тех случаях, когда они сопровождаются понятными объяснениями. Наглядным примером являются операторы << и >> стандартной библиотеки iostream , которые явно ведут себя не как обычные операторы .

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

Matrix a, b; Matrix c = a + b;

Примером плохой перегрузки оператора сложения будет сложение двух объектов типа “игрок” в игре. Что имел в виду создатель класса? Каким будет результат? Мы не знаем, что делает операция, и поэтому пользоваться этим оператором опасно.

Как перегружать операторы?

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

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

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

Class Rational { public: //Constructor can be used for implicit conversion from int: Rational(int numerator, int denominator = 1); Rational operator+(Rational const& rhs) const; }; int main() { Rational a, b, c; int i; a = b + c; //ok, no conversion necessary a = b + i; //ok, implicit conversion of the second argument a = i + c; //ERROR: first argument can not be implicitly converted }

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

Реализуйте унарные операторы и бинарные операторы типа “X =” в виде методов класса, а прочие бинарные операторы - в виде свободных функций.

Какие операторы можно перегружать?

Мы можем перегрузить почти любой оператор C++, учитывая следующие исключения и ограничения:

  • Нельзя определить новый оператор, например, operator** .
  • Следующие операторы перегружать нельзя:
    1. ?: (тернарный оператор);
    2. :: (доступ к вложенным именам);
    3. . (доступ к полям);
    4. .* (доступ к полям по указателю);
    5. sizeof , typeid и операторы каста.
  • Следующие операторы можно перегрузить только в качестве методов:
    1. = (присваивание);
    2. -> (доступ к полям по указателю);
    3. () (вызов функции);
    4. (доступ по индексу);
    5. ->* (доступ к указателю-на-поле по указателю);
    6. операторы конверсии и управления памятью.
  • Количество операндов, порядок выполнения и ассоциативность операторов определяется стандартной версией.
  • Как минимум один операнд должен быть пользовательского типа. Typedef не считается.

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

Бинарный оператор, такой как, например, оператор сложения operator + должен быть определен либо как нестатическая функция - член класса с одним параметром, либо как функция, которая не является членом класса, с двумя параметрами.

Выданные вам сообщения компилятора

Main.cpp:17:5: error: C++ requires a type specifier for all declarations operator+(Cat a, Cat b) { ^ main.cpp:18:16: error: cannot initialize return object of type "int" with an rvalue of type "Cat *" return new Cat(a.value + b.value); ^~~~~~~~~~~~~~~~~~~~~~~~~~

говорят о том, что у определенного вами оператора отсутствует тип возвращаемого значения.

Тип возвращаемого значения может отсутствовать только у функций преобразования. В случае же оператора сложения вы обязаны указать тип возвращаемого значения.

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

Оператор должен возвращать сам объект либо с квалификатором const либо без него.

Как уже выше упомянуто, оператор может быть объявлен как нестатическая функция-член класса с одним параметром.

В этом случае оператор operator + может выглядеть так

Class Cat { private: int value = 1; public: Cat(int _value) { value = _value; } Cat operator +(const Cat &a) const { return Cat(this->value + a.value); } };

Class Cat { private: int value = 1; public: Cat(int _value) { value = _value; } const Cat operator +(const Cat &a) const { return Cat(this->value + a.value); } };

Вы могли бы перегрузить оператор также для rvalue -ссылок, как, например,

Cat operator +(const Cat &&a) const { return Cat(this->value + a.value); }

Cat operator +(Cat &&a) const { return Cat(this->value + a.value); }

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

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

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

#include class Cat { private: int value = 1; public: Cat(int _value) { value = _value; } const Cat operator +(const Cat &a) const { return Cat(this->

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

#include class Cat { private: int value = 1; public: explicit Cat(int _value) { value = _value; } const Cat operator +(const Cat &a) const { return Cat(this->value + a.value); } }; int main() { Cat c1(10); c1 + 5.5; return 0; }

Для этой программы компилятор выдаст сообщение об ошибке на подобие следующего

Prog.cpp:24:5: error: no match for "operator+" (operand types are "Cat" and "double") c1 + 5.5; ^

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

Саму функцию вы можете определить как внутри определения класса, так и вне его.

Например,

#include class Cat { private: int value = 1; public: Cat(int _value) { value = _value; } friend const Cat operator +(const Cat &a, const Cat &b) { return Cat(a.value + b.value); } }; int main() { Cat c1(10); Cat c2(5); Cat c3 = c1 + c2; return 0; }

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

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

Cat(int value) : value(value) { }

Если член класса value не может принимать отрицательные значения, то лучше его и соответствующий параметр конструктора объявить, как имеющие тип unsigned int .

В продолжение темы:
Смартфон

ПРАВИТЕЛЬСТВО МОСКОВСКОЙ ОБЛАСТИПОСТАНОВЛЕНИЕОб установлении штатной численности и утверждении Положения о Министерстве инвестиций и инноваций Московской области Документ с...

Новые статьи
/
Популярные