Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Штерн В. - Основы C++. Методы программной инженерии - 2003

.pdf
Скачиваний:
267
Добавлен:
13.08.2013
Размер:
28.32 Mб
Скачать

430

Часть II • Объектно-ориентированное програ1^мирование но С^-ь

 

Когда для переопределения бинарной операции используются функции-члены,

 

левый аргумент будет неявным в форме указателя.

 

Rational

operatop + (const

Rational &x, const Rational &y)

 

{ return

x.operator+(y); }

/ / вызов Rational::operator+(const Rational&);

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

Отметим, что синтаксис выражения в этой функции неудачен. Он может быть интерпретирован как рекурсивный вызов глобальной функции operator+(), кото­ рый определен здесь.

Rational

operator + (const

Rational &x, const Rational &y)

{ return

X = y;}

//рекурсивный вызов operator+(): бесконечный цикл

Шаги разработки интерфейса для этой глобальной функции специально пока­ заны здесь подробно. Многие программисты не привыкли писать на C++ один и тот же алгоритм как функцию-член и как глобальную функцию. Правила пере­ хода сформулированы в главе 9: глобальная функция имеет один дополнительный параметр типа класса. Функция-член не содержит этого параметра, но использует объект аргумента как получателя сообщения. Данные различия нужно понимать.

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

содним параметром, либо глобальную функцию с двумя параметрами. Каждая функция обеспечивает законную интерпретацию выражения, будь то выражение

сдвумя экземплярами объектов или с одним экземпляром объекта и значением встроенного типа (при этом вызывается соответствующий конструктор преобразо­ вания). Конечно, если применяются два операнда встроенных типов, все однознач­ но: компилятор интерпретирует выражение как встроенную операцию, а не вызов перегруженной операторной функции.

Rational а(1.4),

Ь(3,2), с;

с = а + Ь;

/ /

неоднозначность: с = a.operator+(b);

 

 

/ /

или с = operator+(a,b):?

с = а + 5;

/ /

с = a.operator+(Rational(5));

 

 

/ /

или с = operator+(a,Rational(5));

с = 5

+ а;

/ /

нет неоднозначности: с = operator+(Rational(5),а);

 

 

/ /

а не 5.operator+(a);

с = 5

+ 5;

/ /

нет неоднозначности: встроенная операция сложения

Очень жаль, поскольку это противоречит общему алгоритму анализа компилято­ ром смысла имени, описанного в главе 9. Для неоператорных функций компиля­ тор сначала ищет функцию-член класса, а если не находит соответствия в области действия класса, ищет имя среди глобальных функций, известных в данном файле. Для операторных функций на такую удачу рассчитывать не приходится.

Чтобы исключить неоднозначность выражения, где оба операнда представляют объекты, можно устранить операторную функцию-член и реализовать алгоритм операции непосредственно в глобальной функции. Тогда у компилятора будет только один способ интерпретации выражения.

Rational

operator + (const Rational &x, const Rational &y)

/ / нет Rational::

{ return

Rational(y.nmr*x.clnm+x.nmr*y.clnm,y.clnm*x.clnm); }

 

 

/ /

закрытые данные?

Глава 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,Ь);

Глава 10 • Операторные функции

433

i f

< 5)

cout «

i f

(1

< b)

cout

«

i f

(1

< 5)

cout

«

"а < 5\п" "1 < b\n" "1 < 5\n"

/ / a.operator<(5); или operator<(a,5);

// нет неоднозначности: operator<(1,b);

// встроенная операция неравенства

Для поддержки всех форм выражений отношения можно заменить каждую операторную функцию-член глобальной функцией, обращающейся непосредст­ венно к элементам данных параметров. Чтобы подобный доступ был законным, следует определить такую глобальную операторную функцию как дружественную функцию класса:

class

Rational

{

// закрытые данные

long

nmr; dnm;

void

normalizeO

// закрытая функция-член

public:

n=0, long d=1)

// конструктор: общий,

