Как создать оператор в классе с
Перейти к содержимому

Как создать оператор в классе с

  • автор:

Как создать оператор в классе с

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

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

Если функция оператора определена как отдельная функция и не является членом класса, то количество параметров такой функции совпадает с количеством операндов оператора. Например, у функции, которая представляет унарный оператор, будет один параметр, а у функции, которая представляет бинарный оператор, — два параметра. Если оператор принимает два операнда, то первый операнд передается первому параметру функции, а второй операнд — второму параметру. При этом как минимум один из параметров должен представлять тип класса.

Формальное определение операторов в виде функций-членов класса:

// бинарный оператор ReturnType operator Op(Type right_operand); // унарный оператор ClassType& operator Op();

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

// бинарный оператор ReturnType operator Op(const ClassType& left_operand, Type right_operand); // альтернативное определение, где класс, для которого создается оператор, представляет правый операнд ReturnType operator Op(Type left_operand, const ClassType& right_operand); // унарный оператор ClassType& operator Op(ClassType& obj);

Здесь ClassType представляет тип, для которого определяется оператор. Type — тип другого операнда, который может совпадать, а может и не совпадать с первым. ReturnType — тип возвращаемого результата, который также может совпадать с одним из типов операндов, а может и отличаться. Op — сама операция.

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

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout Counter operator + (const Counter& counter) const < return Counter; > private: int value; >; int main() < Counter c1; Counter c2; Counter c3 = c1 + c2; c3.print(); // Value: 30 >

Здесь в классе Counter определен оператор сложения, цель которого сложить два объекта Counter:

Counter operator + (const Counter& counter) const < return Counter; >

Текущий объект будет представлять левый операнд операции. Объект, который передается в функцию через параметр counter, будет представлять правый операнд операции. Здесь параметр функции определен как константная ссылка, но это необязательно. Также функция оператора определена как константная, но это тоже не обязательно.

Результатом оператора сложения является новый объект Counter, в котором значение value равно сумме значений value обоих операндов.

После опеределения оператора можно складывать два объекта Counter:

Counter c1; Counter c2; Counter c3 ; c3.print(); // Value: 30

Подобным образом можно определить функцию оператора вне класса:

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout int value; // к приватным переменным внешняя функция оператора не может обращаться >; // определяем оператор сложения вне класса Counter operator + (const Counter& c1, const Counter& c2) < return Counter; > int main() < Counter c1; Counter c2; Counter c3 ; c3.print(); // Value: 30 >

Если бинарный оператор определяется в виде внешней функции, как здесь, то он принимает два параметра. Первый параметр будет представлять левый операнд операции, а второй параметр — правый операнд.

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

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

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout Counter operator + (const Counter& counter) const < return Counter; > int operator + (int number) const < return value + number; >private: int value; >; int main() < Counter counter; int number = counter + 30; std::cout 

Здесь определена вторая версия оператора сложения, которая складывает объект Counter с числом и возвращает также число. Поэтому левый операнд операции должен представлять тип Counter, а правый операнд - тип int.

Какие операторы где переопределять? Операторы присвоения, индексирования ([]), вызова (()), доступа к члену класса по указателю (->) следует определять в виде функций-членов класса. Операторы, которые изменяют состояние объекта или непосредственно связаны с объектом (инкремент, декремент), обычно также определяются в виде функций-членов класса. Операторы выделения и удаления памяти ( new new[] delete delete[] ) определяются только в виде функций, которые не являются членами класса. Все остальные операторы можно определять как отдельные функции, а не члены класса.

Операторы сравнения

Результатом операторов сравнения ( == , != , < , >), как правило, является значение типа bool . Например, перегрузим данные операторы для типа Counter:

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout bool operator == (const Counter& counter) const < return value == counter.value; >bool operator != (const Counter& counter) const < return value != counter.value; >bool operator > (const Counter& counter) const < return value >counter.value; > bool operator < (const Counter& counter) const < return value < counter.value; >private: int value; >; int main() < Counter c1(20); Counter c2(10); bool b1 = c1 == c2; // false bool b2 = c1 >c2; // true std::cout c2 b">default:

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout bool operator == (const Counter& counter) const = default; bool operator != (const Counter& counter) const = default; private: int value; >; int main() < Counter c1(20); Counter c2(10); bool b1 = c1 == c2; // false bool b2 = c1 != c2; // true std::cout bool operator == (const Counter& counter) const = default;

