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

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

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

420

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

 

void Rational::setNumer

(long

n)

/ / не const

 

{ nmr = n; }

 

 

 

 

void Rational::setDenom

(long

d)

 

 

{ dnm = d; }

 

 

 

Ha самом деле, такое "усовершенствование" — лишь дополнительная работа для создателя класса и программиста клиентской части. В общем случае следует избегать предоставления клиенту доступа к деталям реализации (упорядоченного или нет). Если алгоритму клиента необходим такой доступ, можно попробовать поймать зубами пулю — сделать элементы данных общедоступными. Кроме того, структура рациональных чисел вряд ли будет меняться, и в классе всегда будет, как минимум, два поля. Изменение их имен также маловероятно, поскольку другие имена не дадут никаких дополнительных преимуществ. Если нужно ввести допол­ нительные поля, это не повлияет на существующий программный код, обращаю­ щийся непосредственно к числителю и знаменателю.

Внимание Убедитесь, что элементы данных определены как закрытые, ^9 а функции-члены — как общедоступные (public). Если локальные функции

нужны только функциям-членам класса, то их нужно сделать закрытыми. Когда клиенту требуется доступ к элементам данных, а архитектура класса стабильная и вряд ли будет меняться, не следует создавать функций get() и set() — сделайте данные общедоступными.

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

Да, использовать в классе общедоступные данные — плохой тон. Это как за­ храпеть на деловой встрече. Но иногда такой вариант может сработать.

Внимание в классе с хорошо определенными и понятными элементами данных их можно сделать глобальными. В качестве примеров легко привести

такие геометрические и алгебраические классы, как Point, Rectangle, Line, Complex, Rational.

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

Параметры смешанных типов

Классы Complex и Rational — хорошие примеры определяемых программистом типов, которые эмулируют свойства встроенных числовых типов C+-I-. С объекта­ ми этих типов можно работать при помощи того же набора операций, какой ис­ пользуется для обычных числовых переменных. При этом реализуется такая цель языка C++, как интерпретация определенных программистом типов в качестве встроенных типов C+ + .

И все же аналогия неполная. Можно применять к переменным числовых типов ряд операций, не применимых к объектам типа Complex или Rational. Примерами таких операций являются деление по модулю, поразрядные логические операции и сдвиги. Конечно, можно перегрузить эти операции подобно арифметическим

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

421

операциям и операциям сравнения, но смысл таких операций (каким бы он ни был) не будет интуитивно понятен программисту, создающему или сопровождаю­ щему клиентское приложение. Пример такого произвольного назначения смыс­ ла — Complex: :operator+(), реализованный для вывода значений элементов данных объекта Complex. Вряд ли можно сразу сказать, что должно делать выражение +х с переменной х типа Complex.

Еще одна проблема с равнозначной интерпретацией объектов встроенных типов и типов, определяемых программистом,— это проблема неявного преоб­ разования типов. C + + поддерживает безоговорочное преобразование типов. Например, для любых числовых типов следующие выражения будут синтаксически и семантически корректными:

с

+=

Ь;

/ /

ОК для

переменных b и с любых встроенных числовых типов

с

+=

4;

/ /

ОК для

с любых встроенных числовых типов

Каким бы ни был числовой тип переменной Ь, он просто преобразуется к чис­ ловому типу переменной с. Независимо от типа переменной с, целое 4 преобразу­ ется к этому типу. Если переменные b и с в приведенных примерах имеют тип Rational, то вторая строка дает ошибку. Чтобы она была синтаксически коррект­ ной, одна изданных функций должна быть доступна в области действия клиента.

void

Rational: :operator+=(int

х);

/ /

с+=4;

естьс. operator+=(4);

void

operator+=(Rational &r,

int x);

/ /

c+=4;

есть operator+=(c,4);

Ни одна из этих функций в листинге 10.5 не реализована, что приводит к син^ таксической ошибке в юшенте. Вот пример функции-члена, устраняющей такую ошибку:

void Rational::operator += (int x)

/ /

целевой объект изменяется

{ nmr = nmr + x * dnm;

/ /

n1/d1 + n = (n1+n*d1)/d1

this->normalize(); }

 

 

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

Если переменные b и с в примере имеют тип Complex, то обе строки будут син­ таксически корректны, так как в листинге 10.4 реализованы две версии функции operator+=().

