Классы вводят в программу производные типы. Такие типы могут входить в списки параметров функций и определять тип возвращаемого значения. В вызовах функций при передаче параметров и возвращении значений данные производных типов используются в программе наравне с данными базовых типов. В условиях фактического равноправия производных и основных типов данных должна существовать возможность сохранения привычной структуры выражений при работе с данными производных типов.
Это означает, что выражение для вычисления суммы двух слагаемых уже известного нам типа ComplexType по своей структуре не должно отличаться от соответствующих выражений для слагаемых типа int или float. Но большинство операций языка C++ определены лишь для основных типов данных. Использование в качестве операндов операций выражений производных типов вызывает ошибки трансляции. Поэтому в классе ComplexType и были определены специальные функции-члены, реализующие арифметические операции над множеством комплексных чисел.
И всё же возможность сохранения привычной структуры выражений для производных типов в C++ существует.
Вернёмся к известному классу ComplexType. Мы определим два объекта класса ComplexType, после чего воспользуемся операцией присвоения.
ComplexType ctVal1(3.14, 0.712); ComplexType ctVal2, ctVal3; /* Комплексные числа со случайными значениями данных-членов.*/ ::::: ctVal2 = ctVal1; ctVal3 = ctVal2 = ctVal1; /* Операция присвоения коммутативна.*/
Если теперь вывести значения данных-членов объектов ctVal2 и ctVal3, то окажется, что они полностью совпадают со значениями данных-членов объекта ctVal1. Операция присваивания изначально определена для объектов класса ComplexType. Её можно рассматривать как предопределённую операцию, которая обеспечивает фактически побитовое копирование объекта, стоящего справа от символа = в объект, расположенный слева от этого знака.
Подобно любому другому выражению, выражение присваивания имеет собственное значение. Это значение равно выражению, стоящему справа от операции =. В ходе выполнения программы изменяется значение l-выражения в выражении присваивания, после чего значение этого выражения оказывается равным изменённому значению этого самого l-выражения.
Строго говоря, операцию присваивания для объектов производных типов нельзя называть операцией. Как и ранее рассмотренные нами "операции" приведения, она является операторной функцией. Это значит, что при её объявлении используется специальное имя, состоящее из ключевого слова operator с символом операции, а для её вызова можно использовать полную и сокращённую форму.
Мы приступаем к очередной модификации объявления класса ComplexType с целью переопределения новой операторной функции, реализующей то, что можно называть "операцией присваивания".
Работу по объявлению этой функции мы начнём с того, что попытаемся представить её общий вид:
class ComplexType { ::::: ComplexType operator = (const ComplexType &); /* Ссылка на константу при объявлении параметра не является обязательным условием для объявления операторной функции. Но это гарантия того, что присваиваемое значение не будет изменено в результате обращения к данным-членам. Операторная функция operator=() возвращает значение (именно значение!) объект класса ComplexType. Это не самый оптимальный способ обеспечения коммутативности операторной функции. Но при этом обеспечивается подобие операторной функции операции присваивания. */ ::::: } ::::: ComplexType ComplexType::operator = (const ComplexType& ctKey) { cout << "This is operator = (ComplexType& ctKey)..." << endl; /* Подтверждение о том, что выполняется именно эта функция. */ this->real = ctKey.real; this->imag = ctKey.imag; this->CTcharVal = ctKey.CTcharVal; this->x = ctKey.x; /* Теперь вся ответственность за корректность процесса копирования целиком и полностью возлагается на программиста. */ return *this; /* Мы возвращаем значение объекта, представленного this указателем. */ } ::::: /* Будем считать, что объекты ctVal1 и ctVal2 уже определены. Осталось рассмотреть варианты вызовов этой функции. */ ctVal2.operator = (ctVal1); /* Вариант полной формы вызова функции.*/ ctVal2 = ctVal1; /* Вариант сокращённой формы вызова функции. Операция обращения, ключевое слово operator в составном имени операторной функции и скобки, заключающие выражение, представляющее значение параметра опускаются. Создаётся иллюзия использования обычной операции присваивания. */ /* Демонстрация коммутативности операторной функции присваивания. */ ctVal3.operator = (ctVal2.operator = (ctVal1)); /* Операторная функция operator=() вызывается непосредственно из объекта ctVal3 со значением атрибута (ссылкой на объект), который сам в свою очередь является результатом применения операторной функции operator=() к объекту ctVal2 с параметром-ссылкой на объект ctVal1. Всё очень просто и красиво! */ ctVal3 = ctVal2 = ctVal1; /* Сокращённая форма коммутативного вызова операторной функции присваивания. */
При объявлении и определении операторных функций (в том числе и operator=() ), используется синтаксическая конструкция, обозначаемая в терминах формальной грамматики нетерминальным символом ИмяФункцииОперации. Несколько форм Бэкуса-Наура позволяют однозначно определить это понятие:
Имя ::= ИмяФункцииОперации ::= ***** ИмяФункцииОперации ::= operator СимволОперации СимволОперации ::= +|-|*|?|%|^|&|~|!|,|=|<|>|<=|>=|++|--|<<|>>|==|!=|&&| |||+=|-=|*=|<<=|>>=|[]|()|->|->*|new|delete|
Как следует из приведённых БНФ, большинство символов операций языка C++ могут участвовать в создании так называемых имён функций операций или операторных функций. То есть на основе этих символов можно объявлять операторные функции, сокращённая форма вызова которых позволяет создавать видимость применения операций к объектам производных типов.
C++ не накладывает никаких ограничений на семантику этих самых операторных функций. Наша операторная функция operator=() могла бы вообще не заниматься присвоением значений данных-членов. Она могла бы не возвращать никаких значений. Само собой, что тогда выражение вызова этой функции не могло бы быть коммутативным. А единственный параметр можно было бы передавать по значению. Но тогда всякий раз при вызове функции неизбежно должен был бы вызываться конструктор копирования, который бы создавал в области активации функции копию объекта, которую впоследствии должен был бы разрушать деструктор.
Операторная функция operator=(), как и любая другая функция, может быть перегружена. Например, объявление параметра типа int, позволило бы присваивать комплексным числам целочисленные значения. Здесь нет пределов совершенствования. В принципе, механизм операторных функций регламентирует лишь внешний вид заголовка функции (его "операторное" имя, количество параметров, в ряде случаев - возвращаемое значение). Информация о заголовке принципиальна, поскольку от этого зависит форма сокращённого вызова операторной функции.
Ещё несколько замечаний по поводу спецификации возвращаемого значения операторной функции.
Операторная функция operator=() может вообще не возвращать никаких значений. Сокращённая форма вызова
ctVal2 = ctVal1;
с точки зрения транслятора абсолютно корректна и полностью соответствует следующим прототипам:
void ComplexType::operator = (const ComplexType& ctKey); void ComplexType::operator = (ComplexType& ctKey); void ComplexType::operator = (ComplexType ctKey);
Правда, в таком случае ни о какой коммутативности, "безопасности" и эффективности вновь определяемой операторной функции нет и быть не может.
С другой стороны, уже существующий вариант нашей операторной функции также может быть оптимизирован. Функция может возвращать не ОБЪЕКТ (ЗНАЧЕНИЕ), а ССЫЛКУ на объект.
В этом случае при возвращении значения не будет создано временного объекта. Также не будет вызываться деструктор для его уничтожения. Модификация операторной функции operator=() минимальна - всего лишь дополнительная ptrОперация & в спецификации возвращаемого значения (мы приводим здесь только прототип новой версии функции):
ComplexType& operator = (const ComplexType &);
Всё остальное транслятор исправит самостоятельно, так что никаких дополнительных модификаций в тексте программы производить не придётся. Эта функция будет эффективней, правда, семантика выражения её вызова будет отличаться от семантики соответствующего выражения присвоения с базовыми типами. В первом случае результатом выполнения выражения оказывается присваиваемое значение, во втором - ссылка на объект.
Следующий пример является подтверждением того факта, что при объявлении операторных функций полностью отсутствуют чёткие правила. Это подтверждает следующий пример, посвящённый объявлению и вызову различных вариантов операторных функций operator():
ComplexType& operator () (const ComplexType&); /* Первый вариант совместно используемой функции operator().*/ void operator () (int); /* Второй вариант совместно используемой функции operator().*/ ::::: /* Определения этих функций. Как всегда, они не делают ничего полезного… */ ComplexType& ComplexType::operator () (const ComplexType &ctKey) { cout << "This is operator (ComplexType& ctKey)..." << endl; return *this; } void ComplexType::operator () (int iKey) { cout << "This is operator ( " << iKey << " )..." << endl; } ::::: /* Полные и сокращённые формы вызова этих функций. Первая операторная функция коммутативна. */ CDw2.operator()(CDw1); CDw2(CDw1); CDw3.operator()(CDw2.operator()(CDw1)); CDw3(CDw2(CDw1)); CDw2.operator()(25); CDw2(50);
И это ещё не всё! Ещё не рассматривались варианты операторной функции operator() с несколькими параметрами. И здесь следует вспомнить о функциях с переменным количеством параметров. Это не единственный, но наиболее оптимальный подход к объявлению операторной функции operator() с несколькими параметрами. Здесь мы не будем вдаваться в детали алгоритма извлечения информации из списка параметров (мы их уже обсуждали раньше), а ограничимся лишь общей схемой объявления и вариантами выражения вызова. В нашей версии (всего лишь одной из возможных!), первым параметром функции всегда будет целое число:
ComplexType& operator () (int, ...);// Прототип. ::::: ComplexType& ComplexType::operator () (int iKey, ...) { cout << "This is operator ( " << iKey << ", ...)" << endl; return *this; } ::::: CDw2(50); CDw2(50, 100); CDw2(50, "Это тоже вызов операторной функции", 3.14, 0,123456789);
В C++ может быть объявлено более трёх десятков различных вариантов операторных функций. К этому выводу приводит анализ списка символов операций, которые потенциально могут входить в качестве элемента имени операции.
Здесь не имеет смысла описывать все возможные операторные функции по отдельности. В этом разделе мы рассмотрим ещё несколько интересных "нетипичных" случаев объявления, в следующих разделах будут описаны типичные общие схемы объявлений операторных функций.
Как известно, операция косвенного обращения -> является бинарной операцией. Её первым операндом является указатель на объект, вторым - имя члена класса.
Однако в C++ соответствующий операторный аналог представляется операторной функцией без параметров. Кроме того, для этой функции регламентируется тип возвращаемого значения. Она должна обязательно возвращать указатель либо ссылку на объект некоторого класса.
Рассмотрим различные варианты объявления, определения и вызова этой операторной функции.
Первый вариант тривиален:
::::: ComplexType* operator -> (); ::::: ComplexType* ComplexType::operator -> () { cout << "This is operator -> ()..." << endl; return this; } :::::
Таково, в общих чертах, объявление и определение функции. Функция без параметров.
::::: if (CDw2.operator->() == NULL) cout << "!!!" << endl; :::::
Это полная форма вызова в выражении равенства в составе условного оператора.
::::: CDw3->real = 125.07; (CDw3.operator->())->real = 125.07; :::::
Сокращённая и полная формы вызова операторной функции в составе оператора присвоения. Функция возвращает адрес, к которому применяется обычная двухместная операция косвенного обращения.
А вот более простого варианта сокращённой формы вызова функции operator->(), наподобие того, который ранее использовался в составе условного оператора, в C++ не существует. Правильно построенных выражений вида (xObject->) с единственным операндом, где -> является символом операции, в C++ нет, поскольку -> бинарная операция.
Из-за того, что не всегда удаётся различить по контексту выражение вызова функции и операцию косвенного обращения, сокращённый вызов операторной функции operator->() используется исключительно для имитации выражений с операцией косвенного обращения.
Операторная функция operator->() возвращает указатель на объект, и как любая нестатическая функция-член класса должна вызываться непосредственно "из объекта". Эта прописная истина не представляла бы никакого интереса, если бы в C++ существовали жёсткие ограничения на тип возвращаемого значения функции-члена класса. Но таких ограничений для операторных функций в C++ не существует, а потому возможны и такие экзотические варианты операторных функций:
::::: class ComplexType { ::::: }; ::::: class rrr // Объявляется новый класс. { public: ComplexType* pComplexVal; // Собственные версии конструкторов и деструкторов. rrr () { pComplexVal = new ComplexType; // Порождение собственного экземпляра объекта ComplexType. } ~rrr () { if (pComplexVal) = delete pComplexVal; } // Наконец, встроенная операторная функция. ComplexType* operator -> () { cout << "This is operator -> ()..." << endl; return pComplexVal; } }; ::::: // А это уже собственно фрагмент программы… rrr rrrVal; // Определяем объект - представитель класса rrr. cout << rrrVal ->real << " real." << endl; ::::: Сокращённая форма вызова операторной функции operator->() имеет вид rrrVal->real и интерпретируется транслятором как (rrrVal.operator->())->real, о чём и свидетельствует оператор, содержащий полную форму вызова этой операторной функции. ::::: cout << (rrrVal.operator->())->imag << " imag." << endl; :::::
В этом случае из объекта-представителя класса rrr вызывается операторная функция, в обязательном порядке возвращающая адрес объекта-представителя класса ComplexType, к которому сразу же (!) применяется операция косвенного обращения.
Здесь мы рассмотрели три операторные функции, сокращённая форма вызова которых имитировала операции присвоения, вызова и косвенного обращения. Эти операторные функции занимают особое место среди прочих операторных функций.
Во-первых, описанные в этом разделе способы объявления и определения этих функций не имеет альтернативы.
Во-вторых, на внешний вид объявления и формы вызова этих функций наложили свой отпечаток особенности синтаксиса и семантики соответствующих операций. Так, операция присвоения возвращает значение (или ссылку на значение), при этом, одновременно изменяя значение первого операнда (объекта, из которого осуществляется вызов операторной функции), бинарная операция косвенного обращения имитируется функцией без параметров, а операторная функция вызова может быть объявлена со списком параметров переменной длины.
Назад | Содержание | Вперед