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

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

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

490

Часть 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().

492

Часть III « Програтттрошаиив с атрвгироваиивт и наследованием

Доступ к элементам данных элементов данных класса

Еще одно важное отличие этой архитектуры класса Rectangle от его предыду­ щей версии состоит в доступе к компонентам компонентов класса. В предыдущей версии класс Rectangle мог делать со своими координатами х и у все что угодно. Они были непосредственно доступны. В последней версии Rectangle они являются компонентами класса Point. Если бы компонентный объект (в данном примере класса Point) имел общедоступные компоненты, то составной класс (Rectangle) мог бы обращаться к элементам данных своего компонентного объекта с помощью операции-селектора (точки). Если бы компоненты класса Point были общедо­ ступными, функция-член Rectangle: :pontIn() могла бы использовать уточненные имена компонентов Point. Таким образом, класс Point может определить, нахо­ дится ли параметр х между jc-координатами элементов данных Rectangle — pt1 и pt2.

bool xIsBetweenBorders

= (pt1.x<x && x <pt2.x)

I I (pt2.x<x

&& x<pt1.x)

Элементы данных Point являются закрытыми, a у^клиентского класса (в при­ мере это Rectangle) нет специальных полномочий на доступ к компонентам сер­ вера (Point). Класс Rectangle использует объекты Point как свои собственные компоненты. Следовательно, его методы могут обращаться к элементам данных Point (pt1 и pt2). Однако методы класса Rectangle не могут обращаться к элемен­ там данных X и у класса Point. Написанная выше строка не является допустимой. Для обращения к компонентам Point в методах Rectangle используйте общедо­ ступные функции-члены, например Point: :get().

Не путайте два разных контекста. Класс Rectangle может без ограничений обращаться к своим собственным закрытым компонентам Point pt1 и pt2, но не к закрытым компонентам своих элементов данных pt1.x, pt1.y, pt2.x и pt2.y. Именно поэтому Rectangle: :pointIn() использует данный код для получения элементов данных Rectangle pt1 и pt2.

bool

Rectangle::pointIn(int x, int y) const

 

{ int

x1,y1,x2,y2;

/ /

координаты углов

pt1.get(x1,y1); pt2.get(x2,y2);

/ /

получить данные точки

bool XIsBetweenBorders = (xKx && x<x2)

11 (x2<x && x<x1);

. .

. }

/ /

и T. Д.

Необходимость использовать серверные функции для доступа к элементам данных класса часто раздражает. Это может сделать работу по проектированию методов составных классов весьма трудоемкой и утомительной.

О с т о р о ж н о ! Если класс содержит элементы данных, принадлежащие другим классам, то функции-члены класса не могут обращаться к закрытым компонентам этих элементов данных. Составной класс должен использовать для доступа к таким элементам данных методы, которые к ним обращаются.

Рассмотрев архитектуру клиента класса Rectangle, можно заметить, что для нее не нужны никакие изменения. Клиент должен подставлять пять аргументов конструктору Rectangle и два аргумента методу Rectangle: :pointIn(). Это озна­ чает, что введение компонента класса Point положительно отражается на архитек­ туре составного класса Rectangle, но не дает преимуществ в плане архитектуры клиента.

int

х1=20,

у1=40;

int х2=70, у2=90;

/ /

углы

прямоугольника

int

х=100,

у=120;

 

/ /

точка,

отлавливаемая в прямоугольнике

Глава 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

494

Часть 111 ^ npcroGi'/rvitiac.>i.-fVia с агрегированием и наследс

 

 

 

