
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf490 |
Часть III • Протратштроваишв с агрег}АроваиУ1ем и иасАВАОвонтвт |
Данный класс предоставляет клиентам возможность перемещения объектапрямоугольника по экрану, изменения толидины линии границы прямоугольника и проверки попадания точки внутрь прямоугольника. Кроме того, он может опре делить объекты класса Rectangle путем спецификации координат углов. Он пере мещает точку и прямоугольник по экрану, пытаясь "поймать" ее в прямоугольник.
i nt |
х1=20,у1=40; int |
х2=70,у2=90; |
/ / |
левый верхний/правый нижний углы |
|||
int |
х=100, |
у=120; |
|
/ / |
точка для отлова.в |
прямоугольник |
|
Rectangle |
гес(х1,у1,х2,у2,4); |
/ / |
создание объекта Rectangle |
||||
rec.setThicknessO; |
|
/ / толщина линии - 1 пиксель |
(по умолчанию) |
||||
X -= 25; у -= 15; |
|
/ / |
перемещение точки |
по экрану |
|||
rec.move(10,20); |
|
/ / |
10 пикселей вправо, 20 вниз |
||||
i f |
(re.pointIn(x,y)) |
cout « "Точка |
внутри\п"; |
/ / точка в прямоугольнике'?' |
Внутренняя структура класса Rectangle является сложной структурой. При написании этого фрагмента намеренно сделаны ошибки: перепутаны х1 и у1, х2 и у2 и т. д. Программист, занимающийся клиентской частью, сразу поймет, что для спецификации объекта Rectangle (пять значений в вызове конструктора) тре буется много работы. Причина такой сложности класса Rectangle и его клиента состоит в отсутствии реализации компонента — класса Point (точка). Концепция точки здесь выглядит вполне естественно, она даже присутствует в комментариях класса Rectangle и его клиента, но не поддерживается с помощью определяемого программистом типа.
Синтаксис C++ для композиции класса
Рассмотрим тот же пример, однако обратим внимание на класс Point, пред лагающий клиенту некоторые новые сервисы. Представим, что это фрагмент большой программы, над разными частями которой работает множество людей. Сосредоточьтесь на синтаксисе композиции класса и на вопросах, связанных со взаимодействием между классами и их разработчиками.
class |
Point { |
|
|
|
|
|
||
private: |
|
|
|
|
|
|
|
|
int |
X, |
у; |
|
|
|
|
/ / |
закрытые координаты |
public: |
|
|
|
|
|
|
|
|
Point |
(int |
a, |
int |
b) |
|
/ / |
обобщенный конструктор |
|
{ |
X = a; |
у = b; |
} |
|
|
|
||
void |
set (int |
a, |
int b) |
/ / |
функция-модификатор |
|||
{ |
X = a; |
у = b; |
} |
|
|
|
||
void |
move (int |
a, |
int |
b); |
/ / |
функция-модификатор |
||
{ |
X += a; |
у += b; } |
|
|
|
|||
void |
get (int& |
a, |
int& |
b) const |
/ / |
функция-селектор |
||
{ |
a = x; |
b = y; |
} |
|
|
|
||
bool |
isOrigin |
() |
const |
|
/ / |
функция-предикат |
||
{ |
return |
X == 0 && у == 0; } } |
; |
|
Для функций-членов здесь используется общепринятая терминология. Моди фикатор — это функция-член, изменяющая состояние целевого объекта (вы заметили отсутствие ключевого слова const?). Селектор — функция-член, не изменяющая состояние целевого объекта (как видно, ключевое слово const в ней присутствует). Предикат представляет собой селектор, возвращающий булево значение, которое сообщает о состоянии целевого объекта (в данном случае го ворит о том, что точка является началом координат).
В этом примере применяются распространенные имена функций-членов. Тем самым иллюстрируется, что границы класса эффективно ограничивают конфликты имен в программе. При выборе имени set() ддя функции-члена класса Point об
Глава 12 • Преимущества и недостатки составных классов |
491 |
этом ненужно уведомлять всех разработчиков других классов приложения. Они также могут использовать в своих классах имя set(). О б имени set() должны знать лишь те, кто использует класс Point в качестве сервера. Одним изтаких клиентских классов является класс Rectangle, представленный в начале главы. В показанной ниже версии класса Rectangle имеются два элемента данных класса Point, обозначаюш.их верхний левый и нижний правый углы прямоугольника. Элементданных thickness обозначаеттолщину линии при выводе прямоугольника на экран.
class Rectangle { |
// координаты верхней левой точки |
Point pt1, pt2; |
|
int thickness; |
// толщина границы прямоугольника |
public:
Rectangle (int inX1, intinYl, int inX2, int inY2, int wid=1);
void move(int a, intb); |
|
II |
перемещение прямоугольника |
void setThickness(int width = 1 ) ; |
// изменение толщины |
||
bool pointIn(int X, int y) const; |
// точка в прямоугольнике? |
||
} ; |
|
// остальная часть Rectangle |
|
Rectangle::Rectangle (int inX1, int |
inY1, int inX2, int inY2, int width) |
||
{ pt1.set(inX1, inY1); pt2. set(inX2, inY2); |
//перенос обязанностей на сервер |
||
thickness = width; } |
|
|
// установка элементов данных |
void Rectangle::move(int a, int b) |
|
// перемещение каждого угла |
|
{ pt1.move(a,b); pt2.move(a,b); |
} |
|
|
void Rectangle::setThickness(int |
width) |
// выполнение работы |
|
{ thickness = width; } |
|
|
|
bool Rectangle::pointIn(int x, int y) const |
// точка внутри? |
||
{ int х1,у1,х2,у2; |
|
// координаты углов прямоугольника |
|
pt1.get(x1,y1); pt2.get(x2,y2); |
// получение данных точки |
bool xIsBetweenBorders = (хКх &&х<х2 11 (х2<х &&х<х1); bool ylsBetweenBorders = (у>у1 &&у<у2) || (у<у1 &&у>у2); return (xIsBetweenBorders &&ylsBetweenBorders); }
Здесь класс Rectangle изменился. Эти изменения представляют обш.ие идиомы программирования для композиции класса.
Вместо набора присваиваний нижнего уровня многочисленным элементам
данных встроенных типов в конструкторе Rectangle передаются всего лишь два сообщения объектам-компонентам.
pt1.set(inX1,inY1), pt2.set(inX2,inY2); |
// перенос обязанностей на сервер |
Это пример изоляции клиента (класса Rectangle) от деталей архитектуры сервера (класса Point). Клиент написан в терминах передачи сообш,ений серверным объектам. О н не использует детали архитектуры сервера нижнего уровня. В коде клиента говорится, что нужно сделать.Детали операций перенесены изкласса Rectangle в класс Point. Такой стиль упрош^ает понимание кJшeнтcкoй программы.
Метод moveО представляет еще более интересную идиому C + + для связи
между составными и компонентными классами. Когда у объекта Rectangle за
требовано перемещение, он просит свои компоненты проделать необходимые действия, вызывая метод с тем же именем move(). Второй метод move()принад
лежит к компонентному классу Point, а не ксоставному классу Rectangle. Таким
образом, здесь показан еще один пример равнозначной интерпретации в C + + объектов разной природы. В данном случае методы с одним и тем же именем
принадлежат классам с аналогичным поведением. Перемещение прямоугольника означает перемещение каждой его точки. Возможность писать методы, передаю- ш.ие информацию своим элементам данных,— одна из причин,почему оба метода
можно называть move(), а не movePointO и moveRectangle().
Глава 12 • Преимущества и недостатки составных классов |
493 |
||||
Rectangle гес(х1,у1,х2,у2,4); |
/ / |
создание объекта Rectangle |
|||
rec.setThicknessO; |
/ / |
линия толщиной в пиксель |
(по умолчанию) |
||
X -= 25; у -= 15; |
/ / |
перемещение точки по экрану |
|||
rec.move(10,20); |
/ / |
10 пикселей |
вправо, |
20 вниз |
|
i f (rec.pointIn(x,y)) cout « |
"Точка внутри\п"; |
/ / точка |
в прямоугольнике? |
В этом маленьком примере различия также невелики и понятны. Подобно первой версии класса Rectangle, этот клиент выполняет обработку в терминах отдельных сущностей х и у. Данный код не агрегирует их в класс и не передает сопровождающему приложение программисту замыслы проектировщика, не со общает, как соотносятся отдельные элементы, и ничего не говорит о том, что они представляют координаты одной точки. Если клиенту потребуются две точки, как при перемещении, сравнении и т. д., то эти действия следует реализовывать в клиенте через отдельные координаты. Такие индивидуальные операции низкого уровня усложняют клиентскую часть и затрудняют понимание смысла действий. Возьмем оператор клиента rec.move(10, 20);. Здесь ясно видно, что прямоуголь ник перемещается. То, что должна перемещаться точка с координатами (100,120), придется уяснить из серии присваиваний х -= 25; у -= 15;. Эти обязанности низко го уровня не перенесены на уровень сервера.
Выражение клиента в терминах объектов класса Point и операций с ними делает программный код более объектно-ориентированным.
Point |
р1(20,40), р2(70,90); |
/ / |
углы прямоугольника |
|
Point |
point(100,120); |
/ / |
точка для отлова в прямоугольнике |
|
Rectangle гес(р1,р2,4); |
/ / |
ниже рассказывается о возможных проблемах |
||
rec.setThicknessO; |
/ / |
линия толщиной в пиксель (по умолчанию) |
||
point.move(-25,-15); |
/ / |
перемещение точки по экрану |
||
rec.[Tiove(10,20), |
/ / |
10 пикселей |
вправо, 20 вниз |
|
i f (гее.pointIn(x,y)) cout « |
"Точка внутри\п"; |
/ / точка в прямоугольнике? |
Здесь два сервера — классы Point и Rectangle. То, что перемещается точка point класса Point, не менее ясно, чем перемещение объекта гее класса Rectangle. Операции низкого уровня перенесены на серверы, а клиент выражается в терми нах вызовов функций.
В этом фрагменте необходимы интерфейсы класса Rectangle, доступ к кото рым не предусматривается. Класс Rectangle предлагает конструктор с пятью параметрами, а клиент подставляет только три. Класс Rectangle ожидает двух аргументов для функции pointInO, а клиент допускает один. Проблему можно разрешить, изменив вызовы функций в клиенте или интерфейсы функций в сер вере Rectangle.
Если бы класс Rectangle был библиотечным и изменить его было бы невоз можно, то клиенту пришлось бы обходить ограничения библиотеки. Если же класс Rectangle представляет собой один из вспомогательных классов, разрабатываемых для приложений, его можно изменить. С точки зрения объектно-ориентированной идеологии именно серверный класс (здесь Rectangle) должен соответствовать ожиданиям и требованиям клиентской части. Класс Rectangle следует переписать следующим образом:
class |
Rectangle { |
|
|
Point p t i , pt2; |
/ / |
координаты верхней левой точки |
|
int |
thickness; |
/ / |
толщина границы прямоугольника |
public: |
|
|
|
Rectangle (const Point& pi, const Point& p2, int widt^i^l); |
|||
void move(int a, int b); |
/ / |
перемещение прямоугольника |
|
void |
setThickness(int width = 1 ) ; |
/ / |
изменение толщины |
bool |
pointIn(const Point& pt) const; |
/ / |
точка в прямоугольнике? |
. |
. . . } ; |
/ / |
остальная часть Rectangle |
496 |
Часть HI • Програл^1М111ро8ание с агрегированием и насАВАОвантвт |
Следовательно, нельзя инициализировать элементы данных в спецификации класса, используя синтаксис, подходящий для инициализации переменных C++. Это могут делать программисты, работающие с Java, но не с C+ + .
class |
Point |
{ |
|
/ / |
недопустимый синтаксис |
инициализации |
int |
х=100, |
у=100; |
|
|||
public: |
|
|
/ / |
подходящий способ инициализации |
||
Point (int |
a, int |
b); |
||||
{ |
X = a; |
у = b; |
} |
/ / |
остальная часть класса |
Point |
|
|
|
|
Синтаксис определения экземпляров объектов и элементов данных в состав ных классах будет таким же. Следующая строка может встречаться как в теле функции, так и в определении класса:
Point pt1, pt2; / / может быть в функции, блоке или классе
Подстановка аргументов для |
инициализации допускается только при определе |
нии экземпляров объекта в области действия функции или блока. |
|
Point pt1(20,40),pt2(70,90); |
/ / OK в функции или в блоке, но не в классе |
Следовательно, элементы данных в составных классах не могут инициализировать ся в спецификациях класса с помощью синтаксиса, подходящего для инициали зации объектов компонентного класса. В следующем примере инициализируются компоненты Point класса Rectangle с применением синтаксиса, подходящего для переменной Point. Компилятор отвергает такой синтаксис.
class Rectangle { Point pt1(20,40); Point pt2(70,90); int thickness = 1;
public:
Rectangle (const Point& p1, const Point& p2, int width = 1 ) ;
void move(int a, int b); |
// остальная часть класса Rectangle |
. . . . } ; |
Вместо этого C + + предлагает два способа инициализации компонентов состав ного класса. Один из них состоит в присваивании значений втеле конструктора компонентного класса. Конструктор может присваивать значения соответствую
щ и м элементам данных, будь то компоненты составных типов иликомпоненты
встроенных типов данных.
Rectangle::Rectangle (const Point& p1,const Point& p2,int width)
{ pt1 = p1; pt2 = p2; |
// присваивает значения компонентам |
thickness = width; } |
// составного типа |
// присваивает значения компонентам |
|
|
// встроенных типов |
Другой способ — использовать список инициализации компонентов, вызыва
ющего конструктор компонентов класса. В следующем примере список инициали
зации компонентов вызывает конструктор Point для инициализации компонентов Point класса Rectangle.
Rectangle::Rectangle (const Point& p1, const Point& p2, int width)
: pt1(p1), pt2(p2); |
// вызов конструкторов для компонентов |
{ thickness = width; } |
// присваивает значения встроенным компонентам |
|
Глава 12 • Преимущества и недостатки составных классов |
497 |
О с т о р о ж н о ! Синтаксис определения переменных и объектов в C++ |
|
|
^Р |
и синтаксис определения компонентов класса одинаков. Однако его нельзя |
|
использовать для инициализации переменных и объектов C++ |
|
|
при определении компонентов класса. Следует применять используемый |
|
|
по умолчанию конструктор или список инициализации компонентов. |
|
Сначала обсудим детали инициализации в теле конструктора, а потом перейдем к списку инициализации.
Применение используемых по умолчанию конструкторов компонента
в главе 9 рассказывалось, что при создании объекта C-f 4- для его компонентов выделяется память. Если не углубляться в вопросы выравнивания памяти для хранения значений, можно предположить, что выделенная объекту память равна сумме размеров его элементов данных.
Говорилось также, что при создании объекта С+4- его элементы данных ини циализируются в вызове конструктора, и подчеркивалось, что конструктор вызы вается после создания всех элементов данных объекта.
Можно допустить, что для классов, которые имеют только несоставные поля встроенных типов, различие между "во время" и ''после''' не очень существенно. Оно подобно различию между присваиванием и инициализацией. Для несоставных элементов встроенных типов различие почти незаметно. Как уже показывалось в главе 11, оно может стать очень важным для классов с динамическим распре делением памяти. Если не различать их, могут возникнуть проблемы с произво дительностью и целостностью программы.
Различие между "во время'' и ''после" приобретает важность для объектов, компоненты которых являются объектами определяемых программистом классов. В данном разделе рассмотрен процесс создания составного объекта.
При создании объекта C + + выделяется память его элементам данных, а затем выполняется тело конструктора. Это означает, что конструктор для объекта вызы вается после создания всех элементов данных.
Важной характеристикой этого процесса является то, что элементы данных создаются в порядке их следования в спецификации класса. Когда процесс закан чивается, объект составного типа выглядит в памяти как сумма его компонентов.
Это означает, что изменение порядка компонентов в классе может повлиять на его свойства. Однако имейте в виду, что это возможно лишь в случае, если элементы данных зависят друг от друга. Например, один элемент данных может представлять число компонентов в другом.
Когда элементы данных создаются последовательно, друг за другом, поля данных встроенных типов (если они имеются) либо остаются неинициализиро ванными (когда память для объекта выделяется в стеке или в динамически рас пределяемой памяти), либо устанавливаются в О (для объектов, созданных как глобальные или статические). Если объекту необходимо сохранить в поле встроен ного типа конкретное значение, об этом нужно позаботиться при вызове конструк тора. Например, поле thickness для объектов класса Rectangle устанавливается в значение, заданное параметром конструктора width.
Что же происходит, если объект представляет собой составной объект, вклю чающий элементы данных, которые являются объектами других классов? Приве денное выше замечание о создании объектов в порядке их следования может сбить с толку. Процедура создания объекта рекурсивна. После создания элемента данных вызывается конструктор.
Вспомните, что ни один объект языка C + + не может создаваться без вызова конструктора, следующего за выделением памяти. Если поле составного объекта содержит данные определяемого программистом типа (структуры или класса), то
498 л Часть III« Програ^г^ирование с агрешровоииег^! и наследованием
конструктор вызывается сразу после выделения памяти для поля и перед си.ща- нием следуюидего поля. После успешного создания всех полей составного объекта выполняется тело конструктора составного класса.
При создании объекта Rectangle события разворачиваются в следуюш,ем по рядке:
1. Создается элемент данных pt1 класса Point.
2 . Создается элемент данных pt2 класса Point.
3 . Создается элемент данных thickness типа int.
4 . Выполняется тело конструктора класса Rectangle.
Когда в процессе построения объекта Rectangle создается каждый элемент данных класса Point, это происходит так:
1. |
Создается |
элемент данных |
х типа |
int. |
2 . |
Создается |
элемент данных |
у типа |
int. |
3 . |
Выполняется тело конструктора класса Point. |
Перед выполнением конструктора класса Rectangle дважды вызывается конст руктор класса Point: в первый раз для инициализации полей элемента данных pt1, во второй — для инициализации полей элемента данных pt2.
Заметим, что при уничтожении составного объекта процесс управления памятью и вызова функций происходит в обратном порядке. Сначала, до освобождения ка кой-либо памяти, вызывается деструктор составного класса. Когда этот деструк тор завершает работу, элементы данных уничтожаются в порядке, обратном их созданию. Уничтожение каждого элемента данных выполняется рекурсивно. Перед освобождением памяти компонента вызывается его деструктор. После заверше ния деструктора компонента уничтожаются элементы данных (от последнего в спе цификации компонентного класса к первому).
Таким образом, при уничтожении объекта Rectangle последовательность со бытий будет зеркальным отражением событий, имевших место при его создании.
1. Выполняется деструктор класса Rectangle.
2 . Уничтожается элемент данных thickness.
3 . Для элемента данных pt2 вызывается деструктор Point.
4 . |
Уничтожается элемент данных |
pt2 |
(сначала |
поле у, потом поле х). |
|
5 . |
Для элемента |
данных pt1 вызывается деструктор Point. |
|||
6. |
Уничтожается |
элемент данных |
pt1 |
(сначала |
поле у, потом поле х). |
При описании процесса создания объекта Rectangle можно было с уверен ностью говорить о вызове конструктора Rectangle, поскольку класс Rectangle имеет только один конструктор. Когда речь идет о выполнении конструктора класса Point, такой уверенности нет. Какой конструктор вызывается при создании полей объекта класса Point?
Как и в случае функции C+ + , ответ зависит от числа аргументов, подставляе мых клиентом в вызове конструктора. Концептуальным "камнем преткновения" для многих программистов является то, что при отсутствии аргументов они не видят, какой именно конструктор вызывается. Между тем, когда нет аргументов, вызывается конструктор без аргументов, т. е. конструктор по умолчанию. Если указывается один аргумент, то задается конструктор с аргументом данного типа и т. д. А что бывает в случае, если конструктор с требуемой сигнатурой недо ступен? Подобно вызову любой функции C + + с некорректной сигнатурой, это означает, что вызов функции (попытка создания объекта) даст синтаксическую ошибку.