По умолчанию будут сравниваться все поля класса, для которых определен оператор ==. Если значения всех полей будут равны, то оператор возвратить true

Операторы присвоения

Оператор присвоения обычно возвращает ссылку на свой левый операнд:

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout // оператор присвоения Counter& operator += (const Counter& counter) < value += counter.value; return *this; // возвращаем ссылку на текущий объект >private: int value; >; int main() < Counter c1; Counter c2; c1 += c2; c1.print(); // Value: 70 >

Унарные операции

Унарные операции обычно возвращают новый объект, созданный на основе имеющегося. Например, возьмем операцию унарного минуса:

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout // оператор унарного минуса Counter operator - () const < return Counter; > private: int value; >; int main() < Counter c1; Counter c2 = -c1; // применяем оператор унарного минуса c2.print(); // Value: -20 >

Здесь операция унарного минуса возвращает новый объект Counter, значение value в котором фактически равно значению value текущего объекта, умноженного на -1.

Операции инкремента и декремента

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

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout // префиксные операторы Counter& operator++ () < value += 1; return *this; >Counter& operator-- () < value -= 1; return *this; >// постфиксные операторы Counter operator++ (int) < Counter copy ; ++(*this); return copy; > Counter operator-- (int) < Counter copy ; --(*this); return copy; > private: int value; >; int main() < Counter c1; Counter c2 = c1++; c2.print(); // Value: 20 c1.print(); // Value: 21 --c1; c1.print(); // Value: 20 >

Префиксные операторы должны возвращать ссылку на текущий объект, который можно получить с помощью указателя this:

Counter& operator++ ()

В самой функции можно определить некоторую логику по инкременту значения. В данном случае значение value увеличивается на 1.

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

Counter operator++ (int) < Counter copy ; ++(*this); return copy; >

Чтобы постфиксная форма отличалась от префиксной постфиксные версии получают дополнительный параметр типа int, который не используется. Хотя в принципе мы можем его использовать.

Переопределение оператора

#include class Counter < public: Counter(int val) < value =val; >int getValue()const private: int value; >; std::ostream& operator <<(std::ostream& stream, const Counter& counter) < stream << "Value: "; stream << counter.getValue(); return stream; >int main() < Counter counter1; Counter counter2; std::cout 

Стандартный выходной поток cout имеет тип std::ostream . Поэтому первый параметр (левый операнд) представляет объект ostream , а второй (правый операнд) - выводимый объект Counter. Поскольку мы не можем изменить стандартное определение std::ostream, поэтому определяем функцию оператора, которая не является членом класса.

std::ostream& operator

В данном случае для выводим значение переменной value. Для получения значения value извне класса Counter я добавил функцию getValue() .

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

После определения функции оператора можно выводить на консоль объекты Counter:

Counter counter1; std::cout 

Выражение одних операторов через другие

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

#include class Counter < public: Counter(int n) < value = n; >void print() const < std::cout Counter& operator+=(const Counter& counter) < value += counter.value; return *this; >; Counter& operator+(const Counter& counter) < Counter copy< value >; // копируем данные текущего объекта return copy += counter; >; private: int value; >; int main() < Counter counter1; Counter counter2; counter1 += counter2; counter1.print(); // value: 30 Counter counter3 ; counter3.print(); // value: 40 >

Здесь вначале реализован оператор сложения с присвоением +=:

Counter& operator+=(const Counter& counter) < value += counter.value; return *this; >;

В функции оператора сложения мы создаем копию текущего объекта и к этой копии и аргументу применяем оператор +=:

Counter& operator+(const Counter& counter) < Counter copy< value >; // копируем данные текущего объекта return copy += counter; >;

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

Дополнительные возможности ООП в C#

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

class Counter < public int Value < get; set; >>

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

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

Counter counter1 = new Counter < Value = 23 >; Counter counter2 = new Counter < Value = 45 >; bool result = counter1 > counter2; Counter c3 = counter1 + counter2;

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

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

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

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

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

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