void Complex::operator += (const Complex &b); void Complex::operator += (int b);

В коде клиента первая функция вызывается для первой строки примера, а вторая — для второй строки.

с

+=

Ь;

/ /

с.Complex: :operator+=(b);

аргумент

Com'plex

с

+=

4;

/ /

с. Complex::operator+=(4);

аргумент

integer

При этом разрешается проблема использования в выражениях операндов смешанного типа. Вторая перегруженная операторная функция работает не толь­ ко для числовых аргументов, но и для символов, коротких и длинных целых, чисел с плавающей точкой и аргументов двойной точности с плавающей точкой. Соглас­ но правилам преобразования аргументов, значение каждого из этих встроенных типов преобразуется в целое. Нет никакой необходимости перегружать для каж­ дого из данных встроенных типов функцию operator+=(). Будет достаточно одной функции.

422

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

Но вздыхать с облегчением рано. А как насчет других операций: -=, *=, /=? Каждая из них требует другой перегруженной операторной функции с числовым параметром. И как насчет других операторных функций — operator+(), operator-(), operator*() и operator/()? Рассмотрим следующий пример для экземпляров объекта класса Rational:

с

= а +

b;

/ /

с = a.operator+(b);

с

= а +

5;

/ /

?? несовместимые типы ??

Вторая строка опять даст синтаксическую ошибку, поскольку перегруженная операция требует, чтобы в фактическом аргументе передавался объект типа Rational, а не значение встроенного числового типа. Однако все эти выраже­ ния — отнюдь не плод больного воображения. В алгоритмах числовые значения часто комбинируются с комплексными и рациональными числами. А как насчет сравнений? Хотелось бы иметь возможность сравнивать объекты типа Rational с целыми, а это ставит дополнительные вопросы.

Применяемое до сих пор решение было вполне законным, но сложным. Для каждой операторной функции с аргументом Rational (или объектом другого класса) приходится писать еш.е одну операторную функцию с аргументом long int. (На 16-разрядных машинах обычно достаточно int.)

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

Что же это за инструмент? Он дает возможность привести числовое значение к типу класса. Начнем с простого примера.

Rational с = 5; / / несовместимые типы?

Казалось бы, данная строка ошибочна. В главе 3 уже рассказывалось о принци­ пах приведения типов — преобразовании значения одного из встроенных число­ вых типов в другой встроенный числовой тип. Конечно, такое преобразование осуш.ествляется только для встроенных типов C++, а не для приведения значе­ ния встроенного типа к значению определенного программистом типа Rational. Но как бы выглядело такое приведение типов, если бы оно суш^ествовало? Можно было бы использовать тот же синтаксис, что и для числовых типов — имя типа указывать в круглых скобках, а в качестве его имени задавать тип, в который должно преобразовываться значение.

Rational с = (Rational)5; / / вот так может выглядеть приведение типа

В главе 3 вы уже видели две синтаксические формы приведения типа, одна из которых заимствована из языка С (эта форма и использована выше), а другая, в стиле C+ + , имеет вид, напоминаюш,ий функцию.

Rational с = Rational(5); / / вот так должно выглядеть приведение типа

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

Но тогда возникает следуюш^ий вопрос — какой конструктор? Это просто. В главе 9 конструктор с одним параметром, тип которого отличается от типа класса, назывался конструктором преобразования. Теперь должно быть ясно, почему используется именно такое название. Данный конструктор преобразует значение (параметр) одного типа в значение типа класса. Чтобы приведенная выше строка была синтаксически корректной, нужно написать конструктор с од­ ним параметром.

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

423

Что должен делать этот конструктор со своим параметром? Если, к примеру, значение параметра равно 5, то значение объекта Rational следует приравнять к 5, т. е. это будет 5/1. Если оно равно 7, то получается соответственно 7/1. Следовательно, значение параметра нужно использовать для инициализации чис­ лителя, а знаменателю, независимо от содержимого фактического аргумента, при­ своить 1. В результате получается следующий конструктор:

Rational::Rational(long n)

/ /

конструктор преобразования

{ nmr = n; dnm = 1; }

/ /

инициализация целым числом

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

class

Rational {

 

 

long

nmr; dnm;

/ /

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

void

normalizeO;

/ /

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

public:

 

 

Rationale)

/ /

конструктор по умолчанию: нулевое значения 0/1

