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