class Counter < public int Value < get; set; >public static Counter operator +(Counter counter1, Counter counter2) < return new Counter < Value = counter1.Value + counter2.Value >; > public static bool operator >(Counter counter1, Counter counter2) < return counter1.Value >counter2.Value; > public static bool operator <(Counter counter1, Counter counter2) < return counter1.Value < counter2.Value; >>

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

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

public static Counter operator +(Counter counter1, Counter counter2) < return new Counter < Value = counter1.Value + counter2.Value >; >

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

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

Counter counter1 = new Counter < Value = 23 >; Counter counter2 = new Counter < Value = 45 >; bool result = counter1 > counter2; Console.WriteLine(result); // false Counter counter3 = counter1 + counter2; Console.WriteLine(counter3.Value); // 23 + 45 = 68

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

public static int operator +(Counter counter1, int val)

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

Counter counter1 = new Counter < Value = 23 >; int result = counter1 + 27; // 50 Console.WriteLine(result);

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

  • унарные операторы +x, -x, !x, ~x, ++, --, true, false
  • бинарные операторы +, -, *, /, %
  • операции сравнения ==, !=, , =
  • поразрядные операторы &, |, ^, <>
  • логические операторы &&, ||

Кроме того, есть несколько операторов, которые надо определять парами:

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

Определение инкремента и декремента

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

public static Counter operator ++(Counter counter1)

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

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

public static Counter operator ++(Counter counter1) < return new Counter < Value = counter1.Value + 10 >; >

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

Counter counter1 = new Counter() < Value = 10 >; Counter counter2 = counter1++; Console.WriteLine(counter1.Value); // 20 Console.WriteLine(counter2.Value); // 10 Counter counter3 = ++counter1; Console.WriteLine(counter1.Value); // 30 Console.WriteLine(counter3.Value); // 30

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

Определение операций true и false

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

class Counter < public int Value < get; set; >public static bool operator true(Counter counter1) < return counter1.Value != 0; >public static bool operator false(Counter counter1) < return counter1.Value == 0; >>
Counter counter = new Counter() < Value = 0 >; if (counter) Console.WriteLine(true); else Console.WriteLine(false);

Также стоит отметить, что если мы хотим использовать операцию отрицания, типа if (!counter) , то нам также необходимо определить для типа операцию ! :

Counter counter = new Counter() < Value = 2 >; if (!counter) Console.WriteLine(true); else Console.WriteLine(false); class Counter < public int Value < get; set; >public static bool operator !(Counter counter1) < return counter1.Value == 0; >public static bool operator true(Counter counter1) < return counter1.Value != 0; >public static bool operator false(Counter counter1) < return counter1.Value == 0; >>

Операция отрицания фактически синонимична операции false , поэтому содержит аналогичное условие.

  • Вопросы для самопроверки
  • Упражнения

Перегрузка операторов в языке C++

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

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

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

Перегрузить можно только те операторы, которые уже определены в C++. Создать новые операторы нельзя.

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

Операторы, которые перегружать нельзя:

  • тернарный оператор ( ? : )
  • оператор sizeof
  • оператор разрешения области видимости ( :: )
  • оператор выбора члена класса "."
  • оператор разыменования указателя на член класса ".*"

Синтаксис перегрузки операторов очень похож на определение функции с именем operator@, где @ — это идентификатор оператора (например +, -, ==).

bool operator==(const MyClass& v1, const MyClass& v2)

В большинстве случаев, операторы (кроме условных) возвращают объект, или ссылку на тип, к которому относятся его аргументы.

Существует три основных способа перегрузки операторов:

  • через обычные функции
  • через функции дружественные для класса
  • через методы класса.

Перегрузка через обычные функции

Для перегрузки оператора через обычную функцию достаточно объявить эту функцию вне тела класса.

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

class MyVal < int m_val; public: MyVal(int val) < m_val = val; >int getVal() < return m_val; >>; MyVal operator+(MyVal& v1, MyVal& v2) < return MyVal(v1.getVal() + v2.getVal()); >MyVal val1(10), val2(20); std::cout 

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

Перегрузка через дружественные функции

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

Это позволяет функции обращаться к членам класса напрямую, без использования геттеров.