{

nmr = 0; dnm = 1; }

// конструктор преобразования: дробь как п/1

Rational(long n)

{

nmr = 0; dnm = 1; }

 

 

Rational(long n, long d)

/ /

общий конструктор: дробь как n/d

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

Rational

operator + (const Rational &x) const

{ return

Rational(nmr*x.dnm + X. nmr*dnm, dnm*x.dnm); }

/ /

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

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

Rational(long п=0, long

d=1)

/ /

конструктор: общий, преобразования,

 

 

/ /

по умолчанию

{ nmr = п; dnm = d;

 

 

 

this->normalize();

}

 

 

Нужно понимать, что этот конструктор вызывается, когда клиент подставляет для инициализации объекта два аргумента, один или О аргументов. В таких случаях вместо отсутствующих аргументов при определении объектов Rational подстав­ ляются значения по умолчанию.

Rational

а(1,4);

/ /

Rational

а =

Rational(1,4)

два аргумента

Rational

b(2);

/ /

Rational

b = Rational(2.1)

один аргумент

Rational

c;

/ /

Rational

с =

Rational(0,1)

нет аргументов

Обратите внимание, что подставляемые в данном примере фактические аргу­ менты имеют тип int, а конструктор ожидает тип long. Это не проблема: применя­ ется неявное преобразование встроенных типов из int в long. Такое приведение типов доступно для встроенных числовых типов по умолчанию. В вызовах функций компилятор допускает не более одного преобразования встроенных типов и не более одного преобразования определенных программистом типов классов (вы­ зовов конструктора преобразования).

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

424

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

 

При обработке выражений с операндами типа Rational компилятор сначала

 

преобразует int в long, а затем в Rational. После такого преобразования компи­

 

лятор генерирует вызов соответствующего оператора.

 

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

/ / настоящий смысл с = а + 5;

Теперь приведенный выше программный код компилируется без оператора Rational: :operator+(long). Создается временный объект Rational, вызывается конструктор преобразования, затемoperator+(), а потом — деструктор Rational.

Итак, можно писать клиентскую часть с числовыми значениями во втором операнде, подставляя в первом операнде значение типа Rational:

int mainO

{ Rational a(1,4), b(3,2), c, d; с = a + 5;

d = b - 1;

с = a * 7; d = b / 2; с += 3;

d *= 2;

i f (b < 2);

cout « "Bee работает\п"; return 0; }

//с = a.operator+(Rational((long)5))

//d = b.operator-(Rational((long)1))

//с = a.operator*(Rational((long)7))

//d = b.operator/(Rational((long)2))

//c.operator+=(Rational((long)3));

//d.operator*=(Rational((long)2));

//if (b.operator<(Rational((long)2));

1/4 + 5 = 21/4 3/2 - 1 = 1/2 1 / 4 * 7 = 7/4 3/2 / 2 = 3/4

7/4 += 3 = 19/4 3/4 *= 2 = 3/2 3/2 < 2

Рис. 10.6.

Результат

программы

из лист,инга

10.6

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

Не забывайте, однако, что преобразование к типу Rational выполняется неявно, с помощью вызовов функций в конструкторе преобразования. Когда функция завершает работу, созданный для преобразования времен­ ный объект уничтожается с помош,ью вызова конструктора. (Для данного класса — это конструктор по умолчанию, поддерживаемый компилято­ ром.) Помните пример с подсчетом количества вызовов функций в выра­ жении? Две функции здесь, две функции там... (Можно было бы сказать: "Два доллара здесь, два доллара там".) Следовательно, эта версия класса Rational более медленная, чем версия, не используюш^ая преобразования аргументов и предлагаюидая для аргументов каждого типа отдельную пе­ регруженную операцию.

Листинг 10.6. Класс Rational поддерживает смешанные типы в выражениях

#inclucle <iostream> using namespace std;

class Rational { long nmr; dnm; void normalizeO

public:

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

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

// поиск наибольшего общего делителя // остановить, когда найден НОД
// вычитание наименьшего члена из наибольшего // делитель положительный

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

425

Pool operator == (const Rational &other) const;

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

} ;

Rational Rational::operator + (const Rational &x) const

{ return Rational(nmr*x.dnm + x.nmr*dnm, dnm*x.dnm); }

Rational Rational::operator - (const Rational &x) const { return Rational(nmr*x.dnm - x.nmr*dnm, dnm*x.dnm); }

