Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf350 |
Часть II • Обьектно-ориентированное програм1М1ирова^ |
Можно, однако, реализовать все или некоторые функции-члены в заголовочном файле. Этот заголовочный файл должен включаться в каждый файл, где упомина ется имя класса, например в исходный файл клиента и даже в исходный файл, где реализуются функции-члены (так как имя класса применяется в операции области действия).
Поскольку компоновщик должен видеть определение функции только один раз, спецификации класса следует ограничивать директивами препроцессора для условной компиляции (см. примеры в главе 2 и 5). Например, заголовочный файл для класса Cylinder может выглядеть так:
#ifnclef |
CYLINDER_H |
|
|
|
|
/ / |
обычное обозначение символического имени |
|||||
#define CYLINDER_H |
|
|
|
|
|
|
|
|
||||
#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; |
} |
|
|
|||||
void printCylinderO |
|
|
|
|
|
/ / |
вывести состояние объекта |
|||||
{ |
cout |
« |
"радиус: " |
« |
radius « |
" |
высота: " |
« height « endl; }, |
|
|||
} |
; |
|
|
|
|
|
|
|
|
/ / |
конец области действия |
класса |
#endif
В соответствии с общепринятым соглашением этот исходный код нужно по местить в файл Cylinder, h и использовать имя CYLINDER. Н для условной ком пиляции. Реализация функций-членов в отдельном файле — важный вклад в повышение модульности программы. Логически функции-члены, например Cylinder: :setCylinder(), определяются в фигурных скобках класса, независимо от того, находятся они в фигурных скобках класса или нет. Вот почему для до ступа к элементам данных radius и height эти функции не нуждаются в квалификаторе (операции ::).
В отдельной реализации квалификатор в имени функции обязателен. Без него функция setCylinder() ссылалась бы на глобальные переменные radius и height, а не на компонентные данные radius и height.
inline |
void 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::printCylinderO |
/ / |
вывести состояние объекта |
||||
{ cout |
« |
"радиус: " |
« radius « |
" высота: |
" |
« height « endl; } |
|
Компилятор без проблем пропустит такое глобальное определение функции — в C+-f это допустимо. Если в области действия файла не описаны переменные radius и height, то компилятор будет жаловаться на то, что они не определены, а вовсе не на отсутствие операции ::. Он даст сообщение об ошибке, вводящее
Глава 9 • Классы С4«+ как единицы модульности программы |
351 |
в заблуждение. Возможно, вы сначала ему не поверите. Неужели компилятор не видит, что radius и height определены вот здесь, в спецификации класса? Навер ное это еще одна ошибка в компиляторе! Но компилятор не может знать, что вы забыли применить для определения глобальных переменных операцию ::. Кстати, если переменные с такими именами определены в данной области действия для каких-то других целей, то компилятор будет молча генерировать код, ссылаюш,ийся на эти глобальные переменные, а не на элементы данных.
О с т о р о ж н о ! программисты иногда забывают указывать перед именем функции операцию ::. Компилятор считает при этом, что реализуется
глобальная функция, и обвиняет программиста в отсутствии определения глобальных переменных с именами применяемых в функции элементов данных.
Определение объектов классов с разными классами памяти
Область действия класса включает в себя все его данные и функции. Наряду с другими функциями и/или классами они вложены в файл (или в другой класс, функцию, блок), где этот класс объявлен. К компонентам данного класса можно обращаться, когда объект класса находится в области действия.
Что касается переменных любого типа, то объекты класса (его экземпляры, переменные) могут определяться в C++ автоматически (см. главу 6, где расска зывается о классах памяти).
Для автоматических и глобальных (extern или static) переменных память распределяется неявно, в результате определения объекта. Все предыдуш.ие при меры определения экземпляров классов — это примеры автоматических пере менных. Они создаются, когда при выполнении программы достигается определение. Например, переменные с1 и с2 в листинге 9.2 создаются, когда выполнение дохо дит до строки функции mainO, содержащей определения этих переменных.
Если переменная класса определяется как глобальная, то память для такого объекта выделяется перед началом выполнения функции main(). Это же относится
кслучаю, когда переменная класса определяется как статическая (глобальная
вфайле или локальная в некоторой функции).
Объединяет все такие экземпляры объектов то, что на них можно ссылаться по именам. Для доступа к полям данных и функциям-членам объектов (и ссылки на них) можно использовать имя объекта с точкой. Такой способ применяется в клиенте при обращении к компонентам класса:
Cylinder х; x.setCylincler(50,80);
double volume = x.getVolumeO; x. radius = 100;
Для динамических переменных память распределяется явно с помощью опера ции new. Объект не определяется по имени — к нему можно обращаться только через указатель. Для доступа к данным и функциям объекта функция-клиент будет использовать имя указателя (а не имя объекта, так как экземпляры объектов не имеют имен) и обозначение-стрелку. Ниже создается имя указателя на объект Cylinder, а затем — неименованный объект класса Cylinder, с которым выполня ются операции:
Cylinder* р; |
// объект еще несоздан |
р = new Cylinder; |
// объект пока несуществует |
p->set Cylinder(50,80); |
// доступ к неименованному объекту |
double volume = p->getVolume(); |
// то жеобозначение |
p->radius = 100; |
|
Глава 9 • Классы C++ как единицы модульности программы |
353 |
setRadiusO, getRadiusO, setHeihgtO и getHeightO вынуждал клиента самому выполнять операции, а не обращаться для этого к функциям-серверам. С этой точки зрения выбор функций-членов в листинге 9.2 предпочтительнее. Вместо получения значения radius и height для масштабирования, печати или вычисле ния объема клиент просит объекты класса Cylinder масштабировать цилиндр и выводить состояние либо вычислять объем.
Перенос обязанностей на функции-серверы — очень важная концепция. Что здесь достаточно, а что нет — часто является субъективным. Можно, конечно, отмахнуться от примера из листинга 8.8, но и он будет полезен, если класс исполь зуется как библиотечная утилита, обслуживаюш^ая большое число пользователей. Для некоторых пользователей подход, применяемый в листинге 9.2, будет слиш ком обш,им. Им может потребоваться вычисление поверхности цилиндра, а не численное значение его объема, но они могут быть заинтересованы и в сравнении объектов-цилиндров (как в листинге 8.9).
О переносе обязанностей на серверные классы часто заходит речь при обсуж дении построения классов. В данном разделе рассказывается о некоторых методах,
позволяющих разработчику класса |
управлять доступом |
к элементам |
данных |
|||||
и функциям класса. |
На рис. 9.2 показана взаимосвязь клас |
|||||||
|
|
|
||||||
|
v^ элементам |
са Cylinder с клиентом main(). Здесь класс |
||||||
.с-^^'Л |
'-^Л^н^ |
имеет три |
компонента: данные, функции |
|||||
Класс Cylinder |
|
|
||||||
|
|
и границу, отделяющую все, что находится |
||||||
(сервер) |
|
|
внутри класса, от того, что находится вне |
|||||
|
|
|
||||||
|
|
|
его. Он показывает, что данные располо |
|||||
|
|
|
жены внутри класса, а функции частично |
|||||
|
|
|
находятся внутри класса (их реализация), |
|||||
|
|
|
а частично — снаружи (интерфейсы, из |
|||||
|
|
Код клиента |
вестные клиенту). Этот рисунок демонст |
|||||
|
|
рирует также, что когда клиенту нужны |
||||||
|
|
Сообщения |
значения полей |
Cylinder |
(например, для |
|||
Локальный доступ |
|
вычисления |
объема |
цилиндра, масштаби |
||||
Функции-члены экземплярам |
||||||||
к элементам данных |
класса |
класса |
рования, печати |
или |
присваивания |
значе |
||
(рекомендуется) |
|
(рекомендуются) |
ний полям), он использует функции-члены |
|||||
|
^ Граница класса |
|||||||
|
getVolumeO, scaleCylinderO и т.д., а не |
|||||||
Рис. 9 . 2 . Класс Cylinder и его |
взаимосвязь |
обращается |
к |
значениям |
полей |
radius |
||
с клиентом |
main() |
|
и , height. |
Вот, |
что |
означает пунктирная |
||
линия. Она показывает, что прямой доступ к данным исключен.
Две причины побуждают ограничение доступа к элементам данных. Первая — необходимость ограничения масштабов изменений в программе при ее модифика ции. Если интерфейсы функций-членов остаются теми же (а обычно нетрудно сохранить их при изменении архитектуры данных), то модифицировать нужно реализацию функций-членов, а не программный код клиента. Набор подлежащих изменению функций хорошо определен. Все они перечислены в определении клас са, и нет необходимости проверять остальную часть программы.
Вторая причина предотвращения прямого доступа клиента к элементам данных заключается в том, что если клиентский код выражается в терминах вызова функций-членов, а не в терминах детальных вычислений со значениями полей, его проще понять. Кроме того, именно это и предполагает перенос обязанностей на функции-члены, которые выполняют работу для клиента, а не просто считыва ют и присваивают значения полей подобно функциям getHeightO и setHeight().
Для достижения названных преимуществ все, что находится внутри класса, должно быть закрытым (private), недоступным извне. Тем самым предотвраща ется создание зависимостей от серверных данных класса. Не забывайте, что
i |
354 1 |
Часть II * Объектно-ориентированное riporpa^fvinpoeaHne но C-f-t- |
|||||
|
|
в программировании слово ^'зависимость'' сродни ругательству. Зависимости |
|||||
|
|
между разными частями программы могут означать: |
|
||||
|
|
• |
Необходимость координации работ и тесной кооперации |
||||
|
|
|
между программистами при разработке программы |
|
|||
|
|
• |
Необходимость изучения и изменения программного кода |
||||
|
|
|
в процессе сопровождения программы |
|
|
||
|
|
• |
Трудности повторного использования программного кода |
||||
|
|
|
в том же или в аналогичном проекте |
|
|
||
|
|
Между тем конструкция класса в листинге 9.2 не предусматривает никакой |
|||||
|
|
защиты от доступа к данным. Клиент может обращаться к полям экземпляров |
|||||
|
|
объектов Cylinder, создавая зависимости от архитектуры данных Cylinder. При |
|||||
|
|
этом теряются наиболее важные преимущества использования классов. |
|||||
|
|
Cylinder |
с1, с2; |
|
/ / определение данных программы |
||
|
|
c1.setCylinder(10,30); с2. setCylinder(20,30); |
/ / |
использование функции |
|||
|
|
|
|
|
|
/ / |
доступа |
|
|
с1.radius |
= 10; с1.height = 20; . . . |
/ / |
все равно работает! |
||
|
|
С++ |
позволяет разработчику детально управлять правами доступа к компо |
||||
|
|
нентам класса. С помощью ключевых слов public, private и protected можно за |
|||||
|
|
дать доступ ддя каждого компонента (данных или функций). Вот еще одна версия |
|||||
|
|
класса Cylinder: |
|
|
|
||
|
|
struct |
Cylinder { |
/ / |
начало области действия класса |
||
|
|
private: |
|
|
|
||
|
|
double radius, height; |
/ / |
данные являются закрытыми |
|||
|
|
public: |
|
|
|
|
|
|
|
void setCylinder(double г, double h); |
|
|
|||
|
|
double getVolumeO; |
/ / |
вычисление объема |
|||
|
|
void |
scaleCylinder(double |
factor); |
|
|
|
|
|
void |
printCylinderO; |
/ / |
вывод состояния объекта |
||
|
|
} ; |
|
|
/ / |
конец области действия класса |
|
Ключевые слова делят область действия класса на сегменты. Все данные или функции, следующие, например, за ключевым словом private, имеют закрытый режим доступа. В этом примере элементы данных radius и height — закрытые (private), а все функции-члены — общедоступные (public).
Сегментов public, private и protected может быть сколько угодно, и следо вать они могут в любом порядке. В приведенном ниже примере элементы данных radius определены как private, две функции — как public, затем элементы данных height — как private и еще две функции — как public:
struct Cylinder { |
/ / |
начало области действия класса |
|
private: |
|
|
|
double radius, |
|
|
|
public: |
|
|
|
void setCylinder(double r, double h); |
|
||
double getVolumeO; |
/ / |
вычисление объема |
|
private: |
|
|
|
double height; |
|
|
|
public: |
|
|
|
void |
scaleCylinder(double |
factor); |
|
void |
printCylinderO; |
/ / |
вывод состояния объекта |
} ; |
|
/ / |
конец области действия класса |
Глава 9 • Классы C++ кок единицы модульности программы |
355 |
Это предоставляет дополнительную гибкость, но программисты обычно груп пируют все компоненты класса с одинаковыми правами доступа в один сегмент.
В общем случае компоненты класса (данные или функции) в сегментах public доступны для остальной части программы (как в предыдущих примерах).
Компоненты класса (также данные и функции) в сегментах private доступны только для функций-членов данного класса (и для функций с правами доступа friend, о чем будет рассказано в главе 10). Использование имени закрытого компонента класса вне области действия класса (или функции friend) даст син таксическую ошибку.
Отметим, что эти правила не запрещают объявлять закрытыми данные и де лать функции общедоступными. Однако обычно в C-f + элементы данных объяв ляются как private, а функции-члены — как public.
Компоненты класса в сегментах protected доступны для функций-членов дан ного класса и функций-членов классов, являющихся их наследниками (прямо или косвенно). Обсуждать наследование пока слишком рано, это намного более широ кая тема, чем синтаксис. Мы вернемся к ней позднее.
Функции-клиенты (глобальные или функции-члены других классов) могут об ращаться к закрытым элементам данных только через функции в части public (если они имеются).
Cylinder с1, с2; |
|
/ / |
определение данных программы |
||
c1.setCylinder(10,30); c2.setCylinder(20,30); |
/ / |
использование функции |
|||
|
|
|
|
/ / |
доступа |
/ / |
с1. radius = 10; |
cl.heihgt = 20; |
/ / |
это не синтаксическая ошибка |
|
i f |
(d.getVolumeO |
< c2.getVolume()) |
/ / |
еще одна функция доступа |
|
|
c1.scaleCylinder(1.2); |
/ / |
масштабирование |
||
Для поддержки клиентов класса и предотвращения нежелательного доступа разработчик класса должен предусмотреть необходимый доступ к данным. Ведь если клиент будет использовать средства класса, которые он применять не дол жен, образуется избыточная зависимость. Изменение этих средств повлияет и на клиента. Кроме того, чем больше средств класса объявляются общедоступными, тем больше информации потребуется программисту, создающему и сопровождаю щему клиента, для продуктивного использования экземпляров класса.
При применении закрытого доступа к элементам данных класса детали реализа ции класса Cylinder будут скрытыми. Если изменяются имена или поля Cylinder, то на клиента это не влияет (если, конечно, интерфейс Cylinder остается тем же). В программном коде клиента не создаются зависимости от архитектуры данных класса Cylinder. Занимающийся разработкой или сопровождением клиента про граммист может не изучать ее.
Обычно изменяются именно данные. Вот почему в типичном классе элементы данных объявляются как private, а функции-члены — как public. Тем самым улучшается модифицируемость программы и облегчается повторное использова ние класса. Заметим, что функции-члены класса (public или private) могут обра щаться к элементам данных того же класса (public или private).
Таким образом, любую группу функций, обращаюш^^хся к одному и тому же набору данных, следует оформлять как функции-члены класса, а вызовы этих функций использовать в клиенте как сообщения экземплярам класса. Тем самым упрощается повторное использование классов.
Класс изолирован от других частей программы. Его закрытые элементы нахо дятся вне пределов досягаемости других функций (подобно локальным перемен ным функции или блока).
Это свойство уменьшает необходимость координации ме>кду разработчиками ПО и снижает вероятность неверного понимания при таком взаимодействии. В итоге улучшается качество ПО.
Глава 9 • Классы C-i-4- как единицы модульности программы |
357 |
|
public: |
|
|
void setCylincler(double г, double h); |
|
|
double getVolumeO; |
|
|
void |
scaleCylinder(double factor); |
|
void |
printCylinderO; |
|
} ; |
/ / конец области действия |
класса |
Некоторые программисты говорят, что ключевое слово struct хуже, чем class, так как при определении класса с использованием прав доступа по умолчанию, данные не будут защиндены от использования клиентом, что вредит инкапсуляции.
struct |
Cylinder { |
/ / |
используются права доступа по умолчанию |
|
double radius, height; |
/ / |
данные не защищаются от доступа из клиента |
||
void |
setCylinder(double г, |
double h); |
/ / методы общедоступные |
|
double getVolumeO; |
|
|
|
|
void |
scaleCylinder(double |
factor); |
|
|
void |
printCylinderO; |
|
|
|
} ; |
|
/ / |
конец области действия класса |
|
При такой конструкции класса проигрывает инкапсуляция, но это же не доказы вает, что ключевое слово struct хуже, чем class! Если здесь заменить struct на class, то результат будет еще хуже. Видите, почему?
class |
Cylinder { |
/ / |
используются права доступа по умолчанию |
|
double radius, height; |
/ / |
данные защищаются от доступа из клиента |
||
void |
setCylinder(double г, |
double h); |
/ / методы недоступны |
|
double getVolumeO; |
|
|
|
|
void |
scaleCylinder(double |
factor); |
|
|
void |
printCylinderO; |
|
|
|
} ; |
|
/ / |
конец области действия класса |
|
Такой класс вообще нельзя использовать. Да, поля данных теперь закрыты (и это превосходно), но недоступны и функции-члены, и клиент не может вызы вать их. Определенно, не очень хорошая конструкция.
Вероятно, предпочтительнее не полагаться на права по умолчанию и назначать права доступа явно. Так что будем называть вещи своими именами.
Инициализация экземпляров объекта
Когда компилятор обрабатывает определение переменной, он использует для выделения требуемого объема памяти определение типа. Память выделяется из динамически распределяемой области (для переменных static и extern или для динамических переменных) или из стека (для локальных автоматических пере менных).
Это относится к простым переменным, массивам, структурам и классам с функциями-членами. Если позднее программа присваивает переменной значе ние, то она не нуждается в инициализации (как в случае определения), но когда алгоритм использует переменную как г-значение, элементам данных необходимы начальные значения.
Cylinder с1; |
/ / |
элементы данных не инициализируются |
double vol = с1.getVolumeO; |
/ / |
нет, нехорошо |
Такой прием программирования может не подойти, если в вычислениях нужно использовать некоторые значения по умолчанию. С++ инициализирует только статические и глобальные переменные (нулями соответствующего типа). Дина мические и автоматические переменные остаются без начальных значений.
358 |
Чость II ^ Объектно-ориентированное riporpaf^f^NpOBOHiie на С+-!- |
|||
|
Иногда желательно определить значения по умолчанию. Хорошо было бы ини |
|||
|
циализировать элементы данных в определении, подобно обычным переменным, |
|||
|
но в C++ определения элементов данных не могут содержать инициализатор. |
|||
|
class Cylinder |
{ |
|
|
|
double radius |
= 100, heihgt |
= 0; .. . |
/ / нет, в C++ это недопустимо |
|
Класс может предусматривать функцию-член, позволяющую клиенту задать |
|||
|
начальное состояние объекта: |
|
|
|
|
class Cylinder |
{ |
|
|
|
double radius, heihgt; |
|
|
|
|
public: |
|
|
|
|
void setCylinder(double r, double h); |
. . . } ; |
||
|
С помош,ью этой функции клиент мог бы передавать сообщение setCylinder() |
|||
|
объектам Cylinder. |
|
|
|
|
Cylinder с1; |
|
|
|
|
c1.setCylinder(100.0,0.0); |
/ / |
присваивает radius значение 100, |
|
|
|
|
/ / |
а height ноль |
Конечно, это перебор. Такой код позволяет задавать любые начальные значения, а не указанные по умолчанию. Здесь становятся полезными конструкторы.
Конструкторы как функции-члены
Объекты класса могут инициализироваться неявно, с помош^ью конструктора. Конструктор — это функция-член класса, но она имеет более строгий синтаксис, чем другие функции-члены. Конструктор не может иметь произвольное имя. Оно должно соответствовать имени класса. Интерфейс конструктора не может специ фицировать возвраш,аемый тип (даже void) и возвраш,ать значения, даже если содержит оператор return.
class |
Cylinder { |
|
|
double radius, |
height; |
|
|
public: |
|
|
|
Cylinder () |
/ / т о |
же имя, что и у класса, нет возвращаемого типа |
|
{ |
radius=1.0; |
heihgt=0.0; } |
/ / нет оператора возврата |
..• |
} ; |
|
|
Когда клиент создает объект, вызывается конструктор, заданный по умолчанию.
Cylinder с1; / / конструктор по умолчанию; нет параметров
Он называется конструктором по умолчанию, так как не имеет параметров. Странная причина, но это так.
Конструктор нельзя вызывать явно, как любую другую функцию-член.
d . C y l i n d e r O ; / / синтаксическая ошибка: явно вызывать конструктор нельзя
Конструктор может вызываться только при создании объекта, но не позднее. Компилятор генерирует код, явно вызывающий конструктор сразу после создания экземпляра объекта, поэтому конструкторы обычно включаются в раздел public спецификации класса. В противном случае попытка создать экземпляр класса даст ошибку, как любой доступ к частному компоненту класса.
В общем случае экземпляр объекта можно создавать:
• В начале программы (объекты extern и static)
•На входе в область действия, содержащую определение объекта (автоматические объекты)