class MyVal < int m_val; public: MyVal(int val) < m_val = val; >int getVal() < return m_val; >friend MyVal operator+(MyVal& v1, MyVal& v2); >; MyVal operator+(MyVal v1, MyVal v2) < return MyVal(v1.m_val + v2.m_val); >MyVal val1(10), val2(20); std::cout 

Если необходимо, дружественные функции могут быть определены внутри класса.

class MyVal < int m_val; public: MyVal(int val) < m_val = val; >int getVal() < return m_val; >friend MyVal operator+(MyVal& v1, MyVal& v2) < return MyVal(v1.m_val + v2.m_val); >>; MyVal val1(10), val2(20); std::cout 

Перегрузка через методы класса

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

class MyVal < int m_val; public: MyVal(int val) < m_val = val; >int getVal() < return m_val; >MyVal operator+(MyVal& v); >; MyVal MyVal::operator+(MyVal& v) < return MyVal(m_val + v.m_val); >MyVal val1(10), val2(20); std::cout 

Дополнительная информация

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

Операторы присваивания (=), индекса ([]), вызова функции (()), и выбора члена (->) могут перегружаться только через методы класса.

Перегрузка операторов через методы класса не используются, если первый операнд не является классом (например int), или это класс, который нельзя изменять (например std::ostream). Соответственно не получится через методы класса переопределить оператор , так как первый его операнд - специальный класс std::ostream.

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

Для удобного вывода сложных структур в поток вывода можно использовать перегрузку оператора вывода .

class MyCoord < int m_x; int m_y; int m_z; public: MyCoord(int x, int y, int z) < m_x = x, m_y = y, m_z = z; >friend std::ostream& operator<<(std::ostream& out, MyCoord& point); >; std::ostream& operator <<(std::ostream& out, MyCoord& v) < out << "Coord(" << v.m_x << "," << v.m_y << "," << v.m_z << ")"; return out; >MyCoord val(10, 20, 30); std::cout 

Так же можно перегрузить и оператор ввода, используя std::istream вместо std::ostream.

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

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

class MyVal < int m_val; public: MyVal(int val) < m_val = val; >int getVal() < return m_val; >MyVal operator-() const; >; MyVal MyVal::operator-() const < return MyVal(-m_val); >MyVal val1(10); MyVal val2 = -val1; std::cout 

Также, поскольку объект класса не меняется, а возвращается новый экземпляр класса, метод следует объявлять константным.

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

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

class MyVal < int m_val; public: MyVal(int val) < m_val = val; >int getVal() < return m_val; >MyVal& operator++(); // Префиксная версия MyVal operator++(int); // Постфиксная версия friend std::ostream& operator<<(std::ostream &out, const MyVal &v); >; std::ostream& operator MyVal& MyVal::operator++() // Префиксная версия < ++m_val; return *this; >MyVal MyVal::operator++(int) // Постфиксная версия < MyVal tmp(m_val); ++(*this); return tmp; >MyVal val(10); std::cout 

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

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

Перегрузка оператора индексации

class MyArr < int m_arr[10]; public: int& operator[](const int index); >; int& MyArr::operator[](const int index) < assert(index >= 0 && index < 10); return m_arr[index]; >MyArr arr; arr[0] = 10; arr[1] = 20; std::cout 

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

class MyArr < int m_arr[10] = ; public: int& operator[](const int index); const int& operator[](const int index) const; >; int& MyArr::operator[](const int index) < assert(index >= 0 && index < 10); return m_arr[index]; >const int& MyArr::operator[](const int index) const < assert(index >= 0 && index < 10); return m_arr[index]; >MyArr arr1; arr1[0] = 10; const MyArr arr2; // arr[0] = 20; - Ошибка! Нельзя по константной ссылке присваивать значение std::cout 

Также при перегрузке оператора индексации можно использовать проверку передаваемого индекса на корректность.

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

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

Перегрузка оператора ( ) позволяет задать произвольные тип и количество параметров при вызове оператора ( ).

