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

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

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

t 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.

г. -^ ...-....--_.

I 344 I

Часть I! ^ Объектно-ориентировонное г1рогра1^^ировоние на С+н-

Struct Cylinder

{

 

 

 

 

 

/ /

начало области действи>i ^^lclUL^cl

 

double

radius,

height;

 

 

 

 

/ /

компоненты данных класса

void setCylinder(double

г,

double

h)

/ /

функции-члены класса

{radius

= г; height

= h;

}

 

 

 

 

 

 

 

 

double getVolumeO

 

 

 

 

 

 

 

 

 

 

{ return height * radius *

radius

*

3.141593;

}

/ /

вычислить объем

void scaleCylinder(double

factor)

 

 

 

 

 

 

 

{radius

*= factor;

height

*= factor;

 

}

/ /

масштабировать размеры

void printCylinderO

 

 

 

 

 

/ /

вывод состояния

объекта

{

cout «

"радиус:

" «

radius «

"

высота: "

«

height «

endl;

}

}

;

 

 

 

 

 

 

 

 

/ /

конец области действия класса

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

Cylinder с1, с2; / / выделяется место для двух экземпляров объектов

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

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

В объектно-ориентированном анализе и проектировании термин "объект" час­ то применяется к набору потенциальных экземпляров с одними и теми же свойст­ вами. Это применение ближе к концепции класса (определяемого программистом типа), чем к концепции экземпляра объекта. Не будем вдаваться в дискуссию, какое применение наиболее корректно, однако во избежание неоднозначности не следует употреблять термин "объект", не описывая его смысл.

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

Наличие функций-членов увеличивает размер определения структуры, но не расширяет размера ее экземпляров. Это все равно будет сумма размеров отдель­ ных полей (с возможным дополнительным местом из-за выравнивания). В данном случае, для каждого экземпляра Cylinder выделяется место, достаточное для раз­ мещения двух значений double.

Глава 9 • Классы C++ как единицы модульности программы

345

Исключение конфликтов имен

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

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

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

с1. radius = 10;

/ /

радиус с1

c2.setCylinder(20.30);

/ /

setCylinder() для с2

Вданном примере значение 10 получает radius экземпляра Cylinder переменной с1,

адля вызова setCylinderO используется переменная с2 экземпляра Cylinder. Если приложение использует другой класс, например Circle, где также имеет­

ся компонент данных radius, то конфликта имен не будет:

struct

Circle {

 

double radius;

/ / может быть целым или иным типом

... }

;

 

Для доступа к полю radius класса Circle приложению нужно определить эк­ земпляры объекта Circle и их имена:

Circle c i r 1 ; c i r 1 . radius = 10;

/ /

нет неоднозначности:

 

/ /

Circle, а не Cylinder

Все компоненты класса (элементы данных и функции-члены) находятся в одной области действия (ограниченной фигурными скобками класса). Следовательно, они могут обращаться друг к другу просто по имени без уточняющих ссылок (операций области действия) с именем класса или именем объекта. Например, функция setCylinderO присваивает значения полям (компонентам-данным) radius и height:

void setCylinder(double

г, double h)

/ / присваивает значения полей

{ radius = г; height = h;

}

 

К чему здесь относятся radius и height? Это поля некоего объекта Cylinder (экземпляра класса). Когда функция-член, например setCylinderO, вызывается в клиенте, объекту передается сообщение. Клиентская программа (находящаяся вне фигурных скобок класса) идентифицирует получателя сообщения (объект, поля которого используются внутри функции-члена) путем явного применения имени объекта, имени функции-члена и разделяющего их селектора-точки.

Cylinder с1, с2;

/ / потенциальные

получатели сообщений

c1.setCylinder(10,30);

с2. setCylinder(20, 30);

//сообщения передаются

 

 

/ /

экземплярам с1, с2

t 346

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

 

; ф

Сообщения применяются к экземплярам объектов данного класса. Когда перс дается первое сообщение, внутри setCylinder() используются radius и height объекта с1. Когда передается второе сообщение, внутри setCylinder() исполь­ зуются radius и height (радиус и высота) объекта с2. Это относится не только к изменению значений полей при передаче сообщений, но и к простому их исполь­ зованию в вычислениях.

if (c1.getVolume() < c2.getvolume())