Rational(long

// преобразования, поумолчанию:

{ nmr = n; dnm = d; this->normalize(); }

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 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);

// ОСТАЛЬНАЯ ЧАСТЬ КЛАССА Rational

};

Такая конструкция устраняет неоднозначность и поддерживает все формы операций отношения с объектами в обоих операндах, только в правом операнде и только в левом операнде (самый трудный случай).

Rational а(1,4).

Ь(3,2);

 

 

i f

(а < Ь) cout «

< Ь\п"

/ /

operator<(a, b);

i f

(а < 5)

cout «

< 5\п"

/ /

operator<(a.Rational(5));

i f

(1 < b)

cout

«

"1 < b\n"

/ /

operator<(Rational(1),b);

i f

(1 < 5)

cout

«

"1 < 5\n"

/ /

встроенная операция неравенства

Как видно, операторная дружественная функция может делать ту же работу, что и операторные функции-члены, и даже более того. Единственные операции, которые не могут перегружаться как дружественные, это операции присваивания (operator=()), индекса (operateг[]()), селектора (operator->()) и операция круг­ лых скобок(operator()()). Такое ограничение необходимо, чтобы первый операнд можно было использовать как 1-значение (адресат сообщения). Во всех ранее при­ веденных примерах данного раздела первый и второй операнды представляли г-значения.

Теперь рассмотрим арифметические операции присваивания. Ситуация здесь несколько другая, так как эти операции возвращают не значение, а void). Они модифицируют состояние целевого объекта. Поскольку они не возвращают нового значения класса Rational, конструктор, нормализующий состояние данного объек­ та, не вызывается. Следовательно, перед возвратом результата арифметические операции должны вызывать функцию Rational: :normalize():

void Rational::operator += (const

Rational &x)

// нет const

{ nmr = nmr * x.dnm + x.nmr * dnm;

dnm = dnm * x.dnm;

 

this->normalize(); }

// нет вызова конструктора

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 и передает значение х конструктору как аргумент. Что же теперь? Модифицировать этот временный объект внутри операторной функции (как параметр х) бесполезно, так как после завершения функции объект будет уничтожен и изменения не передадутся обрат­ но в переменную х. Порядочный компилятор пометит это как синтаксическую ошибку.

// конец спецификации класса
/ / закрытые данные / / закрытая функция-член
/ / конструктор: общий, преобразования, по умолчанию:

Глава 10 • Операторные функции

435

5 + 1/4 = 21/4

1 - 3/2 = -1/2

7 * 1 / 4 = 7/4

г I Ъ/г^ 4/3

7/4 += 3 = 19/4 4/3 *- 2 = 8/3

== с-1 == 15/4

Рис. 10.7.

Результат программы из листпинга 10.7

Хотя в данном случае числовые типы и типы, определяемые програм­ мистом, нельзя интерпретировать одинаково, пример показывает, что пройден достаточно длинный путь к этой цели. Многие программисты предпочитают использовать глобальные операторные функции friend, а не функции-члены, так как их легче писать. Они симметрично интерпре­ тируют свои операции.

В листинге 10.7 приведен пример реализации класса Rational с пере­ груженными операторными функциями, определенными как дружествен­ ные функции, а не как функции-члены. Результат программы представлен на рис. 10.7.

Многие программисты, вместо использования дружественных функций, бьются с реализацией операций как функций-членов. Основная причина — убеждение, что они нарушают инкапсуляцию, сокрытие информации и все другие хорошие вещи, которые обещает нам объектно-ориентированное программирование.

Листинг 10.7. Класс Rational с перегруженными операторными дружественными функциями,

поддерживающими смешанные выражения

#inclucle <iostream.h>

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); 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;

};

void Rational::show() const

 

{ cout « " " «

nmr « "/" dnm; }

 

void Rational::normalizeO

// закрытая функция-член

