Расширение действия (перегрузка) стандартных операций
Одной из привлекательных особенностей языка Си++ является возможность распространения действия стандартных операций на операнды, для которых эти операции первоначально в языке не предполагались. Например, определив новый тип комплексное число нам наверняка захочется иметь возможность записать сумму двух объектов класса комплексное число (comp) в виде s+g. Для того чтобы это стало возможным программист должен определить специальную функцию, называемую "операция-функция" (operator function). Формат определения операции-функции:
тип_функции operator# (список формальных параметров) {операторы_тела_операции-функции}
При необходимости может добавляться и прототип операции-функции с таким форматом:
тип_функции operator#(список формальных параметров);
где # - знак операции (например, =, +, -, и т.д.); operator# - имя функции. Как мы видим определение и описание операции-функции практически ни чем не отличается от обычной функции, но отличия все-таки имеются, и состоят они в наличии двух абсолютно эквивалентных способах вызова оператора-функции. Так для операции сложения двух комплексных чисел после определения операции-функции
comp operator+( comp s, comp g){
comp d;
d.Re = s.Re+g.Re;
d.Im= s.Im+g.Im;
return d;
}
два способа её вызова выглядя следующим образом:
comp k(7,2), e(2,5);
operator+(k, e); //первый способ вызова
k+e+k; //второй способ вызова
comp comp::operator+( comp g){
comp d;
d.Re = Re+g.Re;
d.Im= Im+g.Im;
return d;
}
как видно из примера первый способ очень похож на обычный вызов обычной глобальной функции имя_функции(список фактических параметров), а второй способ более краткий позволяет добиться от типов данных, определённых программистом, такой же функциональности как и у фундаментальных типов данных, т.е. перегрузить для новых типов данных необходимый им набор, определённых в языке Си++ операций.
В отличии от обычных функций при реализации операция-функция надо учитывать несколько ограничений, так количество параметров у операции-функции зависит от арности операции и от способа определения функции. Операция-функция определяет алгоритм выполнения перегруженной операции, когда эта операция применяется к объектам класса, для которого операция-функция введена. Чтобы явная связь с классом была обеспечена, операция функция должна быть либо компонентом класса, либо она должна быть определена в классе как дружественная, либо у нее должен быть хотя бы один параметр типа класс (или ссылка на класс). Начнем с последнего варианта.
Если для класса Т введена операция-функция с заголовком Т operator *(T х, T у) и определены два объекта А, В класса Т, то выражение А*В интерпретируется как вызов функции operator * (А,B).
Вторую возможность перегрузки бинарной операции представляют компонентные функции классов. Любая стандартная бинарная операция @ может быть перегружена с помощью нестатической операции-функции, входящей в число компонентов класса. В этом случае у нее должен быть только один параметр и заголовок может выглядеть так:
Т operator @ (Т х)
(здесь Т - определенный пользователем тип, т.е. класс). В этом случае выражение A@B c объектами А, В класса T в качестве операндов интерпретируется как вызов функции А.operator@(В), причем в теле операции-функции выполняется обработка компонентов объекта-параметра В и того объекта А, для которого осуществлен вызов. При необходимости принадлежность компонентов объекту А в теле операции-функции можно сделать явным с помощью указателя this.
Итак, механизм классов дает возможность программисту определять новые типы данных, отображающие понятия решаемой задачи. Перегрузка стандартных операций языка Си++ позволяет сделать операции над объектами новых классов удобными и общепонятными. Но возникают два вопроса. Можно ли вводить собственные обозначения для операций, не совпадающие со стандартными операциями языка Си++? И все ли операции языка Си++ могут быть перегружены? К сожалению (или как констатация факта), вводить операции с совершенно новыми обозначениями язык Си++ не позволяет. Ответ на второй вопрос также отрицателен - существует несколько операций, не допускающих перегрузки. Вот их список:
прямой выбор компонента структурированного объекта;
.* обращение к компоненту через указатель на него;
?: условная операция;
:: операция указания области видимости;
sizeof() операция вычисления размера в байтах;
# препроцессорная операция;
## препроцессорная операция.
Рассмотрим еще несколько важных особенностей механизма перегрузки (расширения действия) стандартных операций языка Си++.
При расширении действия (при перегрузке) стандартных операций нельзя и нет возможности изменять их приоритеты.
Нельзя изменить для перегруженных операций синтаксис выражений, т.е. невозможно ввести унарную операцию = или бинарную операцию ++.
Нельзя вводить новые лексические обозначения операций, даже формируя их из допустимых символов. Например, возведение в степень ** из языка Фортран нельзя ввести в языке Си++.
Любая бинарная операция @ определяется для объектов некоторого класса двумя существенно разными способами: либо как компонентная функция с одним параметром, либо как глобальная (возможно дружественная) функция с двумя параметрами. В первом случае запись х@у означает вызов х.operator@(у), во втором случае х@у означает вызов operator@(x,y).
В соответствии с семантикой бинарных операций ' = ', ' [ ] ' , ' ->' операции-функции с названиями operator=, operator[], operator-> не могут быть глобальными функциями, а должны быть нестатическими компонентными функциями. "Это гарантирует, что первыми операндами будут lvalue".
Любая унарная операция '$' определяется для объектов некоторого класса также двумя способами: либо как компонентная функция без параметров, либо как глобальная (возможно дружественная) функция с одним параметром.
Для префиксной операции ' $' выражение $z означает вызов компонентной функции z..operator $ ( ) или вызов глобальной функции operator $(z).
Для постфиксной операции выражение z$ означает либо вызов компонентной функции z.operator$(), либо вызов глобальной функции operator$(z).
Синтаксис языка Си++ определяет некоторые встроенные операции над стандартными типами как комбинации других встроенных операций над теми же операндами. Например, для переменной long m = 0; выражение ++m означает m += 1, что в свою очередь означает выполнение выражения m = m + 1. Такие автоматические замены выражений не реализуются и не справедливы для перегруженных операций. Например, в общем случае определение operator *=() нельзя вывести из определений operator * () и operator = ().
Нельзя изменить смысл выражения, если в него не входит объект класса, введенного пользователем. В частности, нельзя определить операцию-функцию, действующую только на указатели. Невозможно для операнда m типа int изменить смысл выражения 2 + m и т.п.
"Операция-функция, первым параметром которой предполагается основной (стандартный) тип, не может быть компонентной функцией". Для объяснения этого ограничения предположим, что аа - объект некоторого класса и для него расширено действие операции ' + '.
При разборе выражения аа + 2 компилятором выполняется вызов операции-функции аа. operator + (2) или operator +(aa,2).
При разборе 2 + аа допустим вызов operator + (2 ,аа), но ошибочен 2.operator + (аа). Таким образом, расширение действия операции + на выражение стандартный _тип + объект_класса допустимо только с помощью глобальных операций-функций.
При расширении действия операций приходится предусматривать всевозможные сочетания типов операндов. Например, определяя операцию сложения ' + ' для комплексных чисел, приходится учитывать сложение комплексного числа с вещественным и вещественного с комплексным, комплексного с целым и целого с комплексным и т.д. Если учесть, что вещественные числа представлены несколькими ти-пами (float, double, long double) и целые числа имеют разные типы (int, long, unsigned, char), то оказывается необходимым ввести большое количество операций-функций. К счастью, при вызове операций-функций действуют все соглашения о преобразованиях стандартных типов параметров, и нет необходимости учитывать сочетания всех типов. В ряде случаев для бинарной операции достаточно определить только три варианта:
• стандартный_тип, класс
• класс, стандартный_тип
• класс, класс.
Например, для рассмотренного класса comp можно ввести как дружественные такие операции-функции:
comp operator+(comp x, comp у)
{return(comp(x.real + y.real, x.imag + y.imag)); }
comp operator + (double x, comp y)
{return(comp(x + y.real, y.imag)); }
comp operator + (comp x, double y)
{ return(comp(x.real + y, x.imag));}
После этого станут допустимыми выражения в следующих операторах:
comp СС(1.0,2.0);
comp ЕЕ;
ЕЕ = 4.0 + СС;
ЕЕ = CC + 2.0;
ЕЕ = СС + ЕЕ;
ЕЕ = СС + 20; // По умолчанию приведение int к double
СС = ЕЕ + 'е' ; //По умолчанию приведение char к double
Вместо использования нескольких (в нашем примере вместо трех) очень схожих операций-функций можно задачу преобразования стандартного типа в объект класса поручить конструктору. Для этого требуется только одно - необходим конструктор, формирующий объект класса по значению стандартного типа. Например, добавление в класс comp такого конструктора
comp (double x)
{ real = x; imag =0.0; }
позволяет удалить все дополнительные операции-функции, оставив только одну с прототипом:
friend comp operator +(comp, comp);
В этом случае целый операнд выражения 6+ЕЕ автоматически преобразуется к типу double, а затем конструктор формирует комплексное число с нулевой мнимой частью. Далее выполняется операция-функция
operator +(comp(double(6),double(0)), ЕЕ)
Вместо включения в класс дополнительного конструктора с одним аргументом можно в заголовке единственного конструктора ввести умалчиваемое значение второго параметра:
compl(double r, double i = 0.0) { Re=r; Im = i; }
Теперь каждое выражение с операцией ' + ', в которое входит, кроме объекта класса comp, операнд одного из стандартных типов, будет обрабатываться совершенно верно. Однако такое умалчивание является частным решением и не для всех классов пригодно.
В отличие от всех других унарных операций операции ++ и --имеют, кроме префиксной формы еще и постфиксную. Это привело к особенностям при их перегрузке. В начальных версиях языка Си++ при перегрузках операций ++ и -- не делалось различия между постфиксной и префиксной формами.
В современной версии языка Си++ принято соглашение, что перегрузка префиксных операций ++ и -- ничем не отличается от перегрузки других унарных операций, т.е. глобальные и, возможно, дружественные функции operator ++() и operator - -() с одним параметром некоторого класса определяют префиксные операции ++ и --. Компонентные операции-функции без параметров определяют те же префиксные операции. При расширении действия постфиксных операций ++ и -- операции-функции должны иметь еще один дополнительный параметр типа int. Если для перегрузки используется компонентная операция-функция, то она должна иметь один параметр типа int. Если операция-функция определена как глобальная (не компонентная), то ее первый параметр должен иметь тип класса, а второй - тип int.
Когда в программе используется соответствующее постфиксное выражение, то операция-функция вызывается с нулевым целым параметром.
Дата добавления: 2020-12-11; просмотров: 383;