Опубликован: 10.10.2006 | Уровень: специалист | Доступ: платный
Лекция 7:

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

Аннотация: Лекция содержит описание механизма перегрузки операций в С++. Программист может задать интерпретацию операций, когда они применяются к объектам определенного класса. Помимо арифметических, логических и операций отношения можно переопределить вызов функций (), индексацию [], косвенное обращение ->, а также присваивание и инициализацию. Можно определить явные и скрытые преобразования между пользовательскими и основными типами. Показано, как определить класс, объект которого можно копировать и уничтожать только с помощью специальных, определенных пользователем функций.

7.1 Введение

Если я выбираю слово, оно значит только то, что я решу, ни больше и ни меньше.

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

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

class complex {
   double re, im;
 public:
   complex(double r, double i) { re=r; im=i; }
   friend complex operator+(complex, complex);
   friend complex operator*(complex, complex);
};

Здесь приведена простая реализация понятия комплексного числа, когда оно представлено парой чисел с плавающей точкой двойной точности, с которыми можно оперировать только с помощью операций + и *.

Интерпретацию этих операций задает программист в определениях функций с именами operator+ и operator*. Так, если b и c имеют тип complex, то b+c означает (по определению) operator+(b,c). Теперь можно приблизиться к привычной записи комплексных выражений:

void f()
 {
   complex a = complex(1,3.1);
   complex b = complex(1.2,2);
   complex c = b;

   a = b+c;
   b = b+c*a;
   c = a*b+complex(1,2);
 }

Сохраняются обычные приоритеты операций, поэтому второе выражение выполняется как b=b+(c*a), а не как b=(b+c)*a.

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

Можно описать функции, определяющие интерпретацию следующих операций:

+    -    *    /    %    ^    &    |    ~    !
=    <    >    +=   -=   *=   /=   %=   ^=   &=
|=   <<   >>   >>=  <<=  ==   !=   <=   >=   &&
||   ++   --   ->*  ,    ->   []   ()   new  delete

Последние пять операций означают: косвенное обращение ( \S 7.9), индексацию ( \S 7.7), вызов функции ( \S 7.8), размещение в свободной памяти и освобождение ( \S 3.2.6). Нельзя изменить приоритеты этих операций, равно как и синтаксические правила для выражений. Так, нельзя определить унарную операцию %, также как и бинарную операцию !. Нельзя ввести новые лексемы для обозначения операций, но если набор операций вас не устраивает, можно воспользоваться привычным обозначением вызова функции. Поэтому используйте pow(), а не **.

Эти ограничения можно счесть драконовскими, но более свободные правила легко приводят к неоднозначности. Допустим, мы определим операцию ** как возведение в степень, что на первый взгляд кажется очевидной и простой задачей. Но если как следует подумать, то возникают вопросы: должны ли операции ** выполняться слева направо (как в Фортране) или справа налево (как в Алголе)? Как интерпретировать выражение a**p как a*(*p) или как (a)**(p)?

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

void f(complex a, complex b)
{
  complex c = a + b;           // краткая форма
  complex d = operator+(a,b);  // явный вызов
}

С учетом приведенного описания типа complex инициализаторы в этом примере являются эквивалентными.

7.2.1 Бинарные и унарные операции

Бинарную операцию можно определить как функцию-член с одним параметром, или как глобальную функцию с двумя параметрами. Значит, для любой бинарной операции @ выражение aa @ bb интерпретируется либо как aa.operator@(bb), либо как operator@(aa,bb). Если определены обе функции, то выбор интерпретации происходит по правилам сопоставления параметров (\S 4.13.2). Префиксная или постфиксная унарная операция может определяться как функция-член без параметров, или как глобальная функция с одними параметром. Для любой префиксной унарной операции @ выражение @aa интерпретируется либо как aa.operator@(), либо как operator@(aa). Если определены обе функции, то выбор интерпретации происходит по правилам сопоставления параметров (\S 4.13.2). Для любой постфиксной унарной операции @ выражение aa@ интерпретируется либо как aa.operator@(int), либо как operator@(aa,int). Подробно это объясняется в \S 7.10. Если определены обе функции, то выбор интерпретации происходит по правилам сопоставления параметров (\S 13.2). Операцию можно определить только в соответствии с синтаксическими правилами, имеющимися для нее в грамматике С++.

В частности, нельзя определить % как унарную операцию, а + как тернарную. Проиллюстрируем сказанное примерами:

class X {
  // члены (неявно используется указатель `this'):

  X* operator&();        // префиксная унарная операция &
                         // (взятие адреса)
  X operator&(X);        // бинарная операция & (И поразрядное)
  X operator++(int);     // постфиксный инкремент
  X operator&(X,X);      // ошибка: & не может быть тернарной
  X operator/();         // ошибка: / не может быть унарной
};

                    // глобальные функции (обычно друзья)

X operator-(X);          // префиксный унарный минус
X operator-(X,X);        // бинарный минус
X operator--(X&,int);    // постфиксный декремент
X operator-();           // ошибка: нет операнда
X operator-(X,X,X);      // ошибка: тернарная операция
X operator%(X);          // ошибка: унарная операция %

Операция [] описывается в \S 7.7, операция () в \S 7.8, операция -> в \S 7.9, а операции ++ и -- в \S 7.10.

7.2.2 Предопределенные свойства операций

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

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

Так, если a типа int, то ++a означает a+=1, что в свою очередь означает a=a+1. Такие соотношения не сохраняются для пользовательских операций, если только пользователь специально не определил их с такой целью. Так, определение operator +=() для типа complex нельзя вывести из определений complex::operator+() и complex 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     // ошибка: операция , частная
}

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

7.2.3 Операторные функции и пользовательские типы

Операторная функция должна быть либо членом, либо иметь по крайней мере один параметр, являющийся объектом класса (для функций, переопределяющих операции new и delete, это не обязательно). Это правило гарантирует, что пользователь не сумеет изменить интерпретацию выражений, не содержащих объектов пользовательского типа. В частности, нельзя определить операторную функцию, работающую только с указателями. Этим гарантируется, что в С++ возможны расширения, но не мутации (не считая операций =, &, и "," для объектов класса).

Операторная функция, имеющая первым параметр основного типа, не может быть функцией-членом. Так, если мы прибавляем комплексную переменную aa к целому 2, то при подходящем описании функции-члена aa+2 можно интерпретировать как aa.operator+(2), но 2+aa так интерпретировать нельзя, поскольку не существует класса int, для которого + определяется как 2.operator+(aa). Даже если бы это было возможно, для интерпретации aa+2 и 2+aa пришлось иметь дело с двумя разными функциями-членами. Этот пример тривиально записывается с помощью функций, не являющихся членами.

Каждое выражение проверяется для выявления неоднозначностей. Если пользовательские операции задают возможную интерпретацию выражения, оно проверяется в соответствии с правилами \S R.13.2.

Равиль Ярупов
Равиль Ярупов
Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?

Евгений Чаленко
Евгений Чаленко
Россия, Новокузнецк, НФИ КемГУ
Антон Свитенков
Антон Свитенков
Беларусь, Речица