{

if (nmr ==0) {dnm = 1;

return; }

 

 

int sing = 1;

 

 

 

 

if (nmr < 0) {sign = -1; nmr = -nmr; }

 

 

if (dnm < 0) {sign = -sign; dnm = -dnm;

// поиск наибольшего общего делителя

 

long gcd = nmr, valut = dnm;

 

while (value != gcd) {

 

// остановиться, когда НОД найден

 

if (gcd > value)

 

// вычесть меньшее число из большего

 

gcd = gcd

-value;

 

 

else value = value - gcd; }

// знаменатель всегда положителен

 

nmr = sign * (nmr/gcd);

dnm = dnm/gcd; }

Rational operator + (const

Rational &x, const

Rational &y)

{

return Rational(y.nmr*x.dnm + x.nmr*y.dnm,

y.dnm*x.dnm); }

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;

}

// конец спецификации класса

Глава 10 • Операторные функции

437

Действительно, частое использование дружественных функций делает про­ грамму запутанной и более трудной в сопровождении. Сомнений здесь быть не может. А как насчет разумного применения дружественных функций? Что можно считать разумным и что излишним в применение к ним?

Лучший способ ответить на этот вопрос — вспомнить основную цель исполь­ зования классов в C+ + : классы нужны, поскольку при применении автономных глобальных функций, обращаюш^ихся к структурам данных, связь между функция­ ми и данными существует только в представлении разработчика, его идеи не пере­ даются сопровождающему приложение программисту. Кроме того, инкапсуляция в этом случае добровольна, и любая функция может обращаться к данным непо­ средственно, без функции доступа. Правильно? А еще мы хотели получить локаль­ ную область действия класса, чтобы имена функций и данных, используемых в одной части программы, не конфликтовали с именами в других ее частях. Помни­ те этот список? Приходится повторять его достаточно часто, чтобы вы могли при­ менять данные критерии для оценки качества ПО на C++.

Вооружившись этими критериями, посмотрим на конструкцию перегруженных операций, реализованных в виде функций в листинге 10.6.

class

Rational

{

// закрытые данные

long

nmr; dnm;

void

normalizeO

// закрытая функция-член

public:

n=0, long d=1)

// конструктор: общий,

Rational(long

 

 

 

// преобразования, по умолчанию

{ nmr - n; dnm = d; this->normalize(); }

Rational operator + (const Rational &x) const;

Rational operator - (const Rational &x) const; Rational operator * (const Rational &x) const; Rational operator / (const Rational &x) const; void operator += (const Rational &x);

void operator -= (const Rational&x); void operator *= (const Rational &x); void operator /= (const Rational&x);

bool operator == (const Rational &other) const; bool operator < (const Rational &other) const; bool operator > (const Rational &other) const; void showO const;

};

//целевой объект - const

//цель изменяется

//целевой объект - const

Ясна ли здесь связь между функциями и данными? Да, эту связь обозначают

открываюш.ая и закрывающая фигурные скобки класса. Защиш.ены ли данные

от доступа из других функций (не членов)? Да, элементы данных определены как закрытые, к ним нельзя обраш^аться вне класса. Суш,ествует ли опасность конф­

ликта имен между компонентами класса Rational и компонентами других классов?

Нет, в других классах можно определять функции с именами вида operator+() и т. д . — конфликта не будет.

Похоже, неплохая конструкция. Теперь сравним ее свариантом из листин­

га 10.7, где используются дружественные функции:

class' Rational {

// закрытые данные

long nmr; dnm;

void normalizeO

// закрытая функция-член

public:

// конструктор: общий,

Rational(long n=0, long d=1)

nmr = п; dnm = d;

// преобразования, по умолчанию:

 

this->normalize();

 

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), поэтому нужно понимать, что они делают и как реализованы.

Очень важно приведенное здесь сравнение функций-членов и дружественных функций. Часто решения при разработке программы принимаются произвольно, без учета целей объектно-ориентированного программирования.

Не нужно рассматривать дружественные функции как нечто непостижимое. Используйте их, если они дают большую гибкость и делают программу понятнее. Но не переусердствуйте.

Соседние файлы в предмете Программирование на C++