Rational Rational::operator * (const Rational &x) const

{ return Rational(nmr * x.nmr, dnm *x.dnm); }

Rational Rational::operator / (const Rational &x) const

{ return Rational(nmr * x.dnm, dnm *x.nmr); }

//цель - const

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

void Rational::operator += (const Rational &x)

{ nmr = nmr * x.dnm + x.nmr *dnm; dnm = dnm * x.dnm; this->normalize(); }

void Rational::operator -= (const Rational &x)

{ nmr = nmr * x.dnm - x.nmr *dnm; dnm = dnm * x.dnm; this->normalize(); }

void Rational::operator *= (const Rational &x)

{ nmr = nmr *x.nmr; dnm = dnm * x.dnm; this->normalize(); }

void Rational::operator /= (const Rational &x)

{ nmr = nmr *x.dnm; dnm = dnm * x.dnm; this->normalize(); }

//3/8+3/2=(6+24)/16=15/8

//n1/d1+n2/d2 = (n1*d2+n2*d1)/(d1*d2)

//3/8+3/2=(6+24)/16=15/8

//n1/d1+n2/d2 = (n1*d2-n2*d1)/(d1*d2)

bool Rational::operator == (const Rational &other) const { return (nmr *other.dnm ==dnm *other.nmr); }

bool Rational::operator < (const Rational &other) const { return (nmr * other.dnm < dnm * other.nmr); }

bool Rational::operator > (const Rational &other) const { return (nmr *other.dnm > dnm * other.nmr); }

void Rational::ShowO const

{ cout « " " « nmr « V " « dnm; }

void Rational:: normalizeO закрытая функция-член { if (nmr ==0) {dnm = 1; return; }

int sign = 1;

if (nmr < 0) {sign = -1; nmr = -nmr; } if (dnm < 0) {sign = -sign; dnm = -dnm; } long gcd = nmr, value = dnm;

while (value !=gcd) { if (gcd > value)

gcd = gcd - value;

else value = value - gcd; }

nmr = sign * (nmr/gcd); dnm = dnm/gcd; }

426

 

 

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

int mainO

endl « endl;

 

 

 

 

 

 

 

 

 

{ cout «

 

 

 

 

 

 

 

 

 

Rational a(1,4), b(3,2), c, d;

 

 

 

 

 

 

с = a + 5;

 

" +"; «

5 «

"

 

 

 

 

/ / позднее обсудим с = 5 + a;

a.showO; cout «

C.showO;

cout

«

endl;

d = b - 1;

 

" -"; «

1 « "

 

 

 

 

 

b.showO; cout «

d.showO;

cout

«

endl;

с = a * 7;

 

" *"; «

7 «

"

 

 

 

 

 

a.showO; cout «

C.showO;

cout

«

endl;

d = b / 2;

 

" /"; «

 

2 «

"

 

 

 

 

 

b.showO; cout «

 

d.showO;

cout

«

endl;

c.showO;

 

 

 

 

 

 

 

 

 

 

 

 

с +=3;

" *= " «

3 «

" ="; C.showO; cout « endl;

 

 

cout «

 

 

d.showO;

 

 

 

 

 

 

 

 

 

 

 

 

d *= 2;

 

 

 

 

 

 

 

 

 

 

 

 

 

cout

«

'• *=

"; « 2

«

"

="; d.show();

cout «

endl;

 

 

i f

(b

< 2)

 

 

 

 

 

 

 

 

 

 

 

 

{

b.showO;

cout

«

"

< "

«

2 «

endl;

}

 

 

 

return 0;

}

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

C++ предлагает замечательные средства предотвращения подобных ошибок, вынуждающие разработчика кода клиента указать сопровождающему приложение программисту, что он делает. Они предусматривают применение в конструкторе ключевого слова explicit:

explicit Rational(long

n=0, long d=1)

/ / не может вызываться неявно

{ nmr = n; dnm = d;

 

 

this->normalize();

}

 

Если конструктор объявляется как явный (explicit), то любой неявный его вызов даст синтаксическую ошибку.

Rational а(1,4), Ь(3,2),

с,

 

 

с = а + 5;

/ /

синтаксическая

ошибка: неявный вызов

с = а + Rationales);

/ /

OK: явный вызов

 

d = b - 1;

/ /

синтаксическая

ошибка: неявный вызов

d = b -

(Rational)1;

/ /

ОК: явный вызов

 

i f

(b

< 1)

/ /

синтаксическая

ошибка: неявный вызов

i f

(b

<

Rational(2))

/ /

ОК: явный вызов

 

 

cout

«

"Bee нормально\п";

 

 

Это очень хорошая идея: она дает разработчику класса большие возможности управления объектами классов, которые использует программист, отвечающий за клиентскую часть.

Классы, определяемые программистом (такие, как Complex и Rational), должны по возможности эмулировать поведение встроенных числовых типов. Применение числовых переменных в выражениях вместо объектов не является ошибкой — это вполне законные методы реализации вычислительных алгоритмов. В приведенном

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

| 427 ||

выше примере одни строки помечены "ОК", а другие — как синтаксическая ошиб­ ка. Необходимость каждый раз анализировать использование числовых операндов

вклиенте приводит к получению эстетически менее привлекательной программы.

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

Внимание не используйте ключевое слово e x p l i c i t в конструкторах

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

Дружественные функции

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

Начнем с оператора, в котором крайне желательно одинаково интерпретиро­ вать переменные встроенных и определяемых программистом типов. С++ под­ держивает такой подход, но программист платит за это отказом от свободы выбора имен функции. Имя функции должно начинаться с ключевого слова operator с до­ бавлением символа (или символов) встроенной операции C-f+ , которую нужно применять к объектам данного класса.

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

C++ позволяет также вызывать перегруженные операторные функции подоб­ но вызову других функций С+Н по имени функции (ключевое слово operator плюс символ операции), однако лишь немногие программисты прибегают к такому способу. Если уж нужно вызвать эту функцию как функцию, зачем вообш.е утруж­ дать себя ключевым словом operator? Было бы удобнее воспользоваться свободой выбора имен и дать функции более содержательное имя, типа addComplexO или addToComplexO. В примерах данной главы в вызовах функций полные имена пе­ регруженных операторных функций использовались с одной целью — показать внутренние механизмы программы C++. Каждое применение перегруженной опе­ рации в выражении представляет собой вызов функции (как минимум одной). Если используются локальные или возвраш,аемые объекты, то применение операций влечет за собой также вызов конструкторов и деструкторов этих объектов.

Как нередко бывает в реальной жизни, выигрыш здесь может быть большим, чем плата за него. Ничто не ограничивает программиста в действиях внутри функций, заголовок которых согласуется с правилами перегруженных оператор­ ных функций. Хорошим примером является перегруженная функция operator+() класса Complex из листинга 10.4. Что означает в клиенте следуюш,ее:

Complex х(20, 40), у(30,50);

//определение, инициализация

+х; +у;

/ / т о ж е , что x.operator+(); y.operator+();

Если бы X и у были целыми, то смысл второй строки ясен: сохранение знака значения. Не очень интересная операция, но двух мнений тут быть не может. В случае объектов Complex это бывает все, что угодно. В нашем примере — вывод

I 428

Часть II« Объектно-ориентированное программирование i

 

 

содержимого элементов данных. Во многих классах операции, применимые к мп^-

 

лам, к объектам применяться не могут. Интерпретация объектов как числовых

 

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

 

понятными (как использование плюса для вывода данных). Это серьезная опас­

 

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

 

вода объектов.)

 

 

 

 

 

 

Опять же, выигрыш может быть и меньше, чем плата за него. Перегруженные

 

операторные функции просты, когда относятся к двум экземплярам объектов.

 

Если же один операнд является экземпляром объекта (получатель сообш.ения),

 

а другой — операндом числового типа, то возникает проблема: применение син­

 

таксиса операции дает вызов перегруженной операторной функции с несовмести­

 

мым типом аргумента.

 

 

 

 

 

В предыдуш,ем разделе уже обсуждались два возможных решения этой пробле­

 

мы. Один из них состоит в удвоении числа перегруженных операторных функций.

 

Для каждой функции с параметром типа класса нужно писать перегруженную

 

операторную функцию с тем же параметром числового типа. Это хорошее реше­

 

ние, но оно приводит к "раздуванию" класса и затрудняет его понимание.

 

Енде одно решение состоит в том, чтобы перегружать для каждой операции

 

только одну функцию (с параметром типа класса) и создавать для данного класса

 

