
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdfГлава 10 • Операторные функции Г4зт i
Хорошее решение, но слишком прямое, "в лоб". Оно предполагает непосред ственный доступ к полям своих параметров, но сама функция находится вне об ласти действия класса Rational, и, следовательно, не имеет право это делать. В результате функция компилироваться не будет.
С-Ь+ предлагает интересный способ обойти данное ограничение: использова ние дружественных функций (friend functions). Дружественная функция — это функция, не являюидаяся членом класса, но имеюш.ая те же права доступа к ком понентам класса, что и любая функция-член класса. Обратите внимание: не права доступа к данным класса, а права доступа к компонентам класса, т. е. дружествен ная функция может так же легко обраш.аться к закрытым (или заш.иш,енным) функциям-членам класса, как и к закрытым (или защиш,енным) элементам данных.
Дружественной функцией может быть глобальная функция или функция-член другого класса. Есть ситуации, когда желательно разрешить доступ к компонентам класса всем функциям-членам другого класса. В таком случае другой класс опре деляется как "друг" этого класса. (Подробнее об этом рассказывается в главе 12.) Между тем чаш^е всего дружественные функции представляют собой глобальные функции. Если нужно определить функцию одного класса как дружественную функцию другого класса, нужно дважды подумать: очень часто все можно сделать гораздо прош,е.
Чтобы определить функцию, как дружественную функцию класса, ее прототип включается в спецификацию класса (как если бы это был его компонент), а перед прототипом указывается ключевое слово friend. Вот и весь фокус: во всех отно шениях эта функция подобна функции-члену класса.
class |
Rational { |
|
|
long |
nmr; dnm; |
/ / |
закрытые данные |
void |
normalizeO |
/ / |
закрытая функция-член |
public: |
|
|
|
Rational(long n=0, long d=1 ) |
/ / |
конструктор: общий, |
|
|
|
/ / |
преобразования, по умолчанию |
{ nmr = n; dnm = d; |
|
|
|
this->normalize(); } |
|
|
|
friend Rational operator + (const |
Rational |
&x) const Rational &y); |
|
|
|
/ / |
ОСТАЛЬНАЯ ЧАСТЬ КЛАССА Rational |
|
|
/ / |
нет необходимости в функциях |
operator+() |
|
|
|
}; |
|
|
|
Мои последние заявления зашли слишком далеко. Между дружественной функ цией и функцией-членом большая разница. Для вызова дружественной функции не нужно указывать целевой объект, как это делается при вызове функции-члена, однако она может обраш^аться к компонентам класса Rational как его функциячлен. Следовательно, теперь вполне допустима такая версия функции:
Rational operator + (const Rational &x, const Rational &y) |
/ / нет |
Rational:: { return Rational(y. nmr*x.dnm+x.nmr*y.dnm,y.dnm*x.dnm); |
} |
/ / да, закрытые данные
Замена функции-члена класса дружественной функцией снимает неоднозначность в коде клиента:
Rational а(1,4), |
Ь(3,2), с; |
с = а + b |
// нет неоднозначности: с = operator+(a,Ь); |
0 = 3 + 5 |
// нет неоднозначности: с = operator+(a,Rational(5)); |
с = 5 + а |
// нет неоднозначности: с = operator+(Rational(5), а); |
с = 5 + 5 |
// нет неоднозначности: встроенная операция сложения |
432 |
Часть II • Объектно-ориентировонное программирование но C+-t- |
Поддерживаются все три формы выражения с объектами Rational. Если неже лательны вызовы конструктора преобразования Rational, можно избежать его, определив три перегруженные операторные функции и три дружественные функ ции класса Rational.
class |
Rational |
{ |
// закрытые данные |
long |
mnr; dnm; |
||
void |
normalizeO |
// закрытая функция-член |
|
public: |
n=0, long d=1) |
// конструктор: общий, преобразования, |
|
Rational(long |
|||
|
|
|
// поумолчанию: |
{ nmr = п; dnm = d; this->normalize(); }
friend Rational operator + (const Rational &x, const Rational &y); friend Rational operator + (const Rational &x, longy);
friend Rational operator + (long x, const Rational &y);
//ОСТАЛЬНАЯ ЧАСТЬ КЛАССА Rational
};
Как уже упоминалось выше, можно использовать аналогичные методы множе ственной перегрузки с функциями-членами. Дружественные функции имеют перед функциями-членами преимущество, так как функции-члены могут поддерживать только те формы, где левый операнд представлен объектом Rational, а не число вой переменной. Поскольку вызов перегруженной операторной функции интерп ретируется как передача сообщения левому операнду, поддержка такой формы потребовала бы от компилятора понимания следующего выражения:
с = 5.operator+(a); |
/ / |
целое значение не может отвечать |
|
/ / |
на сообщения Rational |
Дружественные функции позволяют более гибко комбинировать числовые опе ранды и объекты, так как левый операнд не обязательно должен быть объектом.
Аналогичный подход можно применить к операциям отношения. Функция-член класса с параметром-объектом поддерживает только выражения с операндами, представляющими экземпляры объектов. Если нужно поддерживать выражения с числовым значением в правом операнде, следует добавить операцию преобразо вания или другую перегруженную операторную функцию с числовым параметром. Тем не менее при этом все равно не поддерживаются выражения, где левый операнд — числовое значение, а правый — объект.
Rational а(1,4), |
Ь(3,2); |
|
|
|||||
i f |
(а < Ь) cout « |
"а |
< Ь\п" |
/ / |
a.operator<(b |
|||
i f |
(а |
< 5) |
cout « |
"а |
< 5\п" |
/ / |
a.operator<(5 |
|
i f |
(1 |
< b) |
cout |
« |
"1 < b\n" |
/ / |
1.operator<(b); нонсенс |
|
i f |
(1 < 5) |
cout |
« |
"1 < 5\n" |
/ / |
встроенная операция неравенства |
Для поддержки четвертой строки в данном фрагменте клиента можно добавить в программу перегруженную операторную функцию:
bool operator < (const Rational &x, const Rational &y) { return x.operator<(y); }
Аналогично арифметической операции применение глобальной функции и опе раторной функции-члена создает неоднозначность во второй и третьей строке при мера.
Rational а(1,4), |
Ь(3,2); |
|
i f (а < Ь) cout « |
"а < Ь\п"; |
/ / a.operator<(b); или operator<(a,Ь); |
434 |
Часть II • Объектно-ориентированное програ1^г^ирование на C^^-f |
Эта операция поддерживает выражения, где левый и правый операнды являют ся объектами (например, с+=Ь;). При наличии конструктора преобразования будут поддерживаться также выражения, где левый операнд — объект, а правый — одно из числовых значений (например, с+=5;).
Rational |
а(1,4), Ь(3,2), с; |
|||
с = а + Ь; |
/ / |
с = operator+(a,b); |
||
с |
+= Ь; |
/ / |
c.operator+=(b); |
|
с |
+= |
5 |
/ / |
c.operator+=(Rational(5)); |
5 += |
с |
/ / |
5.operator+=(c); нонсенс, не так ли? |
На первый взгляд, замена перегруженной операторной функции-члена глобаль ной перегруженной операторной функцией не даст особого выигрыша.
class |
Rational |
{ |
// закрытые данные |
long |
nmr, dnm; |
||
void |
normalizeO; |
// закрытая функция-член |
|
public: |
n=0, long d=1) |
// конструкторы: общий, |
|
Rational(long |
// поумолчанию, преобразования
{ nmr = п; dnm = d; this->normalize(); }
friend void operator += (Rational &x, const Rational &y); // нет const // ОСТАЛЬНАЯ ЧАСТЬ КЛАССА Rational
}
Данная операция изменяет значение первого параметра. Вот почему этот параметр не имеет модификатора const.
void operator -i-= (Rational &x, |
const |
Rational &y) |
/ / нет const |
{ x.nmr = x.nmr^y.dnfn + y.nmr*x.dnm; |
x.dnm *= y.dnm; |
|
|
X.normalizeO; } |
/ / здесь у normalizeO |
есть адресат сообщения |
Помните о дружественной функции, обращаюидейся ко всем компонентам класса, а не только к элементам данных? Такая согласованная интерпретация компонен тов класса здесь окупается: подобная операторная функция обращается к закры тым элементам данных своих параметров и закрытой фyнкции-члeнynormalize().
Rational а(1,4), |
Ь(3,2), с; long х = 5; |
|
с = а + Ь; |
/ / |
с = operator-b(a,b); |
с += b |
// |
operator-H=(c,b); |
с += 5 |
// operator+=(c, Rational(5)); |
|
Б += с |
// константа неможет использоваться как 1-значение |
|
X += с |
// operator-i-=(Rational(x),c); что это? |
Сложение чего-либо с числовым литералом (значением-константой) даст синтак сическую ошибку. Для числовой переменной операция будет сложнее. Эта пере менная может изменяться, особенно при передаче функции, у которой параметрссылка не имеет модификатора const.
Между тем данный аргумент не является объектом Rational, а потому требу ется преобразование типа. Компилятор создает временный объект, вызывает для его инициализации конструктор преобразования Rational и передает значение х конструктору как аргумент. Что же теперь? Модифицировать этот временный объект внутри операторной функции (как параметр х) бесполезно, так как после завершения функции объект будет уничтожен и изменения не передадутся обрат но в переменную х. Порядочный компилятор пометит это как синтаксическую ошибку.
436 |
Часть II ® Объектно-ориентированное орогро^^ироеани'е на ^ |
|||||||||
Rational operator - (const Rational &x, const Rational &y) |
||||||||||
{ |
return Rational(x. nmr*y.clnm - y. nmr*x.clnm, x.dnm*y.dnm); } |
|||||||||
Rational operator * |
(const Rational &x, const Rational &y) |
|||||||||
{"return Rational(x. nmr * y.nmr, x.dnm * y.dnm,); } |
|
|
||||||||
Rational operator / (const Rational &x, const Rational &y) |
||||||||||
{ |
return Rational(x.nmr * y.dnm, x.dnm * y.nmr); } |
|
|
|||||||
void operator += (Rational &x, const Rational &y) |
|
|
||||||||
{ x.nmr = x.nmr * y.dnm + y.nmr * x.dnm; x.dnm *= y.dnm; |
|
|||||||||
|
X. normalizeO; } |
|
|
|
|
|
|
|
||
void operator -= (Rational &x, const Rational &y) |
|
|
||||||||
{ x.nmr = x.nmr * y.dnm + y.nmr * x.dnm; x.dnm *= y.dnm; |
|
|||||||||
|
X. normalizeO; } |
|
|
|
|
|
|
|
||
void operator *= (Rational &x, const Rational &y) |
|
|
||||||||
{ x.nmr *=y.nmr; x.dnm *= y.dnm; |
|
|
|
|
|
|||||
|
X.normalizeO; } |
|
|
|
|
|
|
|
||
void operator /= (Rational &x, const Rational &y) |
|
|
||||||||
{ x.nmr = X. nmr * y.dnm; x.dnm = x.dnm *y.nmr; |
|
|
|
|||||||
X.normalizeO; } |
|
|
|
|
|
|
|
|||
bool operator == (const Rational &x, const Rational &y) |
|
|||||||||
{ |
return (x.nmr * y.dnm == x.dnm *y.nmr); } |
|
|
|
||||||
bool operator < (const Rational &x, const Rational &y) |
|
|||||||||
{ |
return (x.nmr * y.dnm < x.dnm * y.nmr); } |
|
|
|
||||||
bool operator > (const Rational &x, const Rational &y) |
|
|||||||||
{ |
return (x.nmr * y.dnm > x.dnm * y.nmr); } |
|
|
|
||||||
int mainO |
|
|
b(3,2), c, d; |
|
|
|
|
|
||
{ |
Rational a(1,4), |
|
|
|
|
|
||||
|
с = 5 + a; |
|
" +"; a.showO; cout « " ="; |
|
|
|||||
|
cout « |
" " « 5 « |
|
|
||||||
|
c.showO; cout « |
endl; |
|
|
|
|
// operator-(Rational(1), b); |
|||
|
d = 1 - b; |
|
|
|
" |
d.showO; |
cout « |
|||
|
cout « |
" 1 -"; b.showO; cout « |
endl; |
|||||||
|
с - 7 * a; |
a.showO; cout « |
" |
C.showO; |
cout « |
// operator*(Rational(7),a); |
||||
|
cout « |
" 7 *"; |
endl; |
|||||||
|
d = 2 / b; |
b.showO; cout « |
" |
d.showO; |
cout « |
// operator/(Rational(2),b); |
||||
|
cout « |
" 2 /"; |
endl; |
|||||||
|
C.showO; |
|
|
|
|
|
|
|
// operator+=(c,Rational(3)); |
|
|
с += 3; |
" +=" « |
3 « |
" ="; C.showO; |
cout « |
endl; |
||||
|
cout « |
|
||||||||
|
d.showO; |
|
|
|
|
|
|
|
// operator*=(d,Rational(2)); |
|
|
d *=2; |
" *=" « |
2 « |
" ="; d.showO; |
cout « |
endl; |
||||
|
cout « |
// operator<(a, Rational(5)); |
||||||||
|
if (a < 5) cout « |
" a < 5\n"; |
|
|
|
|
||||
|
if (1 < b) cout « |
" 1 <b\n"; |
|
|
|
|
// operator<(Rational(1),b); |
|||
|
if (1 < 5) cout « |
" 1 <5\n"; |
|
|
|
|
// встроенная операция неравенства |
if (d * b - a == с - 1) cout « " d*b-a == c-1 ==";
(c - 1).show(); cout « endl; return 0;
}
438 |
Часть II • Объектно-ориентированное программирование на Cnf-t- |
||||
|
friend Rational operator + (const Rational &x, const Rational &y); |
||||
|
friend Rational operator - (const Rational &x, const Rational &y); |
||||
|
friend |
Rational |
operator |
* |
(const. Rational &x, const Rational &y); |
|
friend |
Rational |
operator |
/ |
(const Rational &x, const Rational &y); |
|
friend void operator += (Rational &x, const Rational &y); |
||||
|
friend void operator -= (Rational &x, const Rational &y); |
||||
|
friend void operator *= (Rational &x, const Rational &y); |
||||
|
friend void operator /= (Rational &x, const Rational &y); |
||||
|
friend bool operator == (const Rational &x, const Rational &y); |
||||
|
friend bool operator < (const Rational &x, const Rational &y); |
||||
|
friend bool operator > (const Rational &x, const Rational &y); |
||||
|
void showO const; |
|
|
||
|
}; |
|
|
|
/ / конец спецификации класса |
Видите, к чему я клоню? Список функций, связанных с данными, здесь присут ствует и находится между открывающей и закрывающей фигурными скобками класса. Он виден не только разработчику класса, но и сопровождающему прило жение программисту. Защищены ли данные от доступа из функций, находящихся вне фигурных скобок класса? Да, данные объявлены закрытыми, и любая функ ция, которой нужно к ним обращаться, должна быть объявлена в классе как функция-член или дружественная функция. А как насчет конфликта имен? Пред положим, нужно реализовать перегруженную операторную функцию operator+() как функцию friend класса Complex. Будет ли это имя конфликтовать с именем функции operator+(), являющейся дружественной функцией класса Rational? Нет, у функции operator+(), относящейся к объектам класса Complex, другая сигнатура.
Complex operator + (const Complex &x, const Complex &y);
A как насчет дружественных функций, нарушающих инкапсуляцию, сокрытие информации и другие обещания объектно-ориентированного программирования? Данная конструкция с дружественными функциями во всех отношениях столь же хороша, что и конструкция с функциями-членами. В основном это дело вкуса. На мой взгляд, дружественные операции легче писать и проверять. Еще одно важное отличие в том, что глобальные операции поддерживают все виды выражений, а функции-члены — только те формы, где левый операнд является объектом, а не числовым значением.
С о в е т у е м Без колебаний используйте дружественные функции при реализации перегруженных операторных функций.
Их легче проектировать, чем функции-члены, они поддерживают все три формы выражений в клиенте (когда оба операнда — объекты, левый — объект и только правый операнд — объект).
Не применяйте дружественные функции, когда они усложняют программу.
Итоги
В данной главе мы рассмотрели такие тонкости C-f+ , как перегруженные опе раторные функции. В отличие от средств C+ + , обсуждавшихся в предыдундих гла вах, перегруженные операторные функции не являются абсолютно необходимыми для написания качественного ПО.
Можно возразить, что за исключением таких классов, как Rational, Complex и им подобных, применение перегруженных операций скорее делает программу более запутанной, чем облегчает ее понимание. Причина в том, что большинство классов отличаются от числовых типов, и применить к ним числовые операции непросто.
Глава 10 • Операторные функции |
439 |
Например, что может означать операторная функция operator+() и operator<() для класса Employee ("служащий")? Или для класса Transaction ("транзакция")? Конечно, можно придать этим операциям некоторый смысл, но он не будет интуи тивно понятным и общепринятым. Возможно, программа будет выглядеть лучше, если назвать такие функции giveRaiseO и hasSeniorityO или дать им какие-то более подходящие для приложения имена.
Между тем перегруженные операции применяются не столь уж редко. Они особенно популярны в библиотеках языка С4- + , включая STL (Standard Template Library), поэтому нужно понимать, что они делают и как реализованы.
Очень важно приведенное здесь сравнение функций-членов и дружественных функций. Часто решения при разработке программы принимаются произвольно, без учета целей объектно-ориентированного программирования.
Не нужно рассматривать дружественные функции как нечто непостижимое. Используйте их, если они дают большую гибкость и делают программу понятнее. Но не переусердствуйте.