Лекция 12. Совместное использование. Перегрузка операций

1. Совместное использование

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

double abs(double x); int abs(int x); abs(1); abs(1.0); // Вызов int abs(int x) // Вызов 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]; } ... };

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

  1. точное соответствие типов, т.е. полное соответствие или соответствие, достигаемое тривиальными преобразованиями типов (например, имя массива и указатель, имя функции и указатель на функцию, типы T и const T);
  2. соответствие, достигаемое «продвижением» интегральных типов (например, bool в int, char в int, short в int) и float в double;
  3. соответствие, достигаемое путем стандартных преобразований (например, int в double, double в int, double в long double, указателей на производные типы в указатели на базовые, указателей на произвольные типы в void*, int в unsigned int);
  4. соответствие, достигаемое при помощи преобразований, определяемых пользователем;
  5. соответствие за счет многоточий в объявлении функции.

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

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

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

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

void f(int); void g() { void f(double);
f(1); } // Вызов f(double), хотя 1 – целая константа

2. Перегрузка операций

В каждой технической области – и в большинстве не технических – имеются свои стандартные обозначения, облегчающие представление и обсуждение часто встречающихся концепций. Например, благодаря постоянному использованию, выражение x + y * z яснее для нас, чем фраза умножить y на z и прибавить результат к x. Трудно переоценить значение краткой и выразительной формы записи типичных операций.

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

Большинство операций языка С могут использоваться совместно (быть перегружены). Для этого в объявлении класса необходимо объявить функцию, имеющую следующее имя:
<тип> operator <операция> (<операнды>)

Не могут использоваться совместно операции   .   .*   ::   ?:

И унарная, и бинарная формы операций   +   –   *   &   могут использоваться совместно.

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

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

Недопустимо и невозможно менять старшинство, ассоциативность и число операндов у операции.

Совместно используемая операция не может иметь параметров с умолчаниями.

По поводу определяемых пользователем операций делается всего несколько предположений. В частности, operator=, operator(), operator[] и operator-> должны быть нестатическими функциями-членами класса – это гарантирует, что их первый операнд будет l-значением . Второй параметр (индекс) функции operator[] может быть любого типа. Это делает возможным определение векторов, ассоциативных массивов и т.д.

Тождества, верные для операций над основными типами (например, ++а ~ а += 1), не обязаны выполняться в отношении операций над «классовыми» типами. Такая связь не сохраняется в операциях, определяемых пользователем, если только пользователь не позаботиться об этом сам. Компилятор не сгенерирует определение X::operator+= из определений X::operator+ и X::operator=.

По историческим причинам операторы = (присваивание), & (взятие адреса) и , (последовательность) имеют предопределённый смысл, когда применяются к объектами класса. Этот предопределённый смысл может стать недоступным, если сделать операторы приватными.

class X { private: void operator =(const X&); void operator &(); void operator ,(const X&); ... };
void f(X a, X b) { a = b; &a; a, b; } // Ошибка – operator= является приватным // Ошибка – operator& является приватным // Ошибка – 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; } void main() { Complex a(0, 0), b(2, 2), c; ++a; a++; c = a + b; c = a + 2.5; c = a + ++b; } // Все операции объявляются как члены класса // Префиксная операция ++ // Постфиксная операция ++ // Объект класса надо возвращать, чтобы было возможно вкладывать вызов операций ++ // (как префиксной, так и постфиксной) в другие операторы // Префиксная операция ++ возвращает новое значение операнда, // в то время, как постфиксная операция ++ возвращает старое значение операнда // Эквивалентно a.operator++() // Эквивалентно a.operator++(0) // Эквивалентно a.operator+(b)

Аргументы операций были определены как константные ссылки. Это позволяет пользоваться выражениями, включающими в себя обычные арифметические операторы, без интенсивного копирования. Для класса Complex это, возможно, не даёт значительного выигрыша во времени, но для больших объектов дело обстоит иначе. Указателями в качестве аргументов нельзя воспользоваться потому, что невозможно заместить смысл оператора, применяемого к указателю.

Возвращение ссылки может также показаться эффективным решением.

Complex& operator + (const Complex& c);

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

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

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

class Complex { private: double r, m; public: Complex(double nr = 0, double nm = 0) : r(nr), m(nm) { }
Complex operator + (const Complex& c); // Требует доступа к представлению
}; Complex Complex::operator += (const Complex& c) { r += c.r; m += c.m; return *this; } Complex operator + (const Complex&c1, const Complex& c2) { Complex x = c1;
return x += c2; } // Доступ к представлению при помощи +=
void main() { Complex a(0, 0), b(2, 2), c(7, -5); Complex r1 = a + b + c; Complex r2 = a; r2 += b; r2 += c; } // r1 = operator+(a, operator+(b, c)) // r2 = a // r2.operator+=(b) // r2.operator+=(c)

За исключением разницы во времени вычисления r1 и r2 эквивалентны.

Составные операторы присваивания, такие как += и *=, обычно легче определить, чем их «простые» части + и *. Это сначала удивляет, но данный вывод следует из того факта, что в операции + участвуют три объекта (два операнда и результат), а в операции += – только два. В последнем случае в результате избавления от временных переменных улучшается производительность во время выполнения. Кроме того, такие функции (в простых случаях) легко делаются встраиваемыми.

3. Пример

Разрабатываемый класс Vector представляет собой вектор с переменным числом элементов. Над векторами определены операции присваивание, сложение, вычитание, скалярное произведение и сравнение. Для удобства доступа к элементам вектора также определена операция «индексное выражение».

#include <malloc.h> class Vector { private: int size; double *v; public: explicit Vector(int n = 0); Vector(const Vector& vector); ~Vector(); int GetSize() const { return size; } int SetSize(int n); Vector& operator = (const Vector& vector); double& operator [](int n); Vector operator - () 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; }; // Размер вектора // Адрес массива для вектора // Конструктор умолчания // Конструктор копирования // Деструктор // Получение размера вектора // Изменение размера вектора // Операция присваивания // Индексное выражение // Унарный минус // Сравнение // Сложение // Составное присваивание // Вычитание // Составное присваивание // Сложение с числом // Составное присваивание // Вычитание числа // Составное присваивание // Скалярное произведение
Vector::Vector(int n) { if (n < 0) n = 0; size = n; v = NULL; if (size) if ((v = (double *)malloc(size * sizeof(double))) == NULL) size = 0; } Vector::Vector(const Vector& vector) { size = vector.size; v = NULL; if (size) if ((v = (double *)malloc(size * sizeof(double))) == NULL) size = 0; else for (int i = 0; i < size; i++) *(v + i) = vector[i]; } Vector::~Vector() { if (v) free(v); } int Vector::SetSize(int n) { if (n < 0) n = 0; size = n; if (size) if ((v = (double *)realloc(v, size * sizeof(double))) == NULL) size = 0; return size; // Равенство 0 будет признаком ошибки }
Vector& Vector::operator = (const Vector& vector) { if (this == &vector) // Проверка на самоприсваивание return *this; size = vector.size; if (size)
if ((v = (double *)realloc(v, size * sizeof(double))) == NULL) // Здесь очистка ресурсов осуществляется за счёт использования функции realloc
size = 0; else for (int i = 0; i < size; i++) *(v + i) = vector[i]; return *this; // Возвращаем присвоенное значение } double& Vector::operator [] (int n) { if (n < 0) n = 0; if (n >= size) n = size - 1; // Результатом операции «индексное выражение» объявлена ссылка для того, return *(this->v + n); // чтобы можно было не только получать значение элемента вектора, но и изменять его }
Vector Vector::operator - () const { Vector res(size); for (int i = 0; i < size; i++) *(res.v + i) = -*(this->v + i); return res; } int Vector::operator == (const Vector& vector) const { if (size != vector.size) return 0; for (int i = 0; i < size; i++) if (*(this->v + i) != *(vector.v + i)) return 0; return 1; } Vector Vector::operator + (const Vector& vector) const { Vector res(size); if (size != vector.size) return res; for (int i = 0; i < size; i++) *(res.v + i) = *(this->v + i) + *(vector.v + i); return res; } Vector& Vector::operator += (const Vector& vector) { if (size != vector.size) return *this; for (int i = 0; i < size; i++) *(this->v + i) += *(vector.v + i); return *this; } Vector Vector::operator - (const Vector& vector) const { Vector res(size); if (size != vector.size) return res; for (int i = 0; i < size; i++) *(res.v + i) = *(this->v + i) - *(vector.v + i); return res; } Vector& Vector::operator -= (const Vector& vector) { if (size != vector.size) return *this; for (int i = 0; i < size; i++) *(this->v + i) -= *(vector.v + i); return *this; } Vector Vector::operator + (double value) const { Vector res(size); for (int i = 0; i < size; i++) *(res.v + i) = *(this->v + i) + value; return res; } Vector& Vector::operator += (double value) { for (int i = 0; i < size; i++) *(this->v + i) += value; return *this; } Vector Vector::operator - (double value) const { Vector res(size); for (int i = 0; i < size; i++) *(res.v + i) = *(this->v + i) - value; return res; } Vector& Vector::operator -= (double value) { for (int i = 0; i < size; i++) *(this->v + 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 += *(this->v + i) * *(vector.v + i); return res; } void main() { Vector v1(3), v2, v3(2), v4; int i; double r; v2.SetSize(3); for (i = 0; i < v1.GetSize(); i++) // Задаём элементы векторов v1 и v2 { v1[i] = i + 1; v2[i] = i + 5; } v3 = v1 + v2; v4 = v1 - v2 + v3; v4 = -v4; v3 += v1; r = v1 * v2; v4 = v1 * v2 + v3; // Т.к. нельзя изменить приоритет операций, сначала должна выполняться операция *. // Но поскольку операция сложения числа с объектом класса не определена, // выражение вычислить невозможно v4 = v1 + v2 * v3; // Правильно }