
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf510 |
Часть Hi ^ Програтттро&ание с агрегированием и наследованием |
|
то их совместное использование с другими объектами Rectangle не дает никаких |
|
преимуществ. Их можно сделать компонентами-константами. |
|
class Rectangle { |
const |
Point& pt1; |
/ / |
точки не используются |
|
|
/ / |
совместно с другими фигурами |
const |
Point& pt2; |
/ / |
точки не могут изменять свои координаты |
|
|
/ / |
остальная часть класса Rectangle |
Ссылку на объект-константу можно применять как метод оптимизации. Если большое количество составных объектов должно содержать один и тот же объекткомпонент, имеет смысл создать только один такой объект, а во всех составных объектах использовать ссылки на данный объект.
Процесс построения объекта класса Rectangle представлен на рис. 12.5. Сна чала создаются объекты типа Point (см. рис. 12.5(A)), затем — объект Rectangle (см. рис. 12.5(B)): задаются ссылки pt1 и pt2. Их тип отличается от типа в преды дущем примере. Используются другие размеры фигур, чтобы показать разницу типов. Выполняется список инициализации, и ссылки устанавливаются на объек ты Point, создается и инициализируется поле-константа weight, а затем — файл thickness. Вызывается конструктор Rectangle (см. рис. 12.5(C)). Присваивается значение поля thickness.
А) |
|
|
Point p1 (70,90); |
В) |
|
|
a) Выделение памяти для ссылки pt1 |
|
|
b) Ссылка на объект р1: pt1(p1) |
|
|
|
|
c) Выделение памяти для pt2 |
|
|
|
d) Ссылка на объект р2: pt2(p2) |
|
|
|
e) Создание weight |
|
|
|
f) Инициализация: weight(wt) |
|
|
|
g) Создание thickness |
С) |
pt1 |
|
Выполнение |
|
Pt2 |
|
конструктора Rectangle |
|
weight |
0.01 |
|
|
ickness |
4 1 |
thickness = weight; |
Рис. 12.5. Этапы |
создания |
объекта Rectangle со ссылками |
на внегиние объектны Point
В данной архитектуре ссылки в классе Rectangle представляют собой константы, но объекты класса Point, на которые они указывают, не являются константами. Следовательно, они могут менять свое состояние. Все объекты Rectangle, ассо циированные с этими объектами Point, могут изменять свое положение на экране. За счет ссылок объект Rectangle не может потерять связанный с ним объект Point и использовать вместо него другой объект Point. Это возможно, если класс Rectangle вместо ссылок применяет указатели.
class Rectangle { Point *pt1, *pt2; int thickness; const int weight;
public:
Rectangle (const Point*, const Point*, int = 1, int = 1) void move(int a, intb);
Глава 12 • Преимущества и недостатки составных классов |
511 |
|||||||
void |
setThickness(int w=1); |
|
|
|
|
|||
int |
pointIn(const |
Point |
&pt) |
const; |
|
|
|
|
. . . . } ; |
|
|
|
/ / |
остальная |
часть класса |
Rectangle |
|
Rectangle::Rectangle(const |
Point |
*p1, const |
Point *p2, |
|
||||
int width, |
int wt) |
: pt1(p1), |
pt2(p2), weight(wt) |
/ / тоже не обязательно |
||||
{ thickness |
= width; |
} |
|
|
/ / тот же конструктор, |
что и выше |
Так как указатели могут изменяться в любой момент своего существования, применение списка инициализации здесь не обязательно, но это хорошая практика.
Если объекты, на которые ссылаются указатели, остаются постоянными в те чение всего времени существования объекта Rectangle, их можно объявить как указатели на объекты-константы.
class Rectangle { |
|
|
||
const |
Point |
*pt1; |
/ / |
точки могут использоваться |
|
|
|
/ / |
совместно с другими фигурами |
const |
Point |
*pt2; |
/ / |
точки не могут изменять свои координаты |
. . . . |
} ; |
|
/ / |
остальная часть класса Rectangle |
Такая конструкция может дать полезную оптимизацию, если большое число объектов Rectangle ассоциировано с одними и теми же объектами Point.
Не злоупотребляйте использованием ссылочных и постоянных элементов дан ных. Если жеони отражают свойства серверных объектов и потребности юшентов, их следует рассматривать как законные и полезные инструменты для разработки программы.
Использование объектов как элементов данных своего собственного класса
в предыдупдих разделах обсуждалась ситуация, когда объект одного класса (например. Point) использовался как элемент данных другого класса (например. Rectangle). Может ли объект класса быть компонентом своего собственного класса? Например, приложению могут потребоваться координаты точек, относя щихся к определенным зонам экрана, и спецификация точки привязки как харак теристики класса Point.
class Point { |
|
|
|
int X, y; |
/ / |
закрытые координаты |
|
Point anchor |
/ / |
это не допускается |
|
public: |
|
|
|
Point (int a--=0, int b=--0) |
/ / |
многосторонний |
конструктор |
{ X = a; У = b; } |
/ / |
остальная часть |
класса Point |
. . . . } ; |
Между тем это не допускается. В начале главы уже Рассматривалась последова тельность событий при распределении памяти для объекта. Память для элемен тов данных выделяется в порядке их определения в спецификации класса. (Для статических компонентов — в начале выполнения программы.) Таким образом, при создании объекта Point сначала выделяется память для х и у, а затем — для anchor ("якоря", точки привязки). Однако anchor имеет тип Point, поэтому вновь определяется память для его компонентов х, у, а затем anchor и т. д. Такой ре курсивный процесс не заканчивается, поэтому он запрещен.
Ссылки на объекты собственного класса допустимы (как, например, обозначе ния объектов одного и того же класса). И указатели, и ссылки представляют адрес объекта и не требуют распределения памяти для всего объекта. Следовательно,
512 |
Часть III * Прогром!мирование с огрегировани01\^ и наследованием |
|
память для них выделяется наряду с другими элементами данных без дополнитель |
|
ных трудностей. |
|
class Point { |
int X, |
у; |
|
/ / |
закрытые |
координаты |
Point |
&anchor; |
/ / |
это допускается |
||
public: |
|
|
|
|
|
Point |
(int |
а=0, int |
b=0, Point &focus) |
|
|
: anchor |
(focus) |
/ / |
не может устанавливаться в конструкторе |
||
{ X = а; у = Ь; |
} |
|
|
||
. . . } |
; |
|
/ / |
остальная |
часть класса Point |
Как уже говорилось в предыдущем разделе, ссылочные элементы данных не могут инициализироваться в теле конструктора класса, поэтому здесь они инициа лизируются в списке с помощью объекта класса Point, передаваемого конструк тору в виде аргумента. Параметр конструктора не имеет модификатора const, потому что элемент данных anchor не определен как константа. Через ссылку anchor объект Point может модифицировать объект, переданный как аргумент. Вот почему определение параметра как константы было бы синтаксической ошибкой.
Обратите внимание, что здесь снова присутствует распространенная ошибка: новый параметр добавляется в конструкторе справа. У него нет значения по умол чанию, следовательно, его нужно перенести влево от параметров, имеющих такие значения.
class Point { |
|
|
|
|
|
|
int X, |
у; |
|
|
|
/ / |
закрытые координаты |
Point |
&anchor; |
|
|
/ / |
это допускается |
|
public: |
|
|
|
|
|
|
Point |
(Point &focus, |
int a=0, int |
b=0) |
/ / |
правильный порядок |
|
: anchor |
(focus) |
/ / |
не может устанавливаться в конструкторе |
|||
{ X = а; |
у = Ь; } |
|
|
|
|
|
. . . } ; |
|
|
|
|
/ / |
остальная часть класса Point |
Клиент сначала создает базовую точку (anchor), а затем передает ее в аргументе конструктору новых точек. Первая точка должна быть базовой.
Point р1(р1); / / синтаксическая ошибка: р1 не определяется как аргумент
Это синтаксическая ошибка. Когда р1 используется в аргументе конструктора, данный объект создается. Следовательно, ссылка на него пока не определена. Поэтому память для базовой точки выделяется динамически.
Point |
*р = new Point(*p,80,90); |
/ / |
р еще не имеет значения |
Point |
р1(*р); |
/ / |
*р используется как базовая точка |
Здесь возникает проблема: при использовании р как аргумента конструктора данная переменная еще не имеет значения, однако это приводит не к синтаксиче ской ошибке, а к предупреждению. Избежать предупреждения можно путем ини циализации указателя с помощью нулевого значения перед его использованием.
Point |
*р = 0; |
/ / |
важно избежать предупреждения |
|
р = new Point(*p,80,90); |
/ / |
динамическое |
распределение объекта Point |
|
Point |
р1(*р); |
/ / |
используется |
как базовая точка |
Как видно, применение ссылочных элементов данных на объект того же класса кажется хорошей идеей, но при этом из-за рекурсивности структуры данных воз никает ряд трудностей. Не следует прибегать к такому приему, если в этом нет абсолютной необходимости.
Глава 12 • Преимущества и недостатки составных классов |
| 513 | |
Использование статических элементов данных как компонентов собственного класса
Использование статических элементов данных как компонентов собственного класса допускается, и это намного проще, чем применение ссылочных элементов данных. Например, объекты класса Point могут иметь общую для всех точек плос кости точку начала координат. Такая точка является общей, поэтому ее можно представить как статический элемент данных.
class Point { |
|
|
|
|
|
|
int X, |
у; |
|
|
|
/ / |
закрытые координаты |
static |
int |
count; |
|
|
|
|
static |
Point |
origin; |
/ / |
статический объект, OK |
||
public: |
|
|
|
|
|
|
Point |
(int |
a, |
int |
b) |
|
|
{ X = a; |
у = b; |
count++; |
} |
|
Синтаксис инициализации статического объекта такой же, как для статических элементов данных встроенных типов. (Подробнее о статических элементах данных рассказывается в главе 9.) Следующая строка показывает пример определения и инициализации статического объекта. Первое Point здесь представляет тип определяемых элементов данных (origin). Второе показывает, что определяемый элемент данных принадлежит к классу Point. При создании объекта для инициали зации его полей вызывается конструктор. В данном случае это обобщенный конст руктор Point с двумя параметрами.
Point Point::origin(640,0); |
/ / инициализация с помощью конструктора |
Это аналогично определению статических элементов данных встроенных типов, например count. Данное поле имеет целый тип, и операция области действия по казывает, что оно принадлежит к классу Point. Начальное значение поля устанав ливается в нуль.
int Point::count = 0;
Подобно другим статическим объектам, не вполне понятно, когда именно создает ся объект и вызывается конструктор. Если в программе несколько статических переменных, то порядок их создания не определен. Расположение их в опреде ленном порядке в исходном коде еще не гарантирует, что они будут создаваться и инициализироваться именно так. Отмечается лишь то, что они будут созданы перед выполнением первого оператора функции main().
В случае класса Point такой гарантии недостаточно. Конструктор Point вызы вается как для нестатических объектов Point, так и для статического объекта origin, создаваемого первым. При этом статический элемент данных count дол жен быть создан перед объектом origin.
Статические элементы данных можно использовать в функции-члене своего класса (например, в конструкторе) как аргументы по умолчанию. Нестатические элементы данных для этого применять нельзя.
class |
Point |
{ |
|
|
|
|
|
int |
X, |
у; |
|
|
|
|
|
static |
int |
count; |
|
|
|
||
static |
Point |
origin; |
|
/ / |
статический объект, OK |
||
Point |
&anchor; |
|
/ / |
ссылка или указатель, OK |
|||
public: |
|
|
|
|
|
|
|
Point |
(Point |
&focus |
= origin, |
int a=0, int |
b=0) : anchor(focus) |
||
|
{ X = a; |
у = b; |
count++; |
} |
|
514 |
Часть III« Программирование с агрегированием и наследованием |
|||
|
void set (int а = х, int b) |
/ / |
ошибка, нестатический |
элемент данных |
|
{ X = а; у = b; } |
/ / |
остальная часть класса |
Point |
|
|
Статические элементы данных создаются до начала выполнения программы. Доступ к ним возможен до создания объектов классов.
После создания объектов Point элементы данных count и origin могут быть доступны с помондью любого объекта. Результат будет одним и тем же, поскольку они статические. В отличие от нестатических элементов данных к ним нельзя обращаться с помощью имени класса, а не имени целевого объекта.
int mainO |
|
|
|
|
|
{ Point р1, р2(70.90); |
« |
р1.count « |
endl; |
// выводит 2 |
|
cout « |
"Число точек: |
||||
cout « |
"Число точек: |
« |
р2.count « |
endl; |
// выводит 2 |
. . . |
} |
|
|
|
|
Кроме того, вотличие от нестатических элементов данных, к нимнельзя обра щаться спомощью имени класса соперацией области действия вместо имени целевого объекта с операцией-селектором.
int mainO |
|
{Point р1, р2(70,90); |
|
cout « "Число точек: " « Point::count « endl; |
/ / также выводит 2! |
. . . } |
|
Более того, данный синтаксис доступен, даже когда объекты классов еще не со зданы.
int mainO |
//выводит О |
{ cout « "Число точек: " « Point::count « endl; |
Листинг 12.3 показывает вариант класса Point с двумя статическими элемен тами данных — count и origin. Их можно инициализировать вне определения
класса, хотя они являются закрытыми.Функция quantity() определяется как ста тическая, и к ней можно обращаться с помощью операции области действия (первый вызов) и целевого объекта (второй вызов).
Листинг 12.3. Использование статических элементов данных истатической функции-члена
#include <iostream> using namespace std;
class Point { |
|
// закрытые координаты |
int X, y; |
|
|
static int count; |
|
|
static Point origin; |
|
|
public: |
|
// обобщенный конструктор |
Point (int a=0, int b=0) |
||
{ X = a; у = b; count++; |
" y=" « у |
|
cout « |
Создан: )(=" « x « |
|
« |
" count=" « count « endl; } |
|
static int quantityO |
// const недопускается |
|
{ return count; } |
// функция-модификатор |
|
void set (int a, int b) |
||
{ X = a; у = b; } |
// функция-селектор |
|
void get (int& a, int& b) const |
||
{ a = x; b = y; } |
|
518Часть III • Программирование с агрегированием и наследованием
Со в е т у е м не торопитесь снабжать каждый класс конструктором копирования.
Они усложняют программы и замедляют их работу, поощряют клиентов передавать параметры по значению и возвращать объекты по значению. Вместо/этого сделайте конструктор копирования закрытым.
Таким образом вы сможете предотвратить появление проблем.
Семантикой общего порядка называется способность клиента сравнивать объекты-компоненты между собой и со значениями встроенных типов. Для этого в классе компонента предусматриваются перегруженные операции сравнения. Данное свойство очень полезно, особенно когда в клиенте требуется реализовать механизмы сортировки и поиска.
История |
измерений: |
В следующих примерах объекты класса Sample хранятся |
в контейнерном объекте класса History. Класс History хранит |
||
3 5 |
7 11 13 17 19 23 |
объект Sample в коротком массиве (для простоты в нем всего |
Среднее |
значение: 12.25 |
лишь восемь элементов). Это позволяет клиенту устанавливать |
|
|
значение Sample в заданном месте массива, выводить набор из- |
10 |
7 |
мерений и вычислять среднее для измеренных значений. |
Результат выполнения |
Листинг 12.4 показывает первую версию контейнерного клас- |
|
wpozpaMMbi из листинга 12А |
са. Результат программы представлен на рис. 12.7. |
Листинг 12.4. Контейнерный класс с массивом компонентов фиксированного размера
#inclucle <iostream> using namespace std;
class Sample { |
|
|
// класс компонента |
|
double value; |
|
|
// значение для примера |
|
public: |
|
|
|
// конструктор: поумолчанию и преобразования |
Sample (double x = 0) |
||||
{ value = x; } |
|
|
// метод-модификатор |
|
void set (double x) |
|
|||
{ value = x; } |
|
|
// метод-селектор |
|
double get () const |
|
|||
{ return value; } } ; |
|
|||
class History { |
|
|
// контейнерный класс |
|
enum {size = 8 }; |
|
// массив значений (фиксированного размера) |
||
Sample data[size]; |
|
|||
public: |
|
|
|
// модификация значения |
void set(double,int); |
||||
void print 0 const; |
// вывод предыстории |
|||
void averageO const; |
// вывод среднего значения |
|||
} ; |
|
|
|
|
void History::set(double s, int i) |
// или просто: data[i] = s; |
|||
{ data[i].set(s); } |
|
|||
void History::print |
() const |
// вывод предыстории |
||
{ cout « |
"\n История измерений:" « endl « |
endl; |
||
for (int i = 0; i < size; i++) |
// локальный индекс |
|||
cout « " " « |
data[i].get(); } |
|
||
void History::average |
() const |
// вывод среднего значения |
||
{ cout « |
"\n Среднее значение: |
|||
double sum = 0; |
|
|
// локальное значение |
|
for (int i = 0; i < size; i++) |
// локальный индекс |
|||
cout « |
sum +=data[i].get(); |
|
||
sum/size « |
endl; } |
|
|
|
Глава 12 • Преимущество и недостатки составнь1х классов |
519 |
|||||
i nt |
mainO |
|
|
|
|
|
|
|
{ |
double |
а[] = {3, |
5, |
7, 11, 13. 17, 19, 23, 29 } ; |
/ / |
исходные данные |
|
|
History |
h; |
|
|
/ / |
конструктор по умолчанию |
|
||
for (int i=0; i |
< 9; |
i++) |
/ / |
доступно |
8 слотов |
|
||
|
h . s e t ( a [ i ] , i ) ; |
|
/ / |
установка |
предыстории |
|
||
h . printO; |
|
|
/ / |
вывод предыстории |
|
|||
h.averageO; |
|
, |
//вычисление среднего значения |
|||||
return |
0; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
Обратите внимание, что архитектура контейнерного класса History реализует алгоритм, требующий доступа к памяти. Это означает, что программа сохраняет некоторые значения в памяти для будуидего использования в другом сегменте кода. В зависимости от расположения этих взаимодействуюш^их сегментов программы разработчику приходится использовать те или иные виды их связывания.
Как правило, у разработчика класса есть выбор, позволяющий ему сделать значение или переменную доступной методу класса для хранения или получения значения:
•Глобальная переменная или общедоступный элемент данных
•Параметр метода
•Элементы данных класса
•Локальная переменная в методе
Глобальная переменная может использоваться, когда несколько классов долж ны совместно работать с информацией, но разработчик затрудняется определить, к какому из них относится подобное сообщение. Такой метод коммуникаций между классами представляет наивысшую степень связности, и применять его следует по возможности реже. Общедоступные элементы данных можно использовать, когда разработчик выбирает для хранения информации конкретный класс, но эта информация нужна и другим классам. Она будет доступна им в форме элемента данных public. Такая степень связи столь же высока, как и при использовании глобальной переменной, и не следует применять ее часто.
Когда несколько классов программы взаимодействуют через глобальные пере менные или общедоступные элементы данных, это всегда следует рассматривать как повод для пересмотра архитектуры. Разработчикам необходимо проверить распределение обязанностей между классами. Коммуникации через глобальные переменные или общие элементы данных должны наводить на мысль, что, воз можно, разработчик разделил шаги обработки, которые должны быть объединены. Если сделать все правильно, не исключено, что необходимость в таких "дальних" коммуникациях исчезнет.
Остановимся на трех других методах коммуникаций, поскольку именно между ними приходится ежедневно выбирать программисту, использующему С+Н-.
Коммуникации через параметры метода следует применять в том случае, если значение или переменная будут совместно использоваться классом и его клиен том. Например, параметры метода History: :set() совместно применяются клиен том main() и функцией-членом set() класса History.
Это наивысшая форма связи через данные, когда два разных класса использу ют одно общее значение. Согласованная интерпретация такого значения в двух разных классах требует кооперации и координации усилий между разработчиками классов. Когда над такими классами работает один программист, возможно, это происходит в разное время, поэтому ему потребуется помнить множество разных ограничений.