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

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

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

Во многих языках программирования используются операторы: как минимум, присваивания (= , := или похожие) и арифметические операторы (+ , - , * и /). В большинстве языков со статической типизацией эти операторы привязаны к типам. Например, в 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@, где @ - это идентификатор оператора (например +, -, <<, >>). Рассмотрим простейший пример:
class Integer { private: int value; public: Integer(int i): value(i) {} const Integer operator+(const Integer& rv) const { return (value + rv.value); } };
В данном случае, оператор оформлен как член класса, аргумент определяет значение, находящееся в правой части оператора. Вообще, существует два основных способа перегрузки операторов: глобальные функции, дружественные для класса, или подставляемые функции самого класса. Какой способ, для какого оператора лучше, рассмотрим в конце топика.

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

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

Рассмотрим примеры перегрузки унарных операторов для определенного выше класса Integer. Заодно определим их в виде дружественных функций и рассмотрим операторы декремента и инкремента:
class Integer { private: int value; public: Integer(int i): value(i) {} //унарный + friend const Integer& operator+(const Integer& i); //унарный - friend const Integer operator-(const Integer& i); //префиксный инкремент friend const Integer& operator++(Integer& i); //постфиксный инкремент friend const Integer operator++(Integer& i, int); //префиксный декремент friend const Integer& operator--(Integer& i); //постфиксный декремент friend const Integer operator--(Integer& i, int); }; //унарный плюс ничего не делает. const Integer& operator+(const Integer& i) { return i.value; } const Integer operator-(const Integer& i) { return Integer(-i.value); } //префиксная версия возвращает значение после инкремента const Integer& operator++(Integer& i) { i.value++; return i; } //постфиксная версия возвращает значение до инкремента const Integer operator++(Integer& i, int) { Integer oldValue(i.value); i.value++; return oldValue; } //префиксная версия возвращает значение после декремента const Integer& operator--(Integer& i) { i.value--; return i; } //постфиксная версия возвращает значение до декремента const Integer operator--(Integer& i, int) { Integer oldValue(i.value); i.value--; return oldValue; }
Теперь вы знаете, как компилятор различает префиксные и постфиксные версии декремента и инкремента. В случае, когда он видит выражение ++i, то вызывается функция operator++(a). Если же он видит i++, то вызывается operator++(a, int). То есть вызывается перегруженная функция operator++, и именно для этого используется фиктивный параметр int в постфиксной версии.

Бинарные операторы

Рассмотрим синтаксис перегрузки бинарных операторов. Перегрузим один оператор, который возвращает l-значение, один условный оператор и один оператор, создающий новое значение (определим их глобально):
class Integer { private: int value; public: Integer(int i): value(i) {} friend const Integer operator+(const Integer& left, const Integer& right); friend Integer& operator+=(Integer& left, const Integer& right); friend bool operator==(const Integer& left, const Integer& right); }; const Integer operator+(const Integer& left, const Integer& right) { return Integer(left.value + right.value); } Integer& operator+=(Integer& left, const Integer& right) { left.value += right.value; return left; } bool operator==(const Integer& left, const Integer& right) { return left.value == right.value; }
Во всех этих примерах операторы перегружаются для одного типа, однако, это необязательно. Можно, к примеру, перегрузить сложение нашего типа Integer и определенного по его подобию Float.

Аргументы и возвращаемые значения

Как можно было заметить, в примерах используются различные способы передачи аргументов в функции и возвращения значений операторов.
  • Если аргумент не изменяется оператором, в случае, например унарного плюса, его нужно передавать как ссылку на константу. Вообще, это справедливо для почти всех арифметических операторов (сложение, вычитание, умножение...)
  • Тип возвращаемого значения зависит от сути оператора. Если оператор должен возвращать новое значение, то необходимо создавать новый объект (как в случае бинарного плюса). Если вы хотите запретить изменение объекта как l-value, то нужно возвращать его константным.
  • Для операторов присваивания необходимо возвращать ссылку на измененный элемент. Также, если вы хотите использовать оператор присваивания в конструкциях вида (x=y).f(), где функция f() вызывается для для переменной x, после присваивания ей y, то не возвращайте ссылку на константу, возвращайте просто ссылку.
  • Логические операторы должны возвращать в худшем случае int, а в лучшем bool.

Оптимизация возвращаемого значения

При создании новых объектов и возвращении их из функции следует использовать запись как для вышеописанного примера оператора бинарного плюса.
return Integer(left.value + right.value);
Честно говоря, не знаю, какая ситуация актуальна для C++11, все рассуждения далее справедливы для C++98.
На первый взгляд, это похоже на синтаксис создания временного объекта, то есть как будто бы нет разницы между кодом выше и этим:
Integer temp(left.value + right.value); return temp;
Но на самом деле, в этом случае произойдет вызов конструктора в первой строке, далее вызов конструктора копирования, который скопирует объект, а далее, при раскрутке стека вызовется деструктор. При использовании первой записи компилятор изначально создаёт объект в памяти, в которую нужно его скопировать, таким образом экономится вызов конструктора копирования и деструктора.

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

В C++ есть операторы, обладающие специфическим синтаксисом и способом перегрузки. Например оператор индексирования . Он всегда определяется как член класса и, так как подразумевается поведение индексируемого объекта как массива, то ему следует возвращать ссылку.
Оператор запятая
В число «особых» операторов входит также оператор запятая. Он вызывается для объектов, рядом с которыми поставлена запятая (но он не вызывается в списках аргументов функций). Придумать осмысленный пример использования этого оператора не так-то просто. Хабраюзер в комментариях к предыдущей статье о перегрузке .
Оператор разыменования указателя
Перегрузка этих операторов может быть оправдана для классов умных указателей. Этот оператор обязательно определяется как функция класса, причём на него накладываются некоторые ограничения: он должен возвращать либо объект (или ссылку), либо указатель, позволяющий обратиться к объекту.
Оператор присваивания
Оператор присваивания обязательно определяется в виде функции класса, потому что он неразрывно связан с объектом, находящимся слева от "=". Определение оператора присваивания в глобальном виде сделало бы возможным переопределение стандартного поведения оператора "=". Пример:
class Integer { private: int value; public: Integer(int i): value(i) {} Integer& operator=(const Integer& right) { //проверка на самоприсваивание if (this == &right) { return *this; } value = right.value; return *this; } };

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

Неперегружаемые операторы
Некоторые операторы в C++ не перегружаются в принципе. По всей видимости, это сделано из соображений безопасности.
  • Оператор выбора члена класса ".".
  • Оператор разыменования указателя на член класса ".*"
  • В С++ отсутствует оператор возведения в степень (как в Fortran) "**".
  • Запрещено определять свои операторы (возможны проблемы с определением приоритетов).
  • Нельзя изменять приоритеты операторов
Как мы уже выяснили, существует два способа операторов - в виде функции класса и в виде дружественной глобальной функции.
Роб Мюррей, в своей книге C++ Strategies and Tactics определил следующие рекомендации по выбору формы оператора:

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

Доброго времени суток, уважаемые читатели!

Когда я только начал свой путь по изучению C++, у меня возникало много вопросов, на которые, порой, не удавалось быстро найти ответов. Не стала исключением и такая тема как перегрузка операторов. Теперь, когда я разобрался в этой теме, я хочу помочь другим расставить все точки над i .

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

Для чего нужна перегрузка?

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

Struct Vector3 { int x, y, z; Vector3() {} Vector3(int x, int y, int z) : x(x), y(y), z(z) {} };

Теперь, Вы создаете 3 объекта этой структуры:

Vector3 v1, v2, v3; //Инициализация v1(10, 10, 10); //...

И хотите прировнять объект v2 объекту v1, пишете:

V1 = v2;

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

Поэтому нам необходимо перегрузить оператор присваивания (=).

Общие сведения о перегрузке операторов

Для этого добавим в нашу структуру перегрузку:

Vector3 operator = (Vector3 v1) { return Vector3(this->x = v1.x, this->y = v1.y, this->z = 0); }

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

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