class Matrix < int m_data[5][5] = ; public: int& operator()(int row, int col); const int& operator()(int row, int col) const; void operator()(); >; int& Matrix::operator()(int row, int col) < assert(row >= 0 && row < 5); assert(col >= 0 && col < 5); return m_data[row][col]; >const int& Matrix::operator()(int row, int col) const < assert(row >= 0 && row < 5); assert(col >= 0 && col < 5); return m_data[row][col]; >void Matrix::operator()() < for (int row = 0; row < 5; ++row) for (int col = 0; col < 5; ++col) m_data[row][col] = 55; >Matrix m1; m1(0, 0) = 10; const Matrix m2; std::cout 

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

class MyVal < int m_val = 0; public: MyVal(int v) < m_val = v; >operator int(); >; MyVal::operator int() < return m_val; >MyVal v(10); std::cout 

Также можно перегружать преобразование в свои типы данных.

class MyVal < public: int m_val = 0; MyVal(int v) < m_val = v; >>; class MyClass < int m_val = 100; public: operator MyVal(); >; MyClass::operator MyVal() < return MyVal(m_val); >MyClass val; MyVal v = (MyVal)val; std::cout 

Урок №130. Введение в перегрузку операторов

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

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

Оглавление:

  1. Операторы, как функции
  2. Вызов перегруженных операторов
  3. Ограничения в перегрузке операторов

Операторы, как функции

Рассмотрим следующий фрагмент:

std :: cout << a + b << '\n' ;

Здесь компилятор использует встроенную версию оператора плюс ( + ) для целочисленных операндов — эта функция сложит два целочисленных значения ( a и b ), и возвратит целочисленный результат. Когда вы видите выражение a + b , то думайте о нем, как о вызове функции operator+(a, b) (где operator+ является именем функции).

Теперь рассмотрим следующий фрагмент:

double m = 4.0 ;
double p = 5.0 ;
std :: cout << m + p << '\n' ;

Компилятор также предоставит встроенную версию оператора плюс ( + ) для операндов типа double. Выражение m + p приведет к вызову функции operator+(m, p) , а, благодаря перегрузке оператора, вызовется версия double (вместо версии int).

Теперь рассмотрим, что произойдет, если мы попытаемся добавить два объекта класса:

Mystring hello = "Hello, " ;
Mystring world = "World!" ;
std :: cout << hello + world << '\n' ;

Как вы думаете, какой будет результат? Наверное, вывод строки Hello, World! ? Нет, результатом будет ошибка, так как класс Mystring является пользовательским типом данных, а компилятор не имеет встроенной версии operator+() для использования с операндами Mystring. Для того, чтобы сделать то, что мы хотим, нам придется написать свою версию функции operator+() и указать в ней алгоритм работы с операндами типа Mystring. То, как это сделать в коде, мы рассмотрим на следующем уроке.

Вызов перегруженных операторов

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

Если все операнды являются фундаментальных типов данных, то вызывать следует встроенные соответствующие версии операторов (если таковые существуют). Если таковых не существует, то компилятор выдаст ошибку.

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

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

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

оператор разрешения области видимости ( :: );

операторы выбора члена . и .* .

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

В-третьих, по крайней мере один из операндов перегруженного оператора должен быть пользовательского типа данных. Это означает, что вы не можете перегрузить operator+() для выполнения операции сложения значения типа int со значением типа double. Однако вы можете перегрузить operator+() для выполнения операции сложения значения типа int с объектом класса Mystring.

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

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

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

В математике операция возведения в степень выполняется до выполнения базовых арифметических операций, поэтому 2 + 5 ^ 2 обрабатывается как 2 + (5 ^ 2) => 2 + 25 => 27 . Однако в языке C++ у базовых арифметических операторов приоритет выше, нежели у оператора ^ , поэтому 2 + 5 ^ 2 выполнится как (2 + 5) ^ 2 => 7 ^ 2 => 49 .

Вам нужно будет явно заключать в скобки часть с возведением в степень (например, 2 + (5 ^ 2) ) каждый раз, когда вы хотите, чтобы она выполнялась первой, что очень легко забыть и, таким образом, наделать ошибок. Поэтому проводить подобные эксперименты не рекомендуется.

Примечание: В языке C++ для возведения в степень используется функция pow() из заголовочного файла cmath. В примере, приведенном выше, с выполнением выражения 2 + 5 ^ 2 в языке C++, имеется в виду, что вы перегрузите побитовый оператор XOR ( ^ ) для выполнения операции возведения в степень.

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *