Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdfI 390 I |
Часть ii « Объектно-ориентировонное прогротттроваиив на С+-^ |
Статические элементы данных не могут быть компонентом объединения (union). Они не могут быть битовым полем класса. И объединения, и битовые гюля обо значают специальное использование памяти, принадлежаш.ей конкретному объек ту. Статические элементы данных принадлежат не конкретному объекту, а классу в целом. Вероятно, вам не потребуется часто использовать объединения и бито вые поля или определять их как статические, тогда такое ограничение особо не повредит.
Статические функции-члены
Вот почти и все, но прежде чем завершить данную главу, нужно познакомиться с пятым смыслом ключевого слова static в С+Н-. Это ключевое слово может применяться как модификатор в функции-члене класса, не обращающейся к не статическим элементам данных. Другими словами, такая статическая функциячлен может обращаться только к своим параметрам, статическим элементам данных и (о, ужас!) к глобальным переменным.
Хорошим кандидатом на превращение функции-члена в статическую является функция quantityO. Она не имеет никаких параметров, обращается к элементу данных count и не получает доступа ни к каким нестатическим элементам данных.
class Point { |
// закрытые координаты |
int X, у; |
|
static int count; |
// закрытый счетчик объектов |
public: |
// многосторонний конструктор |
Point (int а=0, intb=0) |
|
{ X = а; у = b; count++; } |
// не изменяет состояние объекта |
static int quantityO |
|
{ return count; } |
// это не может быть const |
. . . } ; |
|
Статическая функция не может объявляться как const, даже если она не изме няет тех значений, к которым обращается. Статическая функция-член обращается к параметрам, статическим элементам данных и глобальным переменным. Данные значения не являются частью состояния объекта. В применении к функции-члену ключевое слово const утверждает, что она не изменяет элементы данных целевого объекта, к которым эта функция обращается. Поскольку статическая функция не обращается к нестатическим элементам данных, ничего обещать нельзя.
Понятно, что это звучит не очень убедительно, но за всем сказанным стоит весьма жесткая логика. Приведенная выше серия примеров начиналась с функции quantity(), реализованной как глобальная функция, обращающаяся к глобальной переменной count. Затем глобальная переменная count была превращена в ста тические компонентные данные. Наконец, функция quantityO в соответствии с переменной count стала функцией-членом класса Point. Как нестатическая функция-член, она была определена как const, показывая тем самым, что она не изменяет нестатические элементы данных класса. Наконец, эта функция была пре вращена в статическую функцию-член, и модификатор const стал нерелевантным.
Подобно статическим элементам данных и аналогично вызову нестатической функции, статическая функция-член может вызываться через целевой объект класса (или указатель на объект класса). Ее можно вызывать непосредственно-с помошд^ю операции области действия класса, даже если объект класса создан не был.
int mainO |
Point::quantityO; |
// выводит О |
|
{ cout « |
"\пЧисло точек " « |
||
Point р1(20,40); |
р1::quantityO; |
// выводит 1 |
|
cout « |
"\пЧисло точек " « |
||
cout « |
"\пЧисло точек " « |
Point::quantityO; |
//выводит О |
. . . } |
|
|
|
392 |
Чость II« Объектно-ориентировонное профатттроваитв на С+^ |
|||||||
|
Переданы ли здесь идеи разработчика сопровождающему приложение про |
|||||||
|
граммисту? Нет, Например, очевидным промахом является то, что в данной |
|||||||
|
версии не отражается тот факт, что функция samePoints() не изменяет состояние |
|||||||
|
целевого объекта. Следовательно, ее нужно определить как const. Но это еще |
|||||||
|
не все. Нужно указать также, что эта функция работает только со своими пара |
|||||||
|
метрами. Она может быть глобальной, так как не нуждается в элементах данных |
|||||||
|
целевого объекта Point и работает только с элементами данных, переданными ей |
|||||||
|
в параметрах. Между тем она включена в класс Point. Это показывает, что она |
|||||||
|
логически принадлежит классу Point и имеет дело с объектами данного класса, |
|||||||
|
а не с объектами класса Rectangle, Circle или др. Вот почему ее следует опреде |
|||||||
|
лить как static. |
|
|
|
|
|||
|
Для иллюстрации посмотрим, как можно вызывать эту функцию. Вот несколь |
|||||||
|
ко способов: |
|
|
|
|
|
||
|
Point р1, р2(30), рЗ(50,70); |
|
|
|
|
|||
|
i f (p1.samePoints(p2,p3)==true) cout |
« |
"Точки совпадают\п"; |
|
||||
|
Но какое отношение объект р1 имеет к сравнению р2 и рЗ? Помните байку про |
|||||||
|
крокодила и обезьяну? Некрасивый вариант. Еще один способ — использовать |
|||||||
|
объект р2 дважды: |
|
|
|
|
|||
|
Point р1, |
|
р2(30). рЗ(50.70); |
|
|
|
|
|
|
i f (p2.samePoints(p2,p3)==true) cout |
« |
"Точки совпадают\п"; |
|
||||
|
Все равно некрасиво. Объект должен использоваться только один раз. Давайте |
|||||||
|
определим эту функцию как статическую: |
|
|
|
||||
|
class Point { |
|
|
/ / |
частные координаты |
|||
|
int X, |
у; |
|
|
||||
|
static |
int |
count; |
|
/ / |
частный счетчик |
объектов |
|
|
public: |
|
|
|
|
|||
|
Point |
|
(int |
a=0, int b=0) |
|
/ / |
многосторонний |
конструктор |
|
{ |
X = a; |
у = b; count++;} |
|
||||
|
static |
int |
quantityO |
|
/ / |
const не разрешается |
||
|
{ return count; } |
|
||||||
|
static |
bool |
samePoints (const Point &p1, const Point &p2) |
|
||||
|
{ |
return |
p1.x == p2.x && p1.y |
== p2.y; |
} |
|
||
|
. . . } |
; |
|
|
|
|
|
|
Теперь можно вызывать функцию с помощью операции области действия:
Point р1. р2(30), рЗ(50,70);
i f (Point::samePoints(p2,p3)==true) cout << "Точки совпадают\п";
Отлично!
Так когда же использовать статические элементы данных и статические функ ции? Если элементы данных определяются как статические, это показывает, что такие глобальные данные логически принадлежат классу (пример — count). Когда в качестве статических определяются функции-члены, это свидетельствует о том, что глобальные функции логически принадлежат классу и работают с его статиче скими данными, глобальными данными или параметрами, но не с нестатическими данными (примеры — quantityO, samePointO).
Применение статических данных и функций не является при изучении C-f-f главной задачей, однако нужно понимать эти вопросы и использовать ключевое слово static для лучшей передачи своих намерений программистам, отвечающим за клиентскую часть и сопровождение приложения.
Глава 9 • Классы 0+4- как единицы модульности програллмы |
393 |
Итоги
в этой главе рассматривалось использование классов С+Н- как инструментов для создания программ. Применение классов устраняет недостатки, свойственные глобальным функциям как инструменту объектно-ориентированного программи рования.
Первый недостаток глобальных функций состоит в следующем: они не всегда сообщают, что имел в виду разработчик и что функции, обращающиеся к одной и той же структуре данных, на самом деле связаны логически.
Например, если программа использует структуры данных Point и Rectangle, а разработчик помещает все функции доступа к Point в одно место, а все функции доступа к Rectange — в другое, то все замечательно. Если же функции доступа разделены, компилятор жаловаться не будет, но при сопровождении в программе сложно разобраться. Важна дисциплина программирования.
Второй недостаток в том, что при применении глобальных функций инкапсу ляция — дело сугубо добровольное. Программист может отказаться от функций доступа и обращаться непосредственно к полям структуры. Правила языка этого не запрещают. Такой подход также требует дисциплины от программиста.
Третий недостаток — глобальная природа функций. Их имена являются частью глобального пространства имен и могут конфликтовать с другими именами функ ций. Поэтому чтобы избежать конфликтов, программисту нужно знать обо всех применяемых в проекте именах функций, а это увеличивает сложность разработки системы.
Классы C++ позволяют устранить перечисленные недостатки. Объединяя вместе данные и операции с ними, мы избавляемся от первого недостатка. Воз можность контролировать доступ к полям данных устраняет второй недостаток, а использование области действия класса — третий.
Классы несут в себе большой потенциал для улучшения качества ПО.
о |
|
^ ^ ^ ^ ^ |
|
|
|
|
ператорные функции |
|
Темы данной главы |
|
|
*^ Перегрузка операций |
|
|
«^ Ограничение на перегрузку операций |
||
•^ Перегруженные операции как компоненты класса |
||
1^ Учебный пример: рациональные числа |
||
•^ Смешанные типы как параметры |
||
ь^ Дружественные функции |
|
|
*^ Итоги |
|
|
] |
^ ^ |
предыдущей главе рассказывалось о синтаксисе и семантике классов |
|
Ш |
^5к -языка C+-I-. Язык С+Н |
не первый язык программирования, под- |
|
^ |
4l!l!l-^^ держивающий концепцию классов, но именно в нем это впервые |
||
успешно реализовано в "индустриальном масштабе". |
|||
|
Первоначально C + + внедрялся достаточно медленно, так как в индустрии ПО |
||
с сомнением воспринималась его эффективность и надежность. Сомнения в основ ном были безосновательны. Большинство программ C + + занимают столько же памяти, сколько эквивалентные программы на С, не более, и почти все выполня ются не медленнее, чем эквивалентные программы на языке С. Безусловно, есть некоторые исключения, связанные с использованием библиотеки lostream, вир туальных функций и шаблонов. (О них подробнее рассказывается в следующих главах.) Между тем значительный прогресс в области создания аппаратных средств привел к увеличению объемов памяти и быстродействия компьютеров. Это осла било требования к памяти и к скорости выполнения программ. Опыт применения языка C + + ясно демонстрирует, что программирование с применением классов может быть весьма эффективным. Ожидается, что все новые языки программиро вания будут поддерживать классы.
Недоверие к надежности языка еще полностью не исчезло. Опыт применения C + + подтвердил, что существуют опасности и определенные недостатки, которых программистам следует избегать. Однако это не стало препятствием на пути пре вращения C + + в основной язык программирования широкого спектра приложе ний, только сложность этого языка является главным препятствием для достижения надежности. Но, как было показано в предыдущей главе, в основе классов C+ + лежит достаточно простая идея. Классы C + + помогают программистам:
•Связывать данные и операции объекта
•Управлять внешним доступом к элементам класса
Глава 10 • Операторные функции |
395 |
•Вводить дополнительные области действия для предотвращения конфликтов имен
•Переносить обязанности с клиентов на серверы
В предыдущей главе показано, что разработчик СН-+ Бьерн Страуструп вклю чил в классы С-Ы- намного больше, чем предусматривает приведенный короткий список. Конструкторы и деструкторы помогают объектам класса управлять своими ресурсами (в основном динамически распределяемой памятью). Доступность этих функций возлагает дополнительное бремя на разработчика классов (программис та, отвечающего за серверную часть приложения). Он обязан предусмотреть раз нообразные конструкторы для поддержки клиента в разнообразных контекстах. Кроме того, программист должен обеспечить инициализацию данных объекта, но это уже незначительный побочный эффект.
Использование составных объектов еще более усложняет дело. Разработчику класса-контейнера надо позаботиться об инициализации компонентов объекта. Новый синтаксис для этого реализует список инициализации компонентов. Идея составного класса требует использования таких дополнительных деталей, как ком поненты-константы, компоненты-указатели и рекурсивные компоненты. Принцип атрибутов класса ведет к другим расширениям этой идеи, таким, как статические элементы данных и статические функции, характеризующие класс в целом, а не отдельные экземпляры объектов класса.
Как уже упоминалось, при разработке C++ ставилась еще одна цель: интерп ретировать экземпляры класса так же, как переменные встроенных типов. В пре дыдущей главе этот принцип проявлял себя в виде единообразного синтаксиса инициализации и объектов, и переменных. В данной главе мы обсудим еще одно проявление той же идеи — ее расширение на операции C+ + , в результате чего один и тот же синтаксис выражений с операциями можно применять как к объек там классов, так и к переменным встроенных типов в обычных выражениях C++.
Обычно C++ позволяет делать это несколькими способами. Мы обсудим раз ные методы реализации перегруженных операторных функций. Данные методы помогают эффективнее использовать C+ + , а также позволяют лучше понять внутренние механизмы программы C+ + .
Перегрузка операций
В C++ концепция определяемых программистом типов (классов) является расширением концепции встроенных числовых типов. Здесь можно определять переменные новых типов, применяя тот же синтаксис, что и для простых числовых переменных. Подобно встроенным типам, разрешается использовать экземпляры объектов определяемых программистом типов как элементы массива или как элементы данных еще более сложных типов, передавать объекты таких вновь образованных типов, как параметры и, возвращать их из функций, присваивать указателям и ссылкам определяемые программистом значения, применяя такой же синтаксис, как для встроенных значений. С помощью того же синтаксиса, что и для встроенных типов, можно также определять указатели и ссылки на значе ния-константы.
Такое подобие не случайно. Одна из важных целей разработки C++ заклю чалась в интерпретации определяемых программистом типов таким же образом, как встроенных типов. Данная цель не имеет ничегЧ) общего с объектно-ориенти рованным программированием, улучшающим продуктивность разработки, повы шающим эффективность сопровождения ПО или положительно отражающимся на других аспектах разработки ПО. Это чисто эстетическая цель. И вполне закон ная. Программирование, как и любая творческая деятельность, имеет значитель ную эстетическую составляющую. Хотя в книгах по программированию данная тема обсуждается редко, создаваемые программы должны быть настолько же элегантны, насколько удобны в чтении и сопровождении.
396 |
Часть II * Объектно-ориентированное програтттроваите на С+4 |
|
|
Конечно, многие программы, особенно крупные, трудно назвать элегантными. |
|
|
Как, впрочем, удобными в сопровождении, легко читаемыми и переносимыми. |
|
|
Между тем язык спроектирован так, чтобы программист имел возможность до |
|
|
стичь этих целей. |
|
|
В то же время существует значительный разрыв между одинаковой интерпре |
|
|
тацией классов и числовых типов. Определяемые программистами типы С+ + |
|
|
не являются в точности такими же, как "естественные" числовые типы. Самая |
|
|
большая разница в том, что операции С+Н |
сложение, вычитание, сравнение |
на равенство и т. д.— нельзя применять к объектам определяемых программистом типов. Конечно, можно написать функции для реализации подобных операций, но запись выражений при этом будет весьма сложной.
Рассмотрим простой пример: комплексные числа, характеризуемые значения ми действительной и мнимой части. Те, кто не знаком с мнимыми числами, могут рассматривать их как точки на плоскости, где действительные компоненты соот ветствуют координате х, а мнимые — координате у. При с/южении или вычитании сложных чисел результатом также будет мнимое число. Его действительная часть есть результат сложения (или вычитания) действительных компонентов двух опе рандов, а мнимая — результат сложения (или вычитания) мнимых компонентов. Умножение и деление выполняется сложнее, но тоже реализуются как операции с компонентами комплексного числа.
Представим комплексные числа как класс с двумя элементами данных — real и imag. Для простоты обсуждения объявим их public (в private они превратятся в следующей версии класса).
struct Complex {
double real, imag; } ; / / общедоступные элементы данных
В листинге 10.1 дан пример программы, определяющей экземпляры типа Complex, инициализирующие и выполняющие некоторые арифметические опера ции с объектами.
••••••••*•••••«•••••"•••••••• мшиммжшммшшшшшммшмм mm т ( i — м ш — — i ii HI ттшттшштттттт
Внимание Данный пример нельзя считать хорошим примером программы C++. Авторы большинства книг по C++ избегают демонстрировать отрицательные примеры. В результате читателю не удается научиться отличать хорошие программы C++ от плохих. Это все равно, что пытаться научиться живописи, только посещая музеи, в которых представлены шедевры. Программирование на C++ — всегда попытка найти лучшее решение, превосходящее конкурирующий или альтернативный вариант.
Вместо демонстрации правильного решения я предпочитаю показывать ущербное, пояснять, что в нем неверно и как можно его поправить.
А затем привожу лучшее решение и рассказываю о том, чем именно оно лучше.
В примере (см. листинг 10.1) клиент выполняет вычисления с комплексными числами, используя прямой доступ к общедоступным компонентам объекта. В кли енте вместо функций-членов определяются имена и элементы данных real и imag. В результате клиентская программа представляет собой сочетание доступа к по лям данных и вычисления с полями данных. Смысл этих вычислений не выража ется в вызовах функций, его трудно уяснить из анализа деталей вычислений на нижнем уровне. Операции низкого уровня не перенесены на функции-серверы, и программисту придется держать в уме одновременно несколько уровней алго ритма: вычисления высокого уровня и детальные операции. Обязанности не разде лены, и изменения в конструкции класса Complex повлияют на всю программу. Применять ключевое слово struct вместо class здесь не стоит, так как все эле менты данных общедоступны.