  • Первое, что мы можем сделать, это передавать в метод перегрузки не весь объект целиком, а ссылку на то место, где он хранится: //Передача объекта по ссылке (&v1) Vector3 operator = (Vector3 &v1) { return Vector3(this->x = v1.x, this->y = v1.y, this->z = 0); }

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

  • Но, если мы передаем объект по ссылке, то он становится изменяемым . То есть ничто не помешает нам при операции присваивания (v1 = v2) изменять не только значение v1, но еще и v2!
    Пример: //Изменение передаваемого объекта Vector3 operator = (Vector3 &v) { //Меняем объект, который справа от знака = v.x = 10; v.y = 50; //Возвращаем значение для объекта слева от знака = return Vector3(this->x = v.x, this->y = v.y, this->z = 0); }

    Разумеется, вряд ли кто-то в здравом уме станет производить такие не очевидные манипуляции. Но все же, не помешает исключить даже вероятность такого изменения.
    Для этого нам всего-лишь нужно добавить const перед принимаемым аргументом, таким образом мы укажем, что изнутри метода нельзя изменить этот объект.

    //Запрет изменения передаваемого объекта Vector3 operator = (const Vector3 &v) { //Не получится изменить объект, который справа от знака = //v.x = 10; v.y = 50; //Возвращаем значение для объекта слева от знака = return Vector3(this->x = v.x, this->y = v.y, this->z = 0); }

  • Теперь, давайте обратим наши взоры на тип возвращаемого значения . Метод перегрузки возвращает объект Vector3, то есть создается новый объект, что может приводить к таким же проблемам, которые я описал в самом первом пункте. И решение не будет отличаться оригинальностью, нам не нужно создавать новый объект - значит просто передаем ссылку на уже существующий. //Возвращается не объект, а ссылка на объект Vector3& operator = (const Vector3 &v) { return Vector3(this->x = v.x, this->y = v.y, this->z = 0); }

    Небольшое отступление о return:
    Когда я изучал перегрузки, то не понимал:

    //Зачем писать this->x = ... (что может приводить к ошибкам в бинарных операторах) return Vector3(this->x = v.x, this->y = v.y, this->z = 0); //Если мы все равно возвращаем объект с модифицированными данными? //Почему такая запись не будет работать? (Применительно к унарным операторам) return Vector3(v.x, v.y, 0);

    Дело в том, что все операции мы должны самостоятельно и явно указать в теле метода. Что значит, написать: this->x = v.x и т.д.
    Но для чего тогда return, что мы возвращаем? На самом деле return в этом примере играет достаточно формальную роль, мы вполне можем обойтись и без него:

    //Возвращается void (ничего) void operator = (const Vector3 &v1) { this->x = v1.x, this->y = v1.y, this->z = 0; }

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

    V1 = (v2 = v3); //Пример для void operator + //v1 = void? - Нельзя v1 = (v2 + v3);

    Т.к. ничего не возвращается, нельзя выполнить и присваивание.
    Либо же в случае со ссылкой , что получается аналогично void, возвращается ссылка на временный объект, который уже не будет существовать в момент его использования (сотрется после выполнения метода).
    Получается, что лучше возвращать объект а не ссылку? Не все так однозначно, и выбирать тип возвращаемого значения (объект или ссылка) необходимо в каждом конкретном случае. Но для большинства небольших объектов - лучше возвращать сам объект, чтобы мы имели возможность дальнейшей работы с результатом.

    Отступление 2 (как делать не нужно):
    Теперь, зная о разнице операции return и непосредственного выполнения операции, мы можем написать такой код:

    V1(10, 10, 10); v2(15, 15, 15); v3; v3 = (v1 + v2); cout << v1; // Не (10, 10, 10), а (12, 13, 14) cout << v2; // Не (15, 15, 15), а (50, 50, 50) cout << v3; // Не (25, 25, 25), а также, что угодно

    Для того, что бы реализовать этот ужас мы определим перегрузку таким образом:

    Vector3 operator + (Vector3 &v1, Vector3 &v2) { v1.x += 2, v1.y += 13, v1.z += 4; v2(50, 50, 50); return Vector3(/*также, что угодно*/); }

  • И когда мы перегружаем оператор присваивания, остается необходимость исключить попеременное присваивание в том редком случае, когда по какой-то причине объект присваивается сам себе: v1 = v1.
    Для этого добавим такое условие: Vector3 operator = (const Vector3 &v1) { //Если попытка сделать объект равным себе же, просто возвращаем указатель на него //(или можно выдать предупреждение/исключение) if (&v1 == this) return *this; return Vector3(this->x = v1.x, this->y = v1.y, this->z = v1.z); }

Отличия унарных и бинарных операторов

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

Vector3 operator + (const Vector3 &v1); // Унарный плюс Vector3 operator - (const Vector3 &v1); // Унарный минус //А так же: //++, --, !, ~, , *, &, (), (type), new, delete

Бинарные операторы - работают с 2-я объектами

Vector3 operator + (const Vector3 &v1, const Vector3 &v2); //Сложение - это НЕ унарный плюс! Vector3 operator - (const Vector3 &v1, const Vector3 &v2); //Вычитание - это НЕ унарный минус! //А так же: //*, /, %, ==, !=, >, <, >=, <=, &&, ||, &, |, ^, <<, >>, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, ->, ->*, (,), ","

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

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

Struct Vector3 { //Данные, конструкторы, ... //Объявляем о том, что в данной структуре перегружен оператор = Vector3 operator = (Vector3 &v1); }; //Реализуем перегрузку за пределами тела структуры //Для этого добавляем "Vector3::", что указывает на то, членом какой структуры является перегружаемый оператор //Первая надпись Vector3 - это тип возвращаемого значения Vector3 Vector3::operator = (Vector3 &v1); { return Vector3(this->x = v1.x, this->y = v1.y, this->z = 0); }

Зачем в перегрузке операторов дружественные функции (friend)?

Дружественные функции - это такие функции которые имеют доступ к приватным методам класса или структуры.
Предположим, что в нашей структуре Vector3, такие члены как x,y,z - являются приватными, тогда мы не сможем обратиться к ним за пределами тела структуры. Здесь то и помогают дружественные функции.
Единственное изменение, которое нам необходимо внести, - это добавить ключевое слово fried перед объявлением перегрузки:

Struct Vector3 { friend Vector3 operator = (Vector3 &v1); }; //За телом структуры пишем реализацию

Когда не обойтись без дружественных функций в перегрузке операторов?

1) Когда мы реализуем интерфейс (.h файл) в который помещаются только объявления методов, а реализация выносится в скрытый.dll файл
2) Когда операция производится над объектами разных классов. Пример:

Struct Vector2 { //Складываем Vector2 и Vector3 Vector2 operator + (Vector3 v3) {/*...*/} } //Объекту Vector2 присваиваем сумму объектов Vector2 и Vector3 vec2 = vec2 + vec3; //Ok vec2 = vec3 + vec2; //Ошибка

Ошибка произойдет по следующей причине, в структуре Vector2 мы перегрузили оператор +, который в качестве значения справа принимает тип Vector3, поэтому первый вариант работает. Но во втором случае, необходимо писать перегрузку уже для структуры Vector3, а не 2. Чтобы не лезть в реализацию класса Vector3, мы можем написать такую дружественную функцию:

Struct Vector2 { //Складываем Vector2 и Vector3 Vector2 operator + (Vector3 v3) {/*...*/} //Дружественность необходима для того, чтобы мы имели доступ к приватным членам класса Vector3 friend Vector2 operator + (Vector3 v3, Vector2 v2) {/*...*/} } vec2 = vec2 + vec3; //Ok vec2 = vec3 + vec2; //Ok

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

Пример перегрузки для бинарных +, -, *, /, %

Vector3 operator + (const Vector3 &v1, const Vector3 &v2) { return Vector3(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); }

Пример перегрузки для постфиксных форм инкремента и декремента (var++, var-- )

Vector3 Vector3::operator ++ (int) { return Vector3(this->x++, this->y++, this->z++); }

Пример перегрузки для префиксных форм инкремента и декремента (++var, --var )

Vector3 Vector3::operator ++ () { return Vector3(++this->x, ++this->y, ++this->z); }

Перегрузка арифметических операций с объектами других классов

Vector3 operator * (const Vector3 &v1, const int i) { return Vector3(v1.x * i, v1.y * i, v1.z * i); }

Перегрузка унарного плюса (+)

//Ничего не делает, просто возвращаем объект Vector3 operator + (const Vector3 &v) { return v; }

Перегрузка унарного минуса (-)

//Умножает объект на -1 Vector3 operator - (const Vector3 &v) { return Vector3(v.x * -1, v.y * -1, v.z * -1); }

Пример перегрузки операций составного присваивания +=, -=, *=, /=, %=

Vector3 operator += (const Vector3 &v1, const Vector3 &v2) { return Vector3(v1.x = v1.x + v2.x, v1.y = v1.y + v2.y, v1.z = v1.z + v2.z); }

Хороший пример перегрузки операторов сравнения ==, !=, >, <, >=, <=

Const bool operator < (const Vector3 &v1, const Vector3 &v2) { double vTemp1(sqrt(pow(v1.x, 2) + pow(v1.y, 2) + pow(v1.z, 2))); double vTemp2(sqrt(pow(v2.x, 2) + pow(v2.y, 2) + pow(v2.z, 2))); return vTemp1 < vTemp2; } const bool operator == (const Vector3 &v1, const Vector3 &v2) { if ((v1.x == v2.x) && (v1.y == v2.y) && (v1.z == v2.z)) return true; return false; } //Перегружаем!= используя другой перегруженный оператор const bool operator != (const Vector3 &v1, const Vector3 &v2) { return !(v1 == v2); }

Пример перегрузки операций приведения типов (type)

//Если вектор не нулевой - вернуть true Vector3::operator bool() { if (*this != Vector3(0, 0, 0)) return true; return false; } //При приведении к типу int - возвращать сумму всех переменных Vector3::operator int() { return int(this->x + this->y + this->z); }

Пример перегрузки логических операторов!, &&, ||

//Опять же, используем уже перегруженную операцию приведения типа к bool const bool operator ! (Vector3 &v1) { return !(bool)v1; } const bool operator && (Vector3 &v1, Vector3 &v2) { return (bool)v1 && (bool)v2; }

Пример перегрузки побитовых операторов ~, &, |, ^, <<, >>

//Операция побитовой инверсии (как умножение на -1, только немного иначе) const Vector3 operator ~ (Vector3 &v1) { return Vector3(~(v1.x), ~(v1.y), ~(v1.z)); } const Vector3 operator & (const Vector3 &v1, const Vector3 &v2) { return Vector3(v1.x & v2.x, v1.y & v2.y, v1.z & v2.z); } //Побитовое исключающее ИЛИ (xor) const Vector3 operator ^ (const Vector3 &v1, const Vector3 &v2) { return Vector3(v1.x ^ v2.x, v1.y ^ v2.y, v1.z ^ v2.z); } //Перегрузка операции вывода в поток ostream& operator << (ostream &s, const Vector3 &v) { s << "(" << v.x << ", " << v.y << ", " << v.z << ")"; return s; } //Перегрузка операции ввода из потока (очень удобный вариант) istream& operator >> (istream &s, Vector3 &v) { std::cout << "Введите Vector3.nX:"; std::cin >> v.x; std::cout << "nY:"; std::cin >> v.y; std::cout << "nZ:"; std::cin >> v.z; std::cout << endl; return s; }

Пример перегрузки побитного составного присваивания &=, |=, ^=, <<=, >>=

Vector3 operator ^= (Vector3 &v1, Vector3 &v2) { v1(Vector3(v1.x = v1.x ^ v2.x, v1.y = v1.y ^ v2.y, v1.z = v1.z ^ v2.z)); return v1; } //Предварительно очищаем поток ostream& operator <<= (ostream &s, Vector3 &v) { s.clear(); s << "(" << v.x << ", " << v.y << ", " << v.z << ")"; return s; }

Пример перегрузки операторов работы с указателями и членами класса , (), *, &, ->, ->*
Не вижу смысла перегружать (*, &, ->, ->*), поэтому примеров ниже не будет.

//Не делайте подобного! Такая перегрузка может ввести в заблуждение, это просто пример реализации //Аналогично можно сделать для () int Vector3::operator (int n) { try { if (n < 3) { if (n == 0) return this->x; if (n == 1) return this->y; if (n == 2) return this->z; } else throw "Ошибка: Выход за пределы размерности вектора"; } catch (char *str) { cerr << str << endl; } return NULL; } //Этот пример также не имеет практического смысла Vector3 Vector3::operator () (Vector3 &v1, Vector3 &v2) { return Vector3(v1 & v2); }

Как перегружать new и delete ? Примеры:

//Выделяем память под 1 объект void* Vector3::operator new(size_t v) { void *ptr = malloc(v); if (ptr == NULL) throw std::bad_alloc(); return ptr; } //Выделение памяти под несколько объектов void* Vector3::operator new(size_t v) { void *ptr = malloc(sizeof(Vector3) * v); if (ptr == NULL) throw std::bad_alloc(); return ptr; } void Vector3::operator delete(void* v) { free(v); } void Vector3::operator delete(void* v) { free(v); }

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

Перегрузка оператора запятая,
Внимание! Не стоит путать оператор запятой с знаком перечисления! (Vector3 var1, var2;)

Const Vector3 operator , (Vector3 &v1, Vector3 &v2) { return Vector3(v1 * v2); } v1 = (Vector3(10, 10, 10), Vector3(20, 25, 30)); // Вывод: (200, 250, 300)