конструктор преобразования, приводяш^ий значение числового типа к значению

 

типа класса. Когда операция применяется с двумя операндами типа класса, конст­

 

руктор перед обраи;ением к перегруженной операторной функции не вызывается.

 

Если второй операнд (параметр функции) имеет числовой тип, то конструктор

 

вызывается неявно (или явно при указании ключевого слова explicit) перед

 

вызовом перегруженной операторной функции. При таком решении размер класса

 

остается управляемым, но оно влечет за собой создание и уничтожение временных

 

объектов класса при каждом использовании в фактических аргументах значений

 

числовых типов. Это может повлиять на производительность программы. Напри­

 

мер, первая строка следуюш,его примера не вызывает никаких конструкторов пре­

 

образования, а вторая вызывает.

 

 

 

 

 

Rational а(1,4),

Ь(3,2), с;

 

 

 

 

 

с = а + Ь;

/ /

с = a.operator+(b);

-

совпадают,

нет вызова

конструктора

 

с = а + 5;

/ /

с = a.operator+(5);

-

вызывается

конструктор

преобразования ,

Но и это еи;е не конец истории о смешанных типах в выражениях. А как насчет подобной последовательности операторов в клиенте? Сложение двух объектов Rational поддерживается непосредственно. Сложение объекта Rational и числа реализуется через дополнительный вызов конструктора преобразования, однако сложение числа и объекта Rational не поддерживается.

Rational а(1,4),

Ь(3.2), с;

 

 

 

 

с = а + Ь;

/ /

с = a.operator+(b);

-

совпадают,

нет вызова

конструктора

с = а + 5;

/ /

с = a.operator+(5);

-

вызывается

конструктор

преобразования

с = 5 + а;

/ /

синтаксическая ошибка: с = 5.operator+(a); невозможно

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

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

Глава 1 0 « О п е р а т о р н ы е функции

| 429 щ

использовать в клиенте. В случае функции operator+() класса Rational функцияоболочка будет иметь то же имя и воспринимать в качестве первого параметра числовое значение.

Rational Rational::operator + (int i , const

Rational &x) const

{

Rational

tempi(i);

/ /

конструктор преобразования

 

Rational

temp2 = tempi.operator+(x);

/ /

перегруженная операция

return temp2; }

 

 

A еще лучше так:

Rational

Rational::operator + (long i ,

const Rational &x) const

{ Rational temp(i);

/ /

конструктор преобразования

return

temp + x; }

II

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

Использовать данную функцию невозможно. Здесь участвуют три стороны: получатель сообщения, числовой параметр и параметр-объект. Как объединить все это в одном вызове функции?

Rational а(1,4), Ь(3,2),

с;

c.operator+(5, b);

/ / с + ?

Вызов данной перегруженной операторной функции означает передачу сооб­ щения ее левому операнду. Значит, смысл последней строки кода таков: объект с, плюс что-то еще. Однако требуется сложить 5 и еще что-то, поместив результат в с. Следовательно, строка даст синтаксическую ошибку. Попробуем снова:

Rational а(1,4). Ь(3,2), с;

 

с = b.operator+(5, b);

/ / с = b + ?

Если бы имя функции не содержало ключевого слова operator, то это могло бы сработать. Значение 5 преобразуется в Rational, складывается с объектом Ь, и результат копируется в объект с. Использование объекта b как получателя сообщения выглядит неуместным. Этот объект ничего не делает с операцией. Имя функции содержит ключевое слово operator, поэтому такой синтаксис также не подходит. Здесь должно участвовать две стороны, а не три.

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

Rational а(1,4), Ь(3,2),

с;

с = operator+(5, b);

/ / с = 5 + b; ?

Помните о первых перегруженных операторных функциях, которые исполь­ зовались в классе Complex в листинге 10.2? Они не были функциями-членами класса. Это глобальные функции. Чтобы приведенный пример заработал, нужно определить функцию-оболочку как глобальную функцию:

Rational

operatop

+ (long i ,

const Rational &x)

/ / не член класса

{ Rational temp(i);

/ /

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

преобразования

return

temp + x;

}

/ /

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

Здесь не просто удалена операция области действия класса. Устранен также модификатор const, указывающий, что в теле функции не изменяются поля целе­ вого объекта. Целевого объекта нет, а потому не надо показывать, что его поля не изменяются.

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

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