Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdfI 530 |
Часть ill • Программирование с огрегировонием и наследованием |
Определения вложенных классов могут включаться в зарытые или общедо ступные части клиентского класса. В любом случае вложенный класс скрыт от остальной программы. Имя класса известно только в области действия (фигурных скобок) композитного класса, где определен вложенный класс.
Вложенные классы не могут использоваться для объявления переменных, имеющих тип этих классов, вне области действия составного класса. Определения класса скрываются таким же образом, как и элементы данных. Следовательно, если имя нужно использовать вне того класса, где оно определено, применяйте операцию области действия.
int mainO
|
5, 7, 11 |
13, 17, 19, 23, 29 |
} ; |
/ / исходные данные |
||
{ double а[] = {3, |
||||||
History h; |
< 9 i++) |
/ / |
конструктор |
по умолчанию |
||
for (int i=0; i |
/ / |
доступно |
8 слотов |
|
||
h.add (a[i]); |
|
/ / |
установка |
предыстории |
||
h.printO; |
|
/ / |
вывод предыстории |
|
||
h.averageO; |
|
/ / |
вычисление |
среднего |
||
Sample s = 5; |
s = 5; |
/ / |
не допускается для вложенного класса |
|||
History::Sample |
/ / |
операция области действия решает проблему |
||||
return 0; } |
|
|
|
|
|
|
Чтобы последний оператор был допустимым, класс Sample нужно определять в разделе public клиентского класса History. Тогда клиент класса History сможет использовать имя Sample, уточненное именем агрегатного класса.
C-f-+ позволяет комбинировать в одном операторе определения класса и его экземпляры. Между тем это нельзя считать хорошей практикой программирова ния, поскольку такие экземпляры часто являются глобальными.
class Sample { |
/ / |
глобальный в файле |
|
double value; |
|
|
|
public: |
|
|
|
Sample (double x = 0) |
|
||
{ |
value = x; |
} |
|
void |
set (double x) |
|
|
{ |
value = x; |
} |
|
double show () |
const { return value; |
} |
|
} |
s1,s2; |
/ / |
глобальные объекты класса Sample |
В то же время это вполне подходит для вложенных классов, так как компонент ные данные обычно глобальны в области действия класса.
class |
History { |
|
|
|
|
|
|
||
class Sample { |
|
|
/ / |
|
невидим вне области действия |
клиента |
|||
double |
value; |
|
|
|
|
|
|
||
public: |
|
|
|
|
|
|
|
|
|
Sample (double x = 0) |
|
|
|
|
|||||
|
{ |
value = x; |
} |
|
|
|
|
|
|
void |
set (double x) |
|
|
|
|
||||
|
{ |
value = x; |
} |
|
|
|
|
|
|
double |
show () |
const |
|
|
|
|
|||
|
{ |
return value; |
} |
|
|
|
|
||
} |
*data; |
|
|
/ / |
конец определения вложенного |
класса |
|||
int |
size, |
count, |
idx; |
|
|
|
|
|
|
public: |
|
|
|
|
|
|
|
|
|
HistoryO |
: size(3), |
count(O), |
idx(O) |
/ / сделать массив пустым |
|||||
{ data |
= new Sample[size]; |
/ / |
выделение новой памяти |
|
|||||
Глава 12 • Преимущества и недостатки составных классов |
533 |
|||
|
public- |
|
|
|
|
Sample (double X = 0) |
|
|
|
|
{ value = х; } |
|
|
|
|
void set (double x) |
|
|
|
|
{ value = x; } |
|
|
|
|
double show () const |
|
|
|
|
{ return value; } |
|
|
|
|
}; |
|
|
|
Другой способ решить эту проблему — |
указать непосредственно в объявлении |
|||
friend, что History является классом. |
|
|
||
class Sample { |
// объявление friend |
|
||
|
friend class History; |
|
||
|
double value; |
|
|
|
public: |
|
|
|
|
|
Sample (double x = 0) |
|
|
|
|
{ value = x; } |
|
|
|
|
void set (double x) |
|
|
|
|
{ value = x; } |
|
|
|
|
double show () const |
|
|
|
|
{ return value; } |
|
|
|
|
}; |
|
|
|
|
Конечно, хорошо было бы иметь только один способ, а енде лучше, если бы |
|||
компоновш,ик мог сам разрешать эти перекрестные ссылки. |
|
|||
|
Теперьфункции-члены History могут непосредственно обраидаться к отличным |
|||
от public данным. |
|
|
||
void History::print () const |
// вывод только допустимых элементов |
|||
{ for (int i = 0; i < count; i++) |
||||
// |
cout « " " « data[i].show(); |
// не нужно использовать методы |
|
|
|
cout « |
" " « data[i].value; |
|
|
|
cout « |
endl; } . |
|
|
Более того,классу Sample теперь не требуются функции-члены для предоставле
ния доступа, его функция friend в них не нуждается.
class Sample { |
// объявление friend |
friend class History; |
|
double value; |
|
public: |
|
Sample (double x = 0) |
// другие функции ненужны |
{ value = x; } |
|
}; |
|
В технике разработки П О и методологии программирования с подозрением
относятся к компонентам friend, поскольку они намеренно нарушают принцип
сокрытия информации. Кроме того, они усложняют архитектуру программы
и затрудняют обучение программированию.
Вместо применения элементов friend класс Sample можно сделать вложенным в класс History и объявить общедоступные поля. Поскольку никакой другой
класс,кроме History, к этим полям не обраш,ается, принцип сокрытия информа
ции соблюдается.
class History { |
idx; |
intsize, count, |
|
struct Sample { |
// невидим вне клиента |
^ ^ / ^ IS
п.юдобные классы и их интерпретация
Темы данной главы
^Интерпретация подобных классов
^Синтаксис наследования в С++
•^ Доступ к сервису базовых и производных классов
^Доступ к базовым компонентам объекта производного класса
^Правила области действия и разрешения имен при наследовании
•^ Конструкторы и деструкторы производных классов
^Итоги
^ |
1^]^ |
данной главе, как и в предыдущей, читатели более подробно познако- |
|
^'•к.мятся с синтаксисом С+Н |
ключевыми словами, списками инициа- |
||
^^^^'Z^ лизации и др.
Впервой части книги рассказывалось о вычислительных аспектах C+ + . Она была посвящена таким традиционным темам программирования, как типы данных, идентификаторы, ключевые слова, выражения, операторы, условия, циклы и дру гие управляющие структуры. Применение этих инструментов позволяет достичь стоящих при написании программы целей и получить результаты, отвечающие требованиям вычислений.
Кроме того, говорилось о методах агрегирования — включения компонентов данных в массивы, структуры и другие определяемые программистом типы, объе динении операторов и управляющих конструкций в функции. В C++ это сложнее, чем в других языках, особенно когда дело касается областей действия, передачи параметров, возврата значений, указателей и ссылок. Читатели узнали также
овозможностях и опасностях динамического управления памятью в C++. Эти ин струментальные средства предполагают разбиение программы на взаимодейству ющие части. В то же время они более удобны для программиста. Вычислительных целей программы можно достичь с помощью множества альтернативных архитек турных решений, но качество программы при этом будет различаться.
Навыки корректного комбинирования элементов программы C++ и разделе ния тех компонентов, которые не следует объединять вместе, являются необходи мой предпосылкой для написания легко сопровождаемых и модифицируемых программ C++.
536 Часть 111 • Программирование с агрегированием и насдадованием
Вторая часть книги была посвящена применению знаний, полученных в ее первой части, для написания классов С4-+. В ней рассказывалось о синтаксисе классов, области их действия, элементах данных и функциях, доступе к данным
ифункциям, сообщениях (их синтаксисе и смысле), инициализации объектов,
остатических данных и функциях. Читатели узнали об операторных функциях, за
счет применения которых программа на C + + становится эффективной, однако они усложняют архитектуру классов. Кроме того, вы познакомились с "дружест венными" функциями, научились распознавать в конструкции класса опасные эле менты и избегать их негативного влияния на программу. Написание программы с использованием классов усложняет C+ + , но оно того стоит.
Навыки объединения (включения в один класс) имеющих отношение друг к другу данных и функций являются необходимой предпосылкой написания объектноориентированных программ. Основное различие между традиционными и объектноориентированными программами состоит в том, что традиционные программы C+ + строятся с помощью взаимодействующих глобальных функций, связывающих шаги каждой операции. Объектно-ориентированная программа состоит из взаимодей ствующих классов, объединяющих данные и операции.
Первые две части книги — это введение к объектно-ориентированному про граммированию. Во всех примерах использовался только один класс, поскольку основное внимание уделялось архитектуре классов, а не связям между ними. В третьей части книги вы приступили к изучению построения программ C+ + как набора взаимодействующих классов. Это требует реализации связей между классами программы. В главе 12 было показано, как можно использовать один объект (сервер) в качестве компонента другого объекта (клиента). Функции-члены объекта-компонента предоставляют свой сервис функциям-членам составного класса. Это наиболее распространенная простая связь между объектами.
Один объект может ссылаться на другой, являющийся элементом данных следующего объекта. Клиентский объект получает доступ к функциям-членам серверного объекта, передавая сообщения его элементу данных (указателю). Кроме того, один объект может использоваться как ссылочный элемент данных объекта-клиента. Синтаксически такой вариант аналогичен простой композиции классов, но фактически это совершенно разные связи между объектами.
При простой композиции класса серверный объект (компонент) является эле ментом данных объекта-клиента (составного объекта). При такой связи клиент ский объект имеет эксклюзивные права на объект-компонент. Когда серверный объект представляет ссылочный элемент данных объекта-клиента, он может со вместно использоваться несколькими клиентами, и несколько объектов могут ссылаться на один серверный объект. Изменения в серверном объекте влияют на состояние клиентского объекта. Вряд ли имеет смысл обсуждать, какая связь "лучше" — эксклюзивная композиция классов или совместное использование компонентов. Между тем во многих практических ситуациях одна связь действи тельно "лучше" другой, так как точнее представляет выраженную в программе C + + взаимосвязь между реальными объектами. Поэтому важно выбирать те связи, которые лучше моделируют реальную ситуацию.
Кроме того, рассматривалась такая популярная связь между объектами, как контейнер, когда один объект включает в себя набор объектов другого класса — компонентов контейнера (а не один единственный объект). Такая взаимосвязь часто встречается в программах C+ + , и нужно хорошо освоиться с организацией объектов в приложении и с выбором подходящих связей между ними.
В данной главе продолжается исследование взаимодействия между фрагмента ми программы C++ . Здесь читатели познакомятся с наследованием как механиз мом, представляющим связь ме>кду классами в приложении. На этом этапе разница между связью объектов и классов может казаться вам незначительной, но к концу главы вы ее почувствуете.
Глава 13 • Подобные классы и их интерпретация |
537 |
Наследование очень часто применяется в программах C+ + . Оно представляет мощный механизм для повторного использования фрагментов программ C++, для разделения труда программистов и использования модульности в приложениях. Для корректного применения наследования следует изучить его синтаксис, методы создания экземпляров производных объектов, технику доступа к компонентам, правила вызова функций, разрешения имен и многое другое. Важно также знать, когда использовать наследование. Программисты, применяющие C+ + , иногда злоупотребляют наследованием. Они создают дополнительные взаимосвязи и за висимости, которые затрудняют понимание программы.
Будьте внимательны.
Интерпретация подобных классов
Программы моделируют различные реальные объекты через их данные (со стояние объекта) и операции (его поведение). Это азбука объектно-ориентирован ного проектирования ПО, но каждый разработчик должен сам решить, что именно следует включать в каждый класс. Моделирование реальных ситуаций должно отражать "общие свойства" объектов, например инвентарных записей, счетчиков событий или банковских счетов.
Все "общие свойства" с точки зрения наблюдателя, и C++ предусматривает разнообразные механизмы для объектов.
Первый механизм, предлагаемый C++ для воплощения общих свойств реаль ных объектов в классе, это сама конструкция класса. Она используется для показа общности объектов. Вы верите, что эти объекты можно охарактеризовать одним и тем же набором атрибутов и шаблонами поведения. Объекты различаются зна чениями атрибутов: точки разных углов фигуры имеют разные координаты, разные инвентарные единицы — различные названия, на каждом банковском счете — свой собственный баланс, и у них разные владельцы. Общность в том, что каждый прямоугольник имеет угловые точки, каждая инвентарная единица — название, каждому счету соответствует баланс и владелец. Если для одного банковского счета требуется задавать проценты, а для другого — нет, то обычно они не рас сматриваются как объекты одного класса.
Часто ситуация бывает не ясна. Например, каждый болт в инвентарной описи имеет индивидуальные характеристики и может отличаться в приложении от дру гих болтов. Чтобы описать каждый болт, придется создать для него отдельный класс, присвоить ему индивидуальный набор элементов данных и функций, задать уникальные имена. Такие имена могут отражать уникальный характер каждого болта в приложении, например RustyBolt, UglyBolt и BoltFoundlnPothole. Не исключено, что это осложняет ситуацию и имеет смысл только в случае, если от дельные болты не обладают общими свойствами и ведут себя по-разному.
Между тем у болтов в инвентарной описи много общего, так что можно пред ставить каждый болт, используя для элементов данных одни и те же имена. Таким образом, не нужно будет представлять каждый болт как объект другого класса. Возникновение проблемы можно предотвратить, используя всего лишь один к^шсс, например Bolt, и представляя каждый болт в приложении как объект данного класса с такими атрибутами, как дата покупки, имя поставщика и шаг резьбы. Аналогично, можно представить все гайки в инвентарной описи как объекты одного класса Nut, если для описания ка>вдого такого объекта достаточно одного и того же набора атрибутов (цвет, материал, размер и т. д.).
Если класс Bolt, Nut и другие инвентарные элементы используют одни и те же имена элементов данных, нужно применить только один класс Inventoryltem, представляющий эти разные объекты. Если все болты с точки зрения приложения одинаковы, можно представить их как, один объект и указать в числе атрибутов класса количество болтов. Так как все болты одинаковы, разница в шаге резьбы не столь важна. Если же она имеет значение, такая конструкция не подойдет.
|
Глава 13 • Подобные КАОССЫ И ИХ интерпретация |
539 |
|||||
Account(double initBalance, double initRate) |
// для сберегательного счета |
|
|||||
{ balance = initBalance; rate = initRate; } |
|
// не плата, а % |
|
||||
double getBalO |
|
|
|
// для обоих счетов |
|
||
{ return balance; } |
|
|
|
|
|||
void withdraw(double amount) |
|
|
|
// общая для обоих счетов |
|
||
{ if (balance > amount) |
|
|
|
|
|
|
|
balance -=amount; } |
|
|
|
|
|
|
|
void deposit(double amount) |
|
|
|
// для обоих счетов |
|
||
{ balance += amount; } |
|
|
|
|
|
|
|
void paylnterest () |
|
|
|
// только для сберегательных счетов |
|
||
{ balance += balance * rate / 365 / 100; } |
|
|
|
|
|||
void applyFeeO |
|
|
|
// только для расчетных счетов |
|
||
{ balance -= fee; } |
|
|
|
|
|||
} : |
|
|
|
|
|
|
|
int mainO |
|
|
|
|
|
|
|
{ |
|
|
• |
|
// a1: расчетный счет, a2: сберегательный счет |
||
Account a1(1000), a2(1000,6.0); |
|
||||||
cout « |
"Начальный баланс: " « |
a1.getBal() |
|
|
|
|
|
« |
" " « a2.getBal() « |
endl; |
// нет проблем |
|
|||
al.withdraw(IOO); a2.deposit(100); |
|
||||||
a2.paylnterest(); a1. applyFeeO; |
|
|
//нет ошибок |
|
|||
cout « |
"Конечный баланс: " « |
a1.getBal() |
|
|
|
|
|
« |
" " « a2.getBal() « |
endl; |
|
|
|
|
|
return 0;
}
Сегодня никто не верит в человеческую непогрешимость. Если что-то где-то может быть написано, то когда-нибудь так и случится. Например, пятая строка клиента может выглядеть так:
a1.paylnterest(); а2.арр1уРее(); |
/ / неудачная попытка... |
Конечно, невозможно предотвратить все ошибки программирования (вот почему необходимо тестирование), но следует хотя бы постараться это сделать. Вы може те добиться того, чтобы сообш,ения об ошибках выводились заранее и не нужно было анализировать результат. Данная архитектура нуждается в улучшении.
Обратите внимание, что в клиенте при "создании счета даются явные коммен тарии о его типе, но ничто не мешает здесь программисту выразить свои идеи в операторах, а не в комментариях. Для этого серверный класс (Account) должен поддерживать потребности клиента, позволяя ему явно различать виды объектов Account.
Перенос ответственности за целостность программы на сервер
Чтобы избежать опасности некорректного использования серверных объектов клиентом, можно добавить в серверный класс дополнительный атрибут — поле тега (признака), описываюш,ее вид счета, к которому относится данный конкрет ный объект. Это означает, что в классе вводятся подклассы.
При создании объекта поле тега можно установить таким образом, чтобы указать подкласс объекта при его инициализации. При использовании объекта (например, paylnterestO или applyFeeO) это поле проверяется, чтобы убедиться в допустимости операции для данного вида объекта.
