Если в некоторой области действия имеется несколько различных объявлений функций с одним именем, это имя называется совместно используемым. Когда употребляется такое имя, нужная функция выбирается путём сравнения типов фактических параметров с типами формальных параметров.
double abs(double x);
int abs(int x);
abs(1); // Вызов int abs(int x)
abs(1.0); // Вызов double abs(double x)
Поскольку для всякого типа Т типы Т и Т& допускают совпадающие множества инициализирующих значений, функции с типами параметров, различающимися только в этом отношении, не могут иметь одно имя.
Функции, которые различаются только возвращаемым типом, не могут иметь одно имя.
Различные версии совместно используемой функции-члена класса могут предоставлять различные права доступа.
class Buffer
{ private:
char *p;
int size;
protected:
Buffer(int s, char *np) { size = s; p = np; }
public:
Buffer(int s) { p = new char[size = s]; }
...
};
Процесс поиска подходящей функции из множества перегруженных заключается в нахождении наилучшего соответствия типов формальных и фактических аргументов. Это осуществляется путем проверки набора критериев в следующем порядке:
Если соответствие может быть получено двумя способами на одном и том же уровне критериев, вызов считается неоднозначным и отвергается.
Результат перегрузки не зависит от порядка объявления функций.
Альтернативой перегрузке является использование нескольких функций с различными именами и разными типами аргументов. При этом приходится помнить несколько имен и то, как их правильно использовать. Кроме того, при отсутствии перегрузки к аргументам функций применяются все стандартные преобразования, что также может привести к дополнительным ошибкам.
Функции, объявленные в различных областях видимости (не пространствах имён), не являются перегруженными.
void f(int);
void g()
{ void f(double);
f(1); // Вызов f(double), хотя 1 – целая константа
}
В каждой технической области – и в большинстве не технических – имеются свои стандартные обозначения, облегчающие представление и обсуждение часто встречающихся концепций. Например, благодаря постоянному использованию, выражение x + y * z яснее для нас, чем фраза умножить y на z и прибавить результат к x. Трудно переоценить значение краткой и выразительной формы записи типичных операций.
Как и большинство других языков C++ поддерживает набор операций для встроенных типов. Однако большинство концепций, для которых обычно используются операторы, не являются встроенными типами языка C++, поэтому они должны быть представлены в виде типов, определяемых пользователем. Определение (или перегрузка) операций для таких классов позволяет программисту реализовать более привычную и удобную форму записи для манипулирования объектами, чем та, которая доступна с использованием только базовой функциональной формы записи.
Большинство операций языка C могут использоваться совместно (быть перегружены). Для этого в объявлении класса необходимо объявить следующую функцию: <тип> operator <операция> (<операнды>)
Не могут использоваться совместно операции . .* :: ?:
И унарная, и бинарная формы операций + – * & могут использоваться совместно.
Функция-оператор должна либо быть функцией-членом класса, либо иметь хотя бы один параметр некоторого класса или ссылки на класс. Префиксная унарная операция может быть объявлена как нестатическая функция-член класса без параметров либо как функция, не являющаяся членом класса, с одним параметром. Бинарная операция может быть объявлена либо как нестатическая функция-член с одним параметром, либо как обычная функция (не член класса) с двумя параметрами.
Для отличия префиксного и постфиксного вариантов операции функция, реализующая постфиксный вариант, объявляется с дополнительным параметром типа int.
Недопустимо и невозможно менять старшинство, ассоциативность и число операндов у операции.
Совместно используемая операция не может иметь параметров с умолчаниями.
По поводу определяемых пользователем операций делается всего несколько предположений. В частности, operator=, operator(), operator[] и operator-> должны быть нестатическими функциями-членами класса – это гарантирует, что их первый операнд будет l-значением. Второй параметр (индекс) функции operator[] может быть любого типа. Это делает возможным определение векторов, ассоциативных массивов и т.д.
Тождества, верные для операций над основными типами (например, ++а ~ а += 1), не обязаны выполняться в отношении операций над «классовыми» типами. Такая связь не сохраняется в операциях, определяемых пользователем, если только пользователь не позаботиться об этом сам. Компилятор не сгенерирует определение X::operator+= из определений X::operator+ и X::operator=.
По историческим причинам операторы = (присваивание), & (взятие адреса) и , (последовательность) имеют предопределённый смысл, когда применяются к объектами класса. Этот предопределённый смысл может стать недоступным, если сделать операторы приватными. С другой стороны, операторам можно придать новый смысл, задав соответствующие определения.
Рассматрим пример определения и использования перегруженных операторов.
class Complex
{ private:
double r, m;
public:
Complex(double nr = 0, double nm = 0) : r(nr), m(nm) { }
// Все операции объявляются как члены класса
Complex& operator ++(); // Префиксная операция ++
Complex operator ++(int); // Постфиксная операция ++
Complex operator + (const Complex& c) const;
Complex& operator +=(const Complex& c);
bool operator ==(const Complex& c) const;
};
Complex& Complex::operator ++() // Объект класса надо возвращать, чтобы было возможно вкладывать вызов операций ++
{ ++r; return *this; } // (как префиксной, так и постфиксной) в другие операторы
Complex Complex::operator ++(int) // Префиксная операция ++ возвращает новое значение операнда,
{ Complex x = *this; // в то время, как постфиксная операция ++ возвращает старое значение операнда
r++;
return x;
}
Complex Complex::operator + (const Complex& c) const
{ return Complex(r + c.r, m + c.m); }
Complex& Complex::operator +=(const Complex& c)
{ r += c.r; m += c.m; return *this; }
bool Complex::operator ==(const Complex& c) const
{ return r == c.r && m == c.m; }
int main()
{ Complex a(0, 0), b(2, 2), c;
++a; // Эквивалентно a.operator++()
a++; // Эквивалентно a.operator++(0)
c = a + b; // Эквивалентно a.operator+(b)
c = a + 2.5;
c = a + ++b;
}
Параметры операций были определены как константные ссылки. Это позволяет пользоваться выражениями, включающими в себя обычные арифметические операторы, без интенсивного копирования. Для класса Complex это, возможно, не даёт значительного выигрыша во времени, но для больших объектов дело обстоит иначе. Указателями в качестве аргументов нельзя воспользоваться потому, что невозможно заместить смысл оператора, применяемого к указателю.
Возвращение ссылки может также показаться эффективным решением.
Complex& operator + (const Complex& c);
Это допустимо, но вызывает проблемы при выделении памяти. Так как ссылка на результат будет выдана вызываемой функций как ссылка на значение, возвращаемое из функции, значение не может быть автоматической переменной. Так как оператор может использоваться более одного раза в выражении результат не может быть статической локальной переменной. Результат, как правило, будет размещаться в свободной памяти. Копирование возвращаемого значения часто обходится дешевле (в смысле времени выполнения, размера кода и данных), чем выделение и освобождение памяти под объект. Кроме того, это легче запрограммировать.
Однако существуют операции, которые возвращают тот объект, к которому была применена функция-член класса. К ним относятся операции, изменяющие свой параметр, а именно: операции префиксного инкремента и декремента, а также простое и составные присваивания. В этом случае можно возвращать ссылку, т.к. получается ссылка на объект, который был создан в вызывающей функции, и он, в отличие от локальных переменных функции, не будет уничтожаться при завершении функции.
В языке С++ возможно реализовать присваивание копированием и присваивание перемещением.
Операцию присваивания копированием необходимо реализовывать в тех же случаях, что и конструктор копирования, т.е. когда объект класса получает дополнительные ресурсы при создании. Операция присваивания, в принципе, выполняет те же действия, что и конструктор копирования, но есть три важных отличия.
Операция присваивания перемещением решает те же задачи, что и конструктор перемещения. Присваивание перемещением, также, как и присваивание копированием, должно осуществлять проверку на самоприсваивание и очистку ресурсов и возвращать ссылку на присвоенное значение. Копирование же заменяется перемещением.
Разрабатываемый класс Vector представляет собой вектор с переменным числом элементов. Над векторами определены операции присваивание, сложение, вычитание, скалярное произведение и сравнение. Для удобства доступа к элементам вектора также определена операция «индексное выражение».
#include <iostream>
#include <iomanip>
using namespace std;
class Vector
{ private:
int size; // Размер вектора
double *array; // Адрес массива для вектора
public:
explicit Vector(int size = 0); // Конструктор умолчания
Vector(const Vector& vector); // Конструктор копирования
Vector(Vector&& vector) noexcept; // Конструктор перемещения
~Vector(); // Деструктор
int Size() const { return size; } // Получение размера вектора
Vector& operator = (const Vector& vector); // Присваивание копированием
Vector& operator = (Vector&& vector) noexcept; // Присваивание перемещением
double& operator [] (int index); // Индексное выражение
double operator [] (int index) const; // Индексное выражение
Vector operator - () const; // Унарный минус
int operator == (const Vector& vector) const; // Сравнение
int operator != (const Vector& vector) const; // Сравнение
Vector operator + (const Vector& vector) const; // Сложение
Vector& operator += (const Vector& vector); // Составное присваивание
Vector operator - (const Vector& vector) const; // Вычитание
Vector& operator -= (const Vector& vector); // Составное присваивание
Vector operator + (double value) const; // Сложение с числом
Vector& operator += (double value); // Составное присваивание
Vector operator - (double value) const; // Вычитание числа
Vector& operator -= (double value); // Составное присваивание
double operator * (const Vector& vector) const; // Скалярное произведение
friend istream& operator >> (istream& f, Vector& vector); // Потоковый ввод
friend ostream& operator << (ostream& f, const Vector& vector); // Потоковый вывод
};
class IncorrectIndex { }; // Класс для исключения
Vector::Vector(int size /* = 0 */) : size(size)
{ array = nullptr;
if (size)
array = new double [size];
}
Vector::Vector(const Vector& vector)
{ size = vector.size;
array = nullptr;
if (size)
{ array = new double [size];
for (int i = 0; i < size; i++)
array[i] = vector.array[i];
}
}
Vector::Vector(Vector&& vector) noexcept
{ size = vector.size;
array = vector.array;
vector.size = 0;
vector.array = nullptr;
}
Vector::~Vector()
{ if (array) delete[] array; }
Vector& Vector::operator = (const Vector& vector)
{ if (this == &vector) return *this; // Самопроверка
if (array) delete[] array; // Очистка ресурсов
size = vector.size;
array = nullptr;
if (size)
{ array = new double [size];
for (int i = 0; i < size; i++)
array[i] = vector.array[i];
}
return *this; // Возвращаем присвоенное значение
}
Vector& Vector::operator = (Vector&& vector) noexcept
{ if (this == &vector) return *this; // Самопроверка
if (array) delete[] array; // Очистка ресурсов
size = vector.size;
array = vector.array;
vector.size = 0;
vector.array = nullptr;
return *this; // Возвращаем присвоенное значение
}
double& Vector::operator [] (int index) // Результатом операции «индексное выражение» объявлена ссылка
{ if (index < 0 || index >= size) // для того, чтобы можно было не только получать значение элемента вектора,
throw IncorrectIndex(); // но и изменять его
return array[index];
}
double Vector::operator [] (int index) const // В случае константного объекта
{ if (index < 0 || index >= size) // возвращаем значение элемента вектора,
throw IncorrectIndex(); // изменить элемент вектора будет невозможно
return array[index];
}
Vector Vector::operator - () const
{ Vector res(size);
for (int i = 0; i < size; i++)
res.array[i] = -array[i];
return res;
}
int Vector::operator == (const Vector& vector) const
{ if (size != vector.size) return 0;
for (int i = 0; i < size; i++)
if (array[i] != vector.array[i])
return 0;
return 1;
}
int Vector::operator != (const Vector& vector) const
{ if (size != vector.size) return 1;
for (int i = 0; i < size; i++)
if (array[i] != vector.array[i])
return 1;
return 0;
}
Vector Vector::operator + (const Vector& vector) const
{ Vector res(size);
if (size != vector.size) return res;
res = *this;
return res += vector;
}
Vector& Vector::operator += (const Vector& vector)
{ if (size != vector.size) return *this;
for (int i = 0; i < size; i++)
array[i] += vector.array[i];
return *this;
}
Vector Vector::operator - (const Vector& vector) const
{ Vector res(size);
if (size != vector.size) return res;
res = *this;
return res -= vector;
}
Vector& Vector::operator -= (const Vector& vector)
{ if (size != vector.size) return *this;
for (int i = 0; i < size; i++)
array[i] -= vector.array[i];
return *this;
}
Vector Vector::operator + (double value) const
{ Vector res = *this;
return res += value;
}
Vector& Vector::operator += (double value)
{ for (int i = 0; i < size; i++)
array[i] += value;
return *this;
}
Vector Vector::operator - (double value) const
{ Vector res = *this;
return res -= value;
}
Vector& Vector::operator -= (double value)
{ for (int i = 0; i < size; i++)
array[i] -= value;
return *this;
}
double Vector::operator * (const Vector& vector) const
{ double res = 0;
if (size != vector.size) return res;
for (int i = 0; i < size; i++)
res += array[i] * vector.array[i];
return res;
}
istream& operator >> (istream& f, Vector& vector)
{ for (int i = 0; i < vector.size; i++)
f >> vector.array[i];
return f;
}
ostream& operator << (ostream& f, const Vector& vector)
{ streamsize s = f.width();
for (int i = 0; i < vector.size; i++)
f << setw(s) << vector.array[i] << ' ';
f << endl;
return f;
}
int main()
{ Vector v1(3), v2(3), v3(2), v4;
double r;
cin >> v1 >> v2;
v3 = v1 + v2;
v4 = v1 - v2 + v3;
cout << v3 << v4;
v4 = -v4;
cout << v4;
v3 -= v2;
v4 += v2;
cout << v3 << v4;
r = v1 * v2;
cout << r << endl;
v4 = v1 * v2 + v3; // Ошибка – double + Complex
v4 = v1 + v2 * v3;
cout << v4;
}