Rectangle:[Rectangle

(const

Polnt&

p1, const

Point&

p2,

int width)

 

{

pt1 = p1; pt2 = p2;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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(const

Point& pt)'const

 

/ /

точка внутри?

 

{

int

Х,у,х1,у1,х2,у2;

 

 

 

 

 

/ /

координ^ы

углов

прямоугольника

 

 

pt.get(x,y);

 

 

 

 

 

 

/ /

получить координаты

параметра

 

 

pt1.get(x1,y1); pt2.get(x2,у2);

 

/ /

получить оба угла

 

 

 

 

bool xIsBetweenBorders

= ( х К х

&& х<х2

| | (х2<х && х<х1);

 

 

 

 

bool ylsBetweenBorders

= (у>у1 && у<у2)

| |

(у<у1 && у>у2);

 

 

 

 

return (xIsBetweenBorders

&& ylsBetweenBorders);

}

 

 

 

Обратите внимание на ключевое слово const (и его отсутствие) в классах Point и Rectangle. Оно отражает изменение (или отсутствие изменений) в целевом объекте и параметрах вызова функции. Так как параметр функции в этом вариан­ те не возвращает указатели или ссылки на объекты, нет никакой необходимости использовать для возвращаемых значений ключевое слово const.

Обобщенный конструктор класса Rectangle может вызываться с двумя или с тремя параметрами. При вызове с двумя параметрами элемент данных thickness устанавливается в значение по умолчанию (единица).

Доступ к элементам данных параметров метода

Обратите внимание, что параметры метода интерпретируются в С+4- анало­ гично элементам данных составного класса. Параметры функции-члена могут иметь любой тип, включая объекты классов. Ограничения не накладываются на режимы параметров для объектов классов. Они могут передаваться по значению, по ссылке, по указателю и при необходимости могут иметь модификатор const.

При доступе к параметрам-объектам в функции-члене соблюдаются те же правила, что и при доступе к другим объектам. Разрешается обращаться только к компонентам public. Сам параметр доступен методу, но его закрытые компо­ ненты — нет. Если клиенту параметра (функции-члену) нужен доступ к закрытым компонентам сервера, используйте функции-члены серверного класса.

Именно поэтому для доступа к компонентам параметра pt.x и pt.y функциячлен pointInO в Rectangle использует функцию доступа get() класса Point.

Если другой объект, к которому производится доступ, представляет объект того же класса, то в C++ имеется важное исключение. Оно применяется, когда объект передается как параметр функции-члена класса, к которому принадлежит этот объект. Если клиентский и серверный классы являются классами одного типа, то объект клиента имеет полные права доступа к компонентам объекта-параметра. Предположим, что нужно добавить к классу Point функцию-член isSamePoint(). С ее помощью сравниваются координаты целевого объекта и объекта-параметра, возвращается true, если они содержат одинаковые значения, и false в противном случае.

bool Point::isSamePoint(const Point& p) const

' / / сравнение данных

{ return x==p.x && у == p.у; }

 

По существу, доступ к другому экземпляру объекта (в данном случае — р в isSamePointO) происходит в области действия целевого объекта (типа Point). Следовательно, он разрешается.

Глава 12 • Преимущества и недостатки составных классов

495

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

Согласно правилу, закрытые компоненты объекта не должны быть доступны извне, поэтому другой компонент того же класса должен использовать функции доступа. Такую функцию можно написать следующим образом:

bool

Point::isSamePoint(const

Point& pt)

const

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

{ int

x1, y1;

 

 

 

pt.get(x1,y1);

/ /

излишне: доступ через функцию-член

bool answer = (x==x1 && y==y1); return answer; }

Чтобы избежать такого ненужного, создающего лишние трудности кода, С+ + допускает незначительную несогласованность в правах доступа. Данная версия isSamePointO синтаксически корректна. Программист, работа которого оценива­ ется по количеству строк исходного кода, пишет огромное число операторов, и его программа будет выглядеть примерно так. Другой вопрос, как этот объем влияет на качество ПО.

Инициализация составных объектов

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

При описании синтаксиса для определения глобальных переменных в главе 3 рассматривалось присваивание переменным начальных значений. В главе 5 при пояснении синтаксиса составных данных (структур, массивов, перечислений, объединений и битовых полей) обсуждалась инициализация этих программных компонентов. Кроме того, говорилось об инициализации объектов.

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

Как уже упоминалось в главе 9, синтаксис C++ является одинаковым для определения переменных и элементов данных класса. Следуюидая строка интерп­ ретируется как допустимая в области действия функции и в области действия класса.

int х,у; / / может быть в функции или блоке, а может быть и в классе

Между тем определение с инициализацией может присутствовать в выполняе­ мом коде только в функции или в блоке.

int х=100, у=100;

/ / может быть в функции или в блоке, но не в классе

/ / некорректная спецификация класса / / допускается в клиенте, но не здесь / / та же проблема / / та же проблема

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 + + с некорректной сигнатурой, это означает, что вызов функции (попытка создания объекта) даст синтаксическую ошибку.

Глава 12 • Преимущества и недостатки составных классов

499

В следующем фрагменте клиента можно видеть, что программа передает пара­ метры для вызова конструктора Rectangle, однако здесь нет параметров, которые отправляли бы данные конструкторам Point.

Point р1(20,40), р2(70,90);

/ /

верхний левый и нижний правый углы

Rectangle гес(р1,р2,4);

/ /

это синтаксическая ошибка

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

В нашем примере класс Point не имеет конструктора по умолчанию. Вспом­ ним, однако, что в этом случае конструктор по умолчанию для класса подставляет компилятор. Данный конструктор ничего не делает, но позволяет клиенту созда­ вать объекты, не передавая аргументы конструктору. И это хорошие новости. Теперь вспомним, что если класс имеет отличные от используемых по умолчанию конструкторы (у класса Point есть один такой конструктор), то компилятор не под­ ставляет свой конструктор. Это плохие новости. Определение объекта составного класса даст синтаксическую ошибку. Вот почему последняя строка в приведенном выше примере ошибочна.

Данный пример показывает связь между классами (Point и Rectangle), которая не очевидна для программистов. Некоторые полагают, что ошибка в определении класса Rectangle вызвана его попыткой создать несуш,ествуюш,ий конструктор класса Point. Между тем компиляция класса Point и класса Rectangle не дает синтаксической ошибки.

Рекомендуется искать источник ошибки в архитектуре компонентного класса Point. Именно этот класс не имеет конструктора по умолчанию. Однако логиче­ ская ошибка проявляется как синтаксическая не в архитектурах компонентного класса Point и составного класса Rectangle, а в клиенте класса Rectangle при попытке создания экземпляра объекта составного класса. Пока клиент не попыта­ ется определить объект Rectangle, никакой синтаксической ошибки не будет.

Чтобы исправить ситуацию, можно добавить к классу Point конструктор по умолчанию. Это устранит синтаксическую ошибку в последнем фрагменте.

class Point

{

 

 

 

 

 

 

int X,

у;

 

 

 

/ /

закрытые координаты

public:

 

 

 

 

 

 

 

 

Point

О

 

 

 

 

 

 

{

х=0; у=0;

}

 

/ /

конструктор

по умолчанию

Point

(int

а,

int

b)

/ /

обобщенный

конструктор

{

X = а;

у = b;

}

 

 

 

. . . }

;

 

 

 

 

/ /

остальная часть класса Point

Еш,е одно решение состоит в добавлении в обобш,енный конструктор значений аргументов по умолчанию. Таким образом, он будет представлять собой исполь­ зуемый по умолчанию конструктор и конструктор преобразования.

class Point {

// закрытые координаты

int X, у;

public:

 

 

 

 

Point (int а=0, int b=0)

 

 

 

{

x=a;

y=b; }

/ /

конструктор no умолчанию, конструктор

 

 

 

/ /

преобразования,

обобщенный конструктор

.

. . }

;

/ /

остальная часть

класса Point

Проанализируем шаги создания объекта Rectangle. На рис. 12.1 показаны действия при выполнении следующ,его фрагмента:

Point р1(20,40), р2(70,90);

/ /

верхний левый и нижний правый углы

Rectangle гес(р1, р2,4);

/ /

ОК, если Point имеет конструктор по умолчанию

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