
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdft 340 I Часть li # Объектно-ориентаро&с - v - -, : \ , .\ -^-^розание на С-^-Ф
и с представлением данных. Клиентские функции не обязательно используют ин капсуляцию и могут обращаться к представлению данных непосредственно, созда вая тем самым зависимости между разными частями программы.
В условиях нехватки времени или из-за недостаточного внимания программис ты могут создать зависимости между функциями. Функции со взаимными зависи мостями трудно отслеживать, так как разным программистам, работающим над разными взаимозависимыми функциями, придется координировать свою деятель ность, что далеко не всегда удается.
Взаимозависимые функции нередко труднее и сопровождать, так как они тре буют от разработчика перед внесением изменений изучения этих зависимостей. Такие функции также труднее повторно использовать, поскольку нельзя перенести
в другое приложение лишь одну функцию. Требуется переносить их совместно
сданными и другими функциями. Вот почему, чтобы справиться со всеми этими проблемами, программисту требуется всяческая помощь со стороны языка про
граммирования. Для создания более качественных программ C + + предлагает великолепную языковую конструкцию — класс. Она физически связывает пред ставление данных и операции (функции) с данными. В противном случае это при шлось бы делать концептуально, в воображении разработчика. Такое связывание данных и операций поддерживает принципы инкапсуляции и сокрытия информации.
В данной главе мы подробнее рассмотрим классы C+ + . Будет рассказано о синтаксисе и семантике, об определении компонентов класса (данных и функ ций), пояснено, как определять права доступа к компонентам класса, реализовывать классы в однофайловых и многофайловых программах, определять объекты (экземпляры классов), манипулировать объектами, пересылать сообщения, пере давать их как параметры и возвращать из функций.
Мы обсудим также специальные функции-члены класса — конструкторы и деструкторы, которые часто понимают неправильно, а также модификатор const, уже описанный в главе 7. Этот модификатор помогает разработчику пояснять свои намерения при проектировании программы, в результате чего ее будет проще сопровождать. Особым видом данных и функций класса являются статические данные и функции. Они позволяют разработчику описывать характеристики, об щие для всех объектов класса.
Это весьма серьезная глава. К концу ее вы вполне освоитесь с использованием больших единиц модульности (классов) вместо более мелких единиц (функций). Возможно, количество технических деталей покажется вам чрезмерным. С+Н сложный язык, поэтому нужно время для понимания его концепций, практических подробностей и подводных камней. Тот, кто обещает легкие способы обучения C+ + , либо лжет, либо сам не понимает сложности данной задачи. Если вы почув ствуете, что объем информации слишком велик и вы не все усваиваете, попробуй те поэтапный подход. Пропустите некоторые части данной главы и прочитайте остальные, а потом вернитесь к этой главе для повторного чтения. Попробуйте модифицировать ее примеры, поэкспериментируйте и попытайтесь разными спо собами выразить на языке C + + одни и те же действия. Вы увидите, насколько прекрасна внутренняя логика соединения разных элементов программирования C+ + . После этого окажется, что применять язык вовсе не сложно. Но это придет к вам только с практикой.
Чтобы освоить основы использования классов, важно периодически возвра щаться назад. Многие программисты "перескакивают" этот этап и слишком быстро переходят к более сложным вопросам, таким, как наследование и поли морфизм, не усвоив предварительно основы. Тем самым они еще больше запуты ваются и пишут программы, которые трудно понимать, сопровождать и повторно использовать. Кроме того, C++ предлагает лишь набор инструментальных средств. Эти инструменты можно применять неверно (подобно оружию, автомобилям и компьютерам). Одно лишь их использование еще не гарантирует автоматически хороших результатов. Программист должен эффективно употреблять в дело дан ные инструменты. Удачи.
Глава 9 • Классы C-f4- кок ВА^^^^ЦЫ модульности программы |
| 341 р |
Базовый синтаксис класса
Целью введения классов в C + + является поддержка практики объектноориентированного программирования и устранение недостатков, свойственных применению более мелких единиц модульности — функций.
Первая основная цель введения конструкции класса состоит в связывании дан ных и операций в один синтаксический блок и обозначении общей принадлежности этих элементов программы. Следующая основная цель заключается в устранении конфликтов имен, благодаря чему данные и функции в разных классах могут без проблем использовать одни и те же имена. Третья важная цель — позволить раз работчику сервера управлять доступом к элементам класса извне (из программыклиента). Четвертой целью является поддержка инкапсуляции, сокрытия инфор мации, переноса обязанностей с клиента на сервер, создание раздельных областей ответственности, устранение необходимости координации действий между про граммистами, работающими над разными частями программы.
Эти цели — естественное расширение практики применения функций для объектно-ориентированного программирования. Если рассматривать классы C+ + как еще одну синтаксическую конструкцию безотносительно к описанным выше целям, то применение классов не улучшит качества программного кода. Убеди тесь, что вы уделяете достаточно внимания этим четырем целям и стараетесь достичь их каждый раз, когда включаете в программу очередной класс.
Классы являются основой объектно-ориентированного программирования на C + + . Они предоставляют программистам инструменты для создания новых типов данных, более соответствующих поведению реальных объектов, чем типы, реализу емые в программах с функциями. Некоторые эксперты полагают, что центральное место в объектно-ориентированном программировании занимает использование наследования и полиморфизма. С этим трудно согласиться. Есть множество программ, не извлекающих никаких преимуществ от применения наследования и полиморфизма. Между тем каждая большая программа C + + получает преиму щества от применения классов, если, конечно, эти классы используются коррект но и достигают четырех описанных выше целей. Правильно сконструированная программа C + + представляет собой комбинацию компонентов (модулей), сов местно выполняющих общую задачу, но при этом независимых и раздельно сопро вождаемых.
Связывание операций и данных
Структуры также поддерживают концепцию связывания, комбинируя поля данных. Они позволяют объединять различные компоненты в сложный объект данных. Такими составными объектами можно манипулировать как единым це лым, например передавать функции как параметр или обращаться к компонентам объекта индивидуально.
Но определяя структуру, мы моделируем лишь набор данных, а не их поведе ние. Разрабатывающий серверную функцию программист сам создает инструмен тальные средства для работы с данными — определяет набор функций доступа для обращения к данным и операциям с ними от имени функций-клиентов. В "функциональном" или "процедурном" программировании данные и алгоритмы структурно разделяются. Они соотносятся друг с другом только в представлении программиста, а не в программном коде. В примерах, обсуждавшихся в главе 8, для демонстрации общей принадлежности функций и данных использовались диаграммы объектов.
Когда программы состоят из функций, связь между разными функциями в про грамме не очевидна. Каждая функция может обращаться к любому фрагменту данных в программе. Это еще более затрудняет разработку и особенно сопровож дение и повторное использование компонентов программы.
342 |
Часть II # Объектно-ориентировонное орогра^^мровоние на С^-нн |
||||||||||||||
|
|
Единственный сгюсоб указать, что данные имеют отношение к программному |
|||||||||||||
|
коду, который с ними работает, состоит в том, чтобы поместить элементы данных |
||||||||||||||
|
и прототипы функций в один заголовочный файл или отдельно компилируемый |
||||||||||||||
|
исходный код. Однако файл на диске — это концепция аппаратного обеспечения |
||||||||||||||
|
(или операционной системы), но отнюдь не языка. Вот почему C++ расширяет |
||||||||||||||
|
средство struct, позволяя связывать вместе элементы данных класса (содержа |
||||||||||||||
|
щие значения) и функции-члены (работающие с этими значениями). |
|
|||||||||||||
|
|
Полученные в результате объекты представляют большие единицы модульности. |
|||||||||||||
|
Программисты, занимающиеся клиентской частью, концентрируют свое внимание |
||||||||||||||
|
на данных и на имеющих к ним отношение функциях, а не на многочисленных |
||||||||||||||
|
автономных функциях, связь которых с данными не очевидна. |
|
|
||||||||||||
|
|
В хорошо спроектированной программе C++ доступ к данным класса осуще |
|||||||||||||
|
ствляется через функции, принадлежащие только этому классу. Клиентский код |
||||||||||||||
|
выражается в терминах операций, а не данных. Это сужает горизонт разработчи |
||||||||||||||
|
ков клиентской части и сопровождающих приложение программистов. |
||||||||||||||
|
|
Формально включением полей в oпpeдeлeниestruct вы создаете класс C++: |
|||||||||||||
|
struct Cylinder { |
|
|
|
/ / |
определенный программистом тип (класс) |
|||||||||
|
double |
raduis, height; |
}; |
|
/ / |
конец области действия класса |
|||||||||
|
|
В C++ |
ключевые слова struct |
и class — почти синонимы. Для только что |
|||||||||||
|
созданного класса Cylinder можно определить объекты (экземпляры или пере |
||||||||||||||
|
менные) этого класса, присвоить значения полям объекта. Такой объект разре |
||||||||||||||
|
шается интерпретировать как единое целое (например, передавать его функции |
||||||||||||||
|
как аргумент или сохранять в файле на диске), либо использовать в вычислениях |
||||||||||||||
|
отдельные его части. |
|
|
|
|
|
|
|
|
|
|
||||
|
|
В следующем примере функция main() определяет два объекта Cylinder (пе |
|||||||||||||
|
ременные и экземпляры), инициализирует их и сравнивает объемы. Если объем |
||||||||||||||
|
первого объекта Cylinder меньше объема второго объекта Cylinder, то первый |
||||||||||||||
|
объект Cylinder масштабируется на 20% и выводится новый размер первого |
||||||||||||||
|
объекта Cylinder. Все это аналогично примеру, уже обсуждавшемуся в начале |
||||||||||||||
|
главы 8. |
|
|
|
|
|
|
|
|
|
|
|
|
||
|
int |
mainO |
|
|
|
|
|
|
|
|
|
|
|
||
|
{ Cylinder |
с1, с2; |
|
|
|
|
|
|
|
/ / |
данные программы |
||||
|
с1.radius |
= 10; с1.height |
= 30; с2. radius = 20; |
с2.height |
= 30; |
|
|||||||||
|
cout |
« |
"\Первоначальный |
размер первого цилиндра\п"; |
|
|
|||||||||
|
cout |
« |
"радиус: " « |
с1. radius « |
" |
высота: и « |
с1. height: « |
endl; |
|||||||
|
i f |
(с1. height*c1. radius*c1.radius*3.141593 |
|
/ / |
сравнить объемы |
||||||||||
|
|
|
< c2.height*c2.radius*c2. radius*3.141593) |
|
|
|
|
||||||||
|
{ |
c1.radius *= 1.2; |
c1.height *= |
1.2; |
|
|
/ / |
масштабировать |
|||||||
|
|
cout |
« |
"\пИзмененный |
размер первого |
цилиндра\п' |
/ / вывести нов. размер |
||||||||
|
|
cout |
« |
"радиус: " |
« |
с1. radius |
« |
" |
высота: |
« |
с1.height « |
endl; } |
|||
|
else |
|
|
|
|
|
|
|
|
|
/ / |
иначе ничего не делать |
|||
|
|
cout |
« |
"\пРазмер первого цилиндра не изменен" « |
endl; |
|
|
||||||||
|
return |
0; |
} |
|
|
|
|
|
|
|
|
|
|
Вданной программе имена полей данных используются явно. Клиент обраи;ается
кзначениям полей, когда это необходимо (вычисление объемов, масштабирова ние размера, печать). Изменения в конструкции Cylinder влияют не только на структуру Cylinder, но и на программный код клиента. Если сопровождающему приложение программисту потребуется уяснить суть обработки, то нужно будет рассмотреть каждый шаг — вычисление объема, масштабирование, вывод. Чтобы проверить, все ли размеры объекта инициализированы, масштабированы или выведены, нужно ссылаться на определение класса Cylinder. Повторное исполь зование этих операций в том же или в другом проекте затруднительно, так как они связаны с контекстом программного кода клиента.
Глава 9 • Классы С-4-4- как единицы модульности програ1М1^ы |
343 |
Эти недостатки можно устранить,используя для инкапсуляции операций спо лями структуры функции доступа: setCylinderO, printCylinderO, getVolumeO
и scaleCylinderO. В листинге 9.1 показана новая версия клиента и сервера. Результаты программы представлены на рис. 9.1.
Листинг 9.1. Пример использования функций доступа, выполняющих операции
от имени клиента
#include <iostream> using namespace std;
struct Cylinder {
double radius, height; } ;
void setCylinder(Cylinder& c, double r, double h)
{ 0.radius = r; c.height |
= h; } |
|
||||
double getVolume(const Cylinder& c) |
// вычисление объемов |
|||||
{ |
return c.height |
* c. radius * с radius * 3.141593; } |
|
|||
void scaleCylinder(Cylinder &c, double factor) |
// масштабировать размеры |
|||||
{ c.radius |
|
*= factor; с height *= factor; } |
||||
void printCylinder(const Cylinder &c) |
// вывод состояния объекта |
|||||
{ cout « |
радиус: |
« |
с. radius « " высота: " « с.height « endl; |
|||
} |
|
|
|
|
// перенос обязанностей насерверные функции |
|
int mainO |
|
|
|
|||
{ |
Cylinder c1, c2;- |
|
// данные программы |
|||
|
setCylinder(c1,20,30); setCylinder(c2,20,30); |
// размеры цилиндров |
||||
|
cout « |
"\Первоначальный размер первого цилиндра\п |
|
|||
|
printCylinder(cl); |
|
// сравнить объемы |
|||
|
if (getVolume(cl) < getVolume(c2)) |
|||||
|
{ scaleCylinder(c1,1.2); |
// масштабировать |
||||
|
cout « |
"\пИзмененный размер первого цилиндра\п" |
// вывести новый размер |
|||
|
printCylinder(cl); } |
// иначе ничего не делать |
||||
|
else |
|
"\пРазмер первого цилиндра не изменен" « |
|||
|
cout « |
endl; |
||||
} |
return 0; |
|
|
|
|
|
|
|
|
|
|
|
Начальный размер первого |
цилиндра |
Данный пример аналогичен примеру из листинга 8.7, и то, |
||
что до сих пор демонстрировалось, не выходит за рамки воз |
||||
радиус: 10 |
высота: 30 |
|
||
Измененный |
размер первого |
цилиндра |
можностей обычной структуры. Давайте сделаем еще один шаг: |
|
скомбинируем поля данных и функции в один класс. В следую |
||||
радиус: 12 |
высота: 36 |
|
щем примере синтаксические границы класса Cylinder обо |
|
|
|
|
значаются открывающей/закрывающей фигурными скобками |
|
Рис. 9 - 1 . Результат |
программы |
и завершающей точкой с запятой. |
||
|
из листинга |
9.1 |
Этот класс содержит два поля или элемента данных: radius |
и height. Кроме элементов данных, класс включает в себя четыре функции-члена (такие функции называются также методами — название пришло из языка SmallTalk и искусственного интеллекта). Функции-члены имеют тот же синтаксис, что и глобальные функции. Они могут использовать параметры и возвращать значения. У каждой функции — своя область действия и локальные переменные. В отличие от глобальных функций, функции-члены определяются внутри границ класса (т. е. его фигурных скобок). Теперь всем видно, что функции setCylinderO, printCylinderO, getVolumeO и scaleCylinderO связаны и сгруп пированы вместе с полями данных radius и height.
348 |
Часть И * Обьектно-ориентировонное програттмроваишв на С'^^^ |
Переход от написания автономных функций к классам требует изменения пара дигмы. Вы должны сделать этот шаг и комфортно чувствовать себя при работе и с функциями, и с классами.
Реализация функций-членов вне класса
Нужно отметить, что эти функции-члены корректны тодько в том случае, если они реализуются в области действия класса, т. е. в его фигурных скобках (как в листинге 9.2). Если реализовать их вне данной области действия, то нужно ис пользовать другой синтаксис. Этот синтаксис применяется, когда соотношение между функциями-членами класса и элементами данных устанавливается через прототипы в объявлении (спецификации) класса, а не с помош,ью полной реализа ции, как в листинге 9.2:
struct Cylinder { |
/ / |
начало области действия |
класса |
|
double radius, height; |
/ / |
поля данных для доступа |
|
|
void setCylinder(double г, double h); |
/ / |
присвоить значения полям Cylinder |
||
double getVolumeO; |
/ / |
вычислить объем |
|
|
void |
scaleCylinder(double factor); |
/ / |
масштабирование размеров |
|
void |
printCylinderO; |
/ / |
вывод состояния объекта |
|
} ; |
|
/ / |
конец области действия |
класса |
Это означает, что определения функций (их тела) должны реализовываться от дельно. Обычно спецификация класса содержится в заголовочном файле с рас ширением . h, а реализация функций — в исходном файле с расширениями . срр или . схх (в зависимости от компьютера).
Прототипы функций в объявлении класса выглядят точно так же, как прото типы автономных глобальных функций. Единственное различие в том, что они определяются в области действия класса. Границы класса следует принимать всерьез. Программисты почти никогда не забывают об открываюш,ей и закрывающ,ей фигурных скобках, но забывают о точке с запятой после фигурной скобки. К сожалению, компилятор редко сообш,ает о том, что отсутствует завершаюш,ая точка с запятой, а это распространенная ошибка программирования. Обычно он указывает на ошибку в следующей строке программы. Когда содержаилая ошибку строка следует за спецификацией класса, нужно проверить, не пропуидена ли точка с запятой.
О с т о р о ж н о ! Программисты иногда забывают о точке с запятой после закрываюш1ей фигурной скобки класса. Часто при пропуске точки с запятой компилятор указывает на следующую строку программы, а не на строку,
#где отсутствует этот символ.
Когда функции-члены реализуются вне спецификации класса, нужно указать имя класса, к которому принадлежит функция. Это естественно, поскольку класс имеет собственную область действия. Каждый класс может, например, содержать функцию-член getVolumeO. Теоретически каждый класс может иметь функции setCylinderO или printCylinderO, но вероятность применения этих имен функ ций для таких классов, как, например, Cube, Circle или Account, не очень высока. В них программисты скорее всего будут использовать имена вида setCubeO, setAccountO, printAccountO и т. д. В то же время функция getVolumeO может использоваться в классах Cylinder, Cube или Circle (в последнем случае с нуле вым результатом).
До появления C++ имена вида setCylinderO и setAccountO были единствен ной разумной возможностью, ведь в языке С не различались функции с разными сигнатурами. C++ различает функции с одним именем и разными сигнатурами
Глава 9 • Классы С^-ь кок ВАИИМЦЫ 1УОдульности прогро^^^ы |
349 |
(см. главу 7 и раздел о перегрузке имен функций), поэтому в нем можно приме нять вместо имен setCylincler() и setAccount() имя set(). Первая функция set() может иметь параметр Cylinder, а вторая — Account.
С введением в C-f+ области действия классов конфликты имен функцийчленов перестали быть серьезной проблемой. Следовательно, можно использо вать имя set() вместо setCylinderO, printO вместо printCylinderO и т.д.
c1.set(10,30); с2. set(20, 30); |
/ / объекты Cylinder как получатели сообщений |
Когда компилятор обрабатывает сообщение, он идентифицирует имя целевого объекта и ищет определение (или описание) данного объекта, чтобы идентифи цировать его тип. В этом случае компилятор легко установит, что экземпляры объектов с1 и с2 имеют тип Cylinder. Затем компилятор ищет определение (или описание) данного типа и смотрит, определена ли там функция-член с указанным в сообщении именем. Если он находит компонентную функцию set(), то проверя ет ее интерфейс — число и типы аргументов. Если число аргументов совпадает, но типы не соответствуют, то компилятор рассматривает возможность преобразо вания типов. Когда в результате удается добиться соответствия типов аргументов, компилятор генерирует объектный код. В противном случае он выводит сообще ние об ошибке.
Все это хорошо, компилятор без труда установит тип целевого объекта. Он просто выполнит поиск по исходному коду (точнее, по таблицам, создаваемым при обработке описаний переменных). Однако этого нельзя сказать о сопровождаю щем приложение программисте. Ему придется просматривать исходный код про граммы, что может потребовать много времени и способствовать ошибкам. С этой точки зрения более длинное имя функции поможет программисту избежать поиска определения адресата сообщения:
c1.setCylinder(10, 30); |
//объекты - Cylinder, не так ли? |
c2.setCylinder(20,30); |
|
Вернемся к обсуждению реализации функций-членов класса, когда специфи кация содержит только прототипы этих функций. Реализация их будет точно такой же, как и функций в границах класса. Единственная разница — в именах функций: они должны содержать уточняющее имя класса. Для этого используется операция области действия (::), разделяющая имя класса и имя функции:
inline |
void Cylinder::setCylinder(double г, |
|
double |
h) |
|
||||
{ radius |
= r; |
height |
= h; } |
/ / |
присвоить |
значения полям данных |
|||
inline |
double |
Cylinder::getVolume() |
|
|
|
|
|
||
{ return height * radius * radius * 3.141593; |
} |
/ / |
вычислить объем |
||||||
inline |
void Cylinder::scaleCylinder(double |
factor) |
|
|
|||||
{ radius |
*= factor; |
height *= factor; } |
|
|
/ / |
масштабировать размеры |
|||
inline |
void Cylinder: :printCylinder() |
|
|
/ / |
вывести состояние объекта |
||||
{ cout |
« |
"радиус: " |
« radius « " высота: |
" |
« |
height « |
endl; } |
В человеческом понимании реальное имя функции-члена setCylinder() — не просто setCylinderO, а функция setCylinderO класса Cylinder. Синтаксически это обозначается как Cylinder: :setCylinderO.
Такое определение класса из двух частей (спецификация с прототипами и отдельными реализациями) позволяет получить в точности такой же класс Cylinder. Отметим, что когда функция-член реализуется внутри спецификации класса, она по умолчанию встраиваемая (inline), а при отдельной реализации — нет, поэтому в последнем случае ее нужно явно определять как inline.
Как уже отмечалось, спецификация класса с прототипами обычно помещается в заголовочный файл, а реализации функций — в отдельный исходный файл.