// сравнение объемов

{ c1.scaleCylinder(1.2); . . .

// масштабирование

В первом сообщении для определения объема (как бы он ни вычислялся) ука­ зываются поля переменной с1. Во втором сообщении применяются поля экземп­ ляра с2. Вовсех случаях используется имя объекта, оператор селектора-точки и имя сообщения (функции-члена).Синтаксис сообщения тотже, что и синтаксис для доступа (или изменения) полей структуры.Для обозначения поля можно ис­ пользовать имя объекта, селектор-точку и имя поля.

с1. radius = 40.0; с1.height = 50.0;

// используется переменная с1

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

Листинг 9.2. Пример связывания данных ифункций в классе (одна область действия)

#include <iostream> using namespace std;

struct Cylinder {

double radius, height; ;

void setCylinder(double r, double h)

{ radius = r; height = h; }

double getVolumeO

{ return height * radius * radius *3.141593;

void scaleCylinder(double factor)

{ radius *= factor; height *= factor; }

//начало области действия класса

//поля данных для доступа

//присваивание данных Cylinder

//вычисление объема

//масштабирование размеров

void printCylinderO

 

// вывод состояния объекта

cout «

"радиус: " « radius « " высота: "<< height «

endl;

};

 

 

 

// конец области действия класса

int mainO

 

 

// перенос обязанностей на серверные функции

{ Cylinder c1, c2;

 

// данные программы

c1.Cylinder(10,30); c2. setCylinder(20,30);

// размеры цилиндров

cout «

"\Первоначальный

размер первого цилиндра\п";

 

с1.printCylinderO;

 

// сравнить объемы

if (d.getVolumeO < c2.getVolume())

{ c1.scaleCylinder(1.2);

// масштабировать

cout «

"\пИзмененный

размер первого цилиндра\п";

// вывести новый размер

с1.printCylinderO; }

 

// иначе ничего не делать

else

 

 

 

cout «

"\пРазмер первого цилиндра не изменен" « endl

return 0;

 

 

 

Глава 9 • Классы C+-I- кок единицы моАУЛьности программы

| 347 |

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

void setCylincler(Cylincler& с,

double г, double h)

/ /

функция доступа

{ с.radius = г; с.height = h;

}

/ /

Cylinder - параметр

Соответствующий экземпляр объекта следует использовать в вызове как факти­ ческий аргумент:

setCylinder(c1, 10, 30); setCylinder(c2, 20, 30);

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

void setCylinder(double

г, double h)

/ / нонсенс: какой

Cylinder?

{ с.radius = г;

с.height

= h; }

 

 

setCylinder(10.

30); setCylinder(20, 30);

//нонсенс: какой

Cylinder?

При разработке функции как функции-члена класса нет необходимости переда­ вать ей в качестве параметра используемый объект.

void setCylinder(double

г,

double

h)

/ / метод: нет параметра Cylinder!

{ radius = г; height = h;

}

/ /

компонентные данные, нет полей-параметров!

Вместо этого соответствуюндий экземпляр объекта задается как получатель со­ общения при вызове функции:

c1.setCylinder(10,30);

/ /

объект

с1

-

получатель

сообщения

c2.setCylinder(20,30);

/ /

объект

с2

-

получатель

сообщения

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

void setCylinder(Cylinder&

с,

double г, double h)

/ /

плохой метод

{ с. radius = г; с.height = h;

}

 

 

c1.setCylinder(c1,10,30);

c2.setCylinder(c2,20,30);

/ /

плохие сообщения

Не знаю, что привлекает программистов в этих идиомах C+ + , но встречаются они довольно часто. Корректна ли такая программа синтаксически? Конечно, ина­ че программисты не смогли бы ее использовать. А семантически? Да, в противном случае программистам пришлось бы с нею что-то делать. Тем не менее выглядит такая программа неприглядно.

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

c2.setCylinder(c1,10,30); с1.setCylinder(c2,20,30);

/ / все равно плохо

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

Эта неуклюжая запись — лишь пример, показывающий, как легко в C++ на­ писать программный код, который делает не то, что предполагается, и использует программные компоненты, не имеющие к нему отношения. Такая запись всегда увеличивает сложность программы и связность между клиентом и сервером.

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.

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

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