Перегрузка бинарных и унарных операций
Следующим шагом в использовании класса как базового типа данных является переопределение операций языка, в которых один или несколько операндов могут быть объектами класса. Это достигается введением функции-элемента специального вида, обращение к которой компилятор формирует при трансляции такой операции. Естественно, что такая функция должна иметь результат, отличный от void, если предполагается использование этой операции внутри другого выражения. Переопределение операций осуществляется в рамках стандартного синтаксиса языка Си, то есть обозначение операций, их приоритеты и количество операндов остается неизменным. Можно описывать функции, определяющие значения следующих операций: + - * / % ^ & | ~! = < > += -= *= /= %= ^= &= (тип)|= << >> >>= <<= ==!= <= >=&& || ++ -- [] () new deleteПоследние четыре - это индексирование, вызов функции, выделение свободной памяти и освобождение свободной памяти. Изменить приоритеты перечисленных операций невозможно, как невозможно изменить и синтаксис выражений. Нельзя, например, определить унарную операцию % или бинарную!. Невозможно определить новые лексические символы операций, но в тех случаях, когда множество операций недостаточно, вы можете использовать запись вызова функции. Используйте например, не **, а pow(). Эти ограничения могут показаться драконовскими, но более гибкие правила могут очень легко привести к неоднозначностям. Например, на первый взгляд определение операции **, означающей возведение в степень, может показаться очевидной и простой задачей, но подумайте еще раз. Должна ли ** связываться влево (как в Фортране) или вправо (как в Алголе)? Выражение a**p должно интерпретироваться как a*(*p) или как (a)**(p)? Имя функции-операции есть ключевое слово operator (то есть, операция), за которым следует сама операция, например, operator<<. Функция-операция описывается и может вызываться так же, как любая другая функция. Использование операции - это лишь сокращенная запись явного вызова функции операции. Например: 1 void f(complex a, complex b)2 {3 complex c = a + b; // сокращенная запись4 complex d = operator+(a,b); // явный вызов оператора-друга5 c = a.operator+(b); // явный вызов оператора-члена6 }Для переопределения операции используется особая форма функции-элемента с заголовком такого вида: operator операция(список_параметров-операндов)Имя функции состоит из ключевого слова operator и символа данной операции в синтаксисе языка Си. Список формальных параметров функции соответствует списку операндов, определяя их типы и способы передачи. Результат функции (тип, способ передачи) является одновременно результатом переопределенной операции. Имеется два способа описания функции, соответствующей переопределяемой операции:
Бинарная операция может быть определена или как функция-член, получающая один параметр, или как функция-друг, получающая два параметра. Таким образом, для любой бинарной операции @ aa@bb может интерпретироваться или как aa.operator@(bb), или как operator@(aa,bb). Если определены обе, то aa@bb является ошибкой. Унарная операция, префиксная или постфиксная, может быть определена или как функция-член, не получающая параметров, или как функция-друг, получающая один параметр. Таким образом, для любой унарной операции @ aa@ или @aa может интерпретироваться или как aa.operator@(), или как operator@(aa). Если определена и то, и другое, то и aa@ и @aa являются ошибками. Рассмотрим следующие примеры: 01 class X {02 // друзья03 friend X operator-(X); // унарный минус04 friend X operator-(X,X); // бинарный минус05 friend X operator-(); // ошибка: нет операндов06 friend X operator-(X,X,X); // ошибка: тернарная07 // члены (с неявным первым параметром: this)08 X* operator&(); // унарное & (взятие адреса)09 X operator&(X); // бинарное & (операция И)10 X operator&(X,X); // ошибка: тернарное11 };Необходимо отметить также и тот факт, что для каждой комбинации типов операндов в переопределяемой операции необходимо ввести отдельную функцию, то есть транслятор не может производить перестановку операндов местами, даже если базовая операция допускает это. Например, при переопределении операции сложения объекта класса dat с целым необходимо две функции dat+int и int+dat. В качестве примера рассмотрим доопределение стандартных операций над датами: 01 /*********************************************************02 * dat.h *03 *********************************************************/04 class dat 05 {06 int day,month,year;07 public:08 void next(); // Элемент-функция вычисления следующего дня09 dat operator++(); // Операция ++10 dat operator+(int);// Операция "дата + целое" с передачей11 // первого операнда через this12 friend dat operator+(int,dat);// Операция с явной передачей всех 13 // аргументов по значению14 dat(int=0,int=0,int=0);15 dat(char *); //16 ~dat(); //17 }; //01 /*********************************************************02 * main.cpp *03 *********************************************************/04 #include "dat.h"05 void main()06 {07 int i;08 dat a, b(17,12,1990), c(12,7), d(3), e;09 dat *p = new dat[10];10 e = ++a; // следующий день11 d = b+14; // через 2 недели12 for (i=0; i<10; i++) p[i] = i + с; // календарь на декаду13 delete[] p;14 }01 /*********************************************************02 * dat.cpp *03 *********************************************************/04 #include "dat.h"05 static int days[]={ 0,31,28,31,30,31,30,31,31,30,31,30,31};06 //------ Функция вычисления следующего дня -----------------07 // Используется ссылка на текущий объект this,08 // который изменяется в процессе операции09 void dat::next()10 {11 day++;12 if (day > days[month])13 {14 if ((month==2) && (day==29) && (year%4==0)) return;15 day=1; month++;16 if (month==13)17 {18 month=1; year++;19 }20 }21 }22 //------ Операция инкремента даты -------------------------23 // 1. Первый операнд по указателю this24 // 2. Возвращает копию входного объекта (операнда)25 // до увеличения26 // 3. Соответствует операции dat++ (увеличение после27 // использования)28 // 4. Замечание: для унарных операций типа -- или ++29 // использование их до или после операнда не имеет30 // значения (вызывается одна и та же функция).31 dat dat::operator++()32 { // Создается временный объект33 dat x = *this; // В него копируется текущий объект34 next(); // Увеличивается значение текущего объекта35 return(x); // Возвращается временный объект по36 } // значению37 //------ Операция "дата + целое" --------------------------38 // 1. Первый операнд по указателю this39 // 2. Входной объект не меняется, результат возвращается40 // в виде значения автоматического объекта x41 dat dat::operator+(int n)42 {43 dat x;44 x = *this; // Копирование текущего объекта в x45 while (n--!=0) x.next();// Вызов функции next для объекта x46 return(x); // Возврат объекта x по значению47 }48 //------ Операция "целое + дата" -------------------------49 // 1. Дружественная функция с полным списком операндов50 // 2. Второй операнд класса dat - передается по значению,51 // поэтому может модифицироваться без изменения исходного52 // объекта53 dat operator+(int n, dat p)54 {55 while (n--!=0) p.next(); // Вызов функции next для p56 return(p); // Возврат копии объекта p57 }Для многих переопределяемых операций тип результата совпадает с типом одного из операндов. Это позволяет выполнить подряд несколько операций в одном выражении. Возможны различные варианты реализации в соответствии со способами передачи параметров и результата: по значению или по ссылке. Отметим наиболее важные из них: 01 //------ Операция "дата + целое" --------------------------02 // 1. Функция с неявным первым операндом по указателю this03 // 2. Меняется значение текущего объекта04 // 3. Результат - ссылка на текущий объект05 dat& dat::operator+(int n)06 {07 while (n--!=0) next(); // Вызов next с текущим объектом08 return(*this); // Возврат ссылки на объект this09 }10 //------ Операция "дата + целое" -------------------------11 // 1. Дружественная функция с полным списком аргументов12 // 2. Первый операнд класса dat - ссылка, меняется при13 // выполнении операции14 // 3. Результат - ссылка на операнд15 dat& operator+(dat& p,int n)16 {17 while (n--!=0) p.next(); // Вызов next для объекта p,18 // заданного ссылкой19 return(p); // Возврат ссылки на p20 }21 //----- Операция "целое + дата" --------------------------22 // 1. Дружественная функция с полным списком аргументов23 // 2. Второй операнд класса dat - ссылка, меняется при24 // выполнении операции25 // 3. Результат - ссылка на операнд26 dat& operator+(int n, dat& p)27 {28 while (n--!=0) p.next(); // Вызов next для объекта p,29 // заданного ссылкой30 return(p); // Возврат ссылки на p31 }32 //--------------------------------------------------------33 void main()34 {35 dat a, b; // "Арифметические" эквиваленты36 a + 2 + 3; // a = a + 2; a = a + 3;37 5 + b + 4; // b = 5 + b; b = b + 4;38 }Во всех трех случаях ссылка на операнд - объект класса возвращается в качестве результата. Все действия, выполняемые операцией, реализуются в том же объекте, который "накапливает" результат. Естественный "арифметический" вид переопределяемых операций получается, когда результат возвращается по значению не в виде ссылки, а в виде объекта: 01 //------ Операция "дата + целое" --------------------------02 // 1. Функция с неявным первым операндом по указателю this03 // 2. Изменяется автоматический объект - копия операнда04 // 3. Результат - значение автоматического объекта05 dat dat::operator+(int n)06 {07 dat tmp = *this; // Объект - копия операнда08 while (n--!=0) tmp.next();// Вызов next с объектом tmp09 return(tmp); // Возврат значения объекта tmp10 }11 //------ Операция "дата + целое" -------------------------12 // 1. Дружественная функция с полным списком аргументов13 // 2. Первый параметр класса dat передается по значению,14 // является копией первого операнда и меняется при15 // выполнении операции16 // 3. Результат - значение формального параметра17 dat operator+(dat p,int n)18 {19 while (n--!=0) p.next(); // Вызов next для объекта p,20 // копии операнда21 return(p); // Возврат значения22 } // формального параметраВо втором случае, когда формальный параметр - операнд передается по значению, он является отдельным объектом, в который копируется объект - фактический параметр. Поэтому его изменение не затрагивает входного операнда. Кроме того, в обоих случаях при возвращении объекта в качестве результата транслятор создает в вызывающей функции временный объект, в который копируется содержимое объекта-результата в операторе return. Дополнительная проблема для таких объектов заключается в их корректном конструировании. При отсутствии переопределения операции присваивания производится побайтное копирование объектов. Такая интерпретация операции присваивания некорректна, если объект имеет указатели на динамические переменные или массивы, идентификаторы связанных ресурсов и т.д. При копировании таких объектов необходимо сначала уничтожить связанные динамические переменные и ресурсы левого операнда, а затем заново резервировать, но уже с параметрами, необходимыми для интерпретации операции присваивания: 00 #include <string.h>01 class string02 {03 char *Str;04 int size;05 public:06 string& operator=(string&);07 };08 string& string::operator=(string& right)09 {10 if(Str!=NULL) delete Str;// Освободить динамическую память11 size = right.size; // Резервировать память12 Str = new char[size];13 strncpy(Str,right->Str,size); // Копировать строки14 return *this;15 }
|