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

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

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

480

Часть II • Объектно-ориентированное програмтироваитв на С-^-ь-

Второе решение; возврат по ссылке

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

String&

String::operator

= (const

char s [ ] )

/ /

возврат ссылки

{ delete

str;

/ /

этого не следует делать в конструкторе копирования

len

= strlen(s);

/ /

выделение памяти,

копирование входящего текста

str

= allocate(s);

cout

«

"Присвоен: " «

str «

" ' \ п " ;

/ /

для отладки

return

*this; }

 

 

 

 

 

 

 

 

 

То же самое следует реализовать для первой

Присвоен:

'Атланта'

 

 

операции присваивания с параметром String. При

 

 

возврате ссылок из функций убедитесь, что ссылка

Присвоен:

'Бостон'

 

 

Присвоен:

'Чикаго'

 

 

все еш,е указывает на объект в левой части операции

Присвоен:

'Денвер'

 

 

присваивания в области действия клиента, как на­

Скопирован: 'Денвер'

 

 

 

 

пример clata[i] в приведенном выше цикле. Он рас­

Введите название города для поиска: Денвер

Создан: 'Денвер'

 

 

полагается после операции присваивания, так как

Присвоен:

'Денвер'

 

 

определен в области действия клиента. Нужно акку­

Город Денвер найден

 

 

 

 

 

 

ратно возвраш.ать ссылки на объекты, определенные

Рис. 11.20. Результат

 

программы

в области действия сервера — после вызова они ис­

 

чезают. В этом случае лишь некоторые компиляторы

 

из листинга

11.7

 

с добавленной

вт^орой

предупреждают вас о возможных последствиях при­

 

операцией

присваивания

нятия такого решения.

 

возвращающей

ссылку

 

на объект.

String

Результат программы из листинга 11.7 с двумя

операциями присваивания и возвраш.аемыми ссыл­ ками на объекты см. на рис. 11.20.

Некоторые "блюстители нравов" могут настаивать на том, что было сделано недостаточно, поскольку данная конструкция не запреш,ает программисту, разрабатываюш.ему код клиента, в частности изменять содержимое возвраш.аемого объекта-строки перед его уничтожением. Например, в C + + для операций при­ сваивания из листинга 11.7 допустим следуюш,ий фрагмент кода.

for (int j=0; j<MAX; j++)

{ (clata[j] = c[j].moclify("Город, о котором никто не слышал");

/ / допустимо

Один объект присваивается другому, возвраш,ается ссылка на целевой объект и немедленно передается сообш^ение для его модификации. Присвоенное значение никогда не используется. Подобные действия нужно пометить как синтаксическую ошибку. Чтобы сгенерировать синтаксическую ошибку, сделайте возвраш^аемую ссылку ссылкой на константу.

constString& String::operator = (const char

s [ ] )

/ / слишком много?

{ delete

str;

/ /

этого

не следует делать в конструкторе копирования

len

= strlen(s);

 

 

 

 

 

str

= allocate(s);

/ /

выделение памяти,

копирование

входящего текста

cout

«

"Присвоен:

«

str «

" ' \ п " ;

 

/ / для отладки

return

*this; }

 

 

 

 

 

Трудно настаивать на именно таком решении, но некоторые сторонники "чистоты нравов" предпочли бы данный вариант.

// формирование данных из компонентов

Глава 11 * Конструкторы и деструкторы: потенциальные проблв1^1ы

481

Практические вопросы: что подлежит реализации

Рекомендуем вам внимательно и аккуратно работать с динамическим управ­ лением памятью. Помните, что вы можете повлиять на производительность программы или ее целостность.

Многие программисты считают, что при разработке класса с динамическим управлением памятью нужно снабдить этот класс полным набором вспомогатель­ ных функций-членов:

Конструктором по умолчанию

Конструкторами преобразования

Конструктором копирования

Перегруженными операциями присваивания

Деструктором

Не уверен, что необходимо автоматически следовать этой рекомендации. В зависи­ мости от требований клиента может понадобиться лишь часть данных функций. Если вы предусмотрите операции с неподходящими интерфейсами, то проблемы целостности не возникнет, но без всяких на то оснований ухудшится производи­ тельность программы. Вы должны разбираться в вопросах, о которых рассказы­ валось в данной главе. Тогда вы сможете выбирать функции-члены согласно требованиям клиента и создавать корректные и эффективные классы. Если же автоматически следовать приведенным выше рекомендациям, то код клиента будет прекрасно работать, но вы забудете о различиях между инициализацией и присваиванием. А это опасно.

Убедитесь, что вы используете для своих классов подходяш,ие инструменты. При возникновении трудностей проанализируйте ситуацию, используйте операто­ ры отладки, рисуйте диаграммы, но не перегружайте класс ненужными компонен­ тами. Подбирайте инструментальные средства, соответствуюш,ие задаче, и обходите "подводные камни" (конструкторы, операции присваивания и т.д.). Запомните, что конструктор копирования и операция присваивания не могут быть взаимо­ заменяемыми.

Часто клиенту не нужно инициализировать один объект с помош,ью другого или присваивать один объект другому. Предположим, что реализуемый класс представляет диалоговое окно. Рассмотрим только один элемент данных, представляюш,ий выводимый в окне текст. Такой класс Window будет аналогичен классу String. Он содержит динамически распределяемый символьный массив, деструктор и операцию конкатенации, воспринимаюшую символьный массив, который отображается в окне и добавляется к его содержимому.

class

Window {

 

 

 

 

char

*str;

 

/ /

динамически распределяемый символьный массив

int

 

len;

 

 

 

 

public:

 

 

 

 

WindowO

 

 

 

 

{ len

= 0; str

= new char; s t r [ 0 ]

= 0; }

/ / пустая строка String

"WindowO

 

 

 

 

{ delete str;

}

/ /

возвращает динамически распределяемую память

void

operator

+= (const

char[])

/ /

параметр-массив

{ len

 

= strlen(str) + strlen(s);

 

 

char* p = new char[len

+ 1];

/ /

выделение достаточного объема

i f

(p==NULL)

exit(1);

 

/ /

динамически распределяемой памяти

 

 

 

strcpy(p,str); strcat(p,s); delete str; str = p; }

I 482

Часть II # Объектно-ориентированное програмтшроваитв на 0-^4-

 

const char* showO

const

 

{ return str; } } ;

/ / указатель на содержимое

Конечно, в приложении меньше объектов класса Window, чем объектов String. Кроме того, при создании класса Window он инициализируется пустым содержи­ мым, а данные добавляются в процессе выполнения программы.

Объекты типа этого класса не следует передавать по значению. Что, если программист передаст в клиенте параметр Window по значению, или просто про­ пустит операцию &, тем самым ненамеренно передав параметр по значению?

void display(const Window window)

/ / не делайте этого!

{ cout « window. showO; }

 

Инициализировать одно окно с помощью другого или присваивать одно другому не имеет смысла.

Window w1; w1 += "Welcome, Dear Customer! Window w2 = w1;

w2 =w1; display(w2);

 

/ / разумное применение

/ /

бессмысленное применение

/ /

менее, чем разумное применение

/ /

пропущено значение: slow

Вторая и третья строки в этом фрагменте кода не имеют смысла. Большинству программистов этого не потребуется. Кроме того, функция displayO передает свой параметр по значению. Большая часть программистов ничего подобного не напишут, но это не значит, что никто не создаст класс Window без конструктора копирования или операции присваивания. Если программист захочет написать такой фрагмент, возникнет проблема целостности и производительности. Однако сам фрагмент будет вполне допустимым исходным кодом C+ + .

Следует ли писать для класса Window длинные комментарии, типа: "Уважае­ мый программист клиентской части, пожалуйста, не инициализируйте объекты Window с помош,ью других объектов Window. Не присваивайте один объект Window другому объекту Window. И не передавайте объект Window функции по значению, а также не возвращайте его из функции. Из-за этого в программе возможны проблемы". Хорошая подача, но не мешало бы сделать что-нибудь для заи;иты кода клиента.

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

Кроме того, можно такой программный код сделать синтаксически некоррект­ ным. Очень интересная идея. Для этого класс проектируется таким образом, что­ бы подобное использование его объектов клиентом приводило к синтаксической ошибке. Разработчик класса решает, какое именно применение объектов будет некорректным. Тогда даже не потребуются комментарии к классу.

Такой метод не прост. Можно исключить определяемый программистом конст­ руктор копирования и перегруженную операцию присваивания, но система под­ ставит для класса свой собственный конструктор копирования, и предусмотренные системой функции-члены способны привести для класса с динамическим управ­ лением памяти к проблемам целостности. Чтобы предотвратить это, добавьте к классу определяемый программистом конструктор копирования и перегружен­ ную операцию присваивания. Рекомендуем вам сделать так, чтобы клиент не мог их использовать и попытка вызова давала синтаксическую ошибку.

Советуем вам написать функцию, которую не может вызывать клиент. Сделай­ те ее закрытой (или заи|ииденной) функцией.

Это решение показано в листинге 11.8. Конструктор копирования и операция присваивания определены здесь как закрытые. Их даже не нужно реализовывать.

Глава 11 • Конструкторы и деструкторы: потенциальные проблемы

483

Если задается только прототип функции и функция вызывается клиентом, компоновидик даст ошибку. Он не видит код. Компилятор сообидит, что последние три строки функции mainO ошибочны. Закомментируйте объявления для операции присваивания и конструктора копирования, и компилятор безропотно примет клиентский код.

Листинг 11.8. Пример использования закрытых прототипов,

 

чтобы некорректное использование объектов было незаконным

 

#inclucle <iostream>

 

 

 

 

using namespace std;

 

 

 

 

class Window {

 

// динамически распределяемый

символьный

массив

 

char *str;

 

 

int len;

 

// закрытый

конструктор копирования

 

Window(const Window& w);

 

Window& operator = (const Window &w); .

// закрытое

присваивание

 

 

public:

 

 

 

 

 

WindowO

 

// пустая строка String

 

 

{ len = 0; str = new char; str[0] = 0; }

 

 

"WindowO

 

// возвращает динамически распределяемую

память

{ delete str; }

 

void operator += (const char s[])

// параметр-массив

 

 

{

len = strlen(str) + strlen(s);

// выделение достаточного объема

 

 

char* p = new char[len + 1];

 

 

if (p==NULL) exit(1);

// динамически распределяемой

памяти

 

 

// формирование данных из компонентов

 

 

strcpy(p,str); strcat(p,s);

 

 

delete str; str = p; }

 

 

 

 

const char* showO

const

// указатель на содержимое

 

 

{ return str; } }

;

 

 

void display(const Window window)

// не передавать объекты по значению

 

{ cout « window. showO; }

 

 

 

 

int mainO

 

 

 

// разумно

 

{

Window w1; w1 += "Добро пожаловать, уважаемые покупатели!\n";

 

 

Window w2 = w1;

 

// неразумное использование: синтаксическая ошибка

 

w2 = w1;

 

// еще менее разумно: синтаксическая ошибка

error

 

// передача позначению: синтаксическая ошибка

 

display(w2);

 

 

return 0;

 

 

 

 

 

Этот способ защитит ваши классы от постоянного использования их програм­ мистами клиентской части. Если клиентский код, помечаемый в функции main() листинга 11.8 как необдуманный, по какой-то причине нужно поддерживать и важна производительность программы, класс должен предусматривать конст­ руктор копирования и операцию присваивания или несколько операций присваи­ вания. Следует предусмотреть операции преобразования, если объекты класса должны инициализироваться из простых объектов данных, а не из объектов того же типа. Еще одна веская причина для добавления операций преобразования со­ стоит в том, что они позволяют избежать определения нескольких перегруженных операторных функций. За счет этого уменьшается число функций в классе, но потребуются дополнительные вызовы конструкторов и добавочные операции по распределению памяти.

I 484

Итоги

Часть il« Объектно-ориентированное г1рогра1^1^ирование на С^-^*

в этой главе рассматривалась "темная сторона" мош,ных возможностей С+4-. Сделано это не для того, чтобы пугать читателя, а чтобы внушить ему чувство ответственности за производительность и целостность программы СН- + .

Здесь пришлось еш.е раз покритиковать передачу параметров по значению. Передавайте параметры по ссылке и используйте модификатор const, когда пара­ метр в функции не изменяется.

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

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

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

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

Если объекты класса требуется присваивать в клиенте друг другу, предусмот­ рите перегруженную операцию присваивания, реализующую семантику значений и предоставляюшую каждому объекту собственную область динамической памяти. В операции присваивания не забудьте о предотвращении "утечек памяти": нужно возвращать используемую объектом ранее память перед присваиванием нового значение, переданного в параметре-объекте. Определитесь с тем, нужно ли под­ держивать цепочку присваиваний. Часто в клиенте она не требуется.

Применение конструкторов преобразования позволяет значительно ослабить правила строгого контроля за типами в С+Н-. В качестве фактического аргумента можно передавать данные, тип которых отличается от типа, требуемого классом. При этом программный код все равно будет вполне законным. Хороший метод, но пользоваться им нужно аккуратно. Дополнительные вызовы конструкторов преобразования — это накладные расходы, особенно если приходится поддержи­ вать семантику значений.

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

Возвращайтесь к материалу данной главы. Рисуйте диаграммы использования памяти, экспериментируйте с примерами программ. К сожалению, в C+-f к тради­ ционным категориям ошибок — синтаксическим и семантическим (этапа выполне­ ния) — добавляется еще одна: программа может быть синтаксически и семантически корректной, но при этом все равно неверной. Относитесь к этому языку с должным уважением. Удачи вам.

ЧлС/иЯЬ III

бъектноориентированное программирование

сагрегированием

инаследованием

1 ^ ^ этой части книги обсуждаются методы объектно-ориентированного

ш^!Я^-лрограммирования. Рассматриваются инструментальные средства про-

^ 4^^^^ граммиста: композиция и наследование классов. Некоторые програм­ мисты не знают, какой метод выбрать и как избежать чрезмерного усложнения программы.

В главе 12 описывается синтаксис использования объектов как компонентов другого класса, определяются правила доступа к таким объектам и их элементам данных, поясняется, как инициализировать компоненты составного объекта. Кроме того, эта глава знакомит читателей с методами совместного использования компонентов объектов как статических компонентов и с помощью компонен­ тов-ссылок, описывается применение вложенных и "дружественных" классов.

Вглаве 13 представлены методы использования наследования. Рассказывается

осинтаксисе наследования C+ + , обсуждаются различные режимы наследования

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

Вглаве 14 рассматривается унифицированный язык моделирования — UML (Unified Modeling Language). Он становится все более популярным в объектноориентированном программировании и применяется для описания проектов. Эта глава поможет читателю сделать выбор между наследованием и композицией класса и подобрать подлодяш,ие инструменты для разработки классов. Кроме того, здесь рассказывается об опасностях чрезмерного применения наследования и зна­ чительного усложнения программ.

sr^^6^

n,^преимущества и недостатки составных классов

Темы данной главы

^Использование объектов классов как элементов данных

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

^Элементы донных со специальными свойствами

^Контейнерные классы

^Итоги

]^^^

первых двух частях этой книги говорилось в основном о правилах

Ж

••11|^языка C + + . Вы узнали, что может делать программист, чего нужно

^

^^"'^Z^

избегать, чтобы программа не перестала функционировать. C + + был

представлен как мощный язык, при работе с которым от программиста ожида­ ется глубокое понимание того, что происходит внутри программы и лежит "на поверхности".

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

Связывание данных и функций в классе для того, чтобы показать их логическое единство.

Определение как закрытых тех компонентов класса, которые делают клиентов зависимыми от низкоуровневых деталей архитектуры класса.

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

Создание функций-членов, делающих излишним прямой доступ к именам полей данных сервера в клиенте.

• Перенос обязанностей с клиента на серверные классы и функции-члены.

Написание клиентской части в терминах вызова методов сервера,

врезультате чего в программе не создаются зависимости

от архитектуры сервера.

488

Часть III • Прогрол^1М1ирование с агрегированием и наслеАОвание1У1

Создание конструкторов и деструкторов для правильной инициализации объектов, управления ресурсами

и для дальнейшего переноса обязанностей на серверные классы.

Передача программисту, сопровождающему приложение,

ипрограммистам, отвечающим за клиентскую часть,

идей разработчика и его знания поведения сервера, например,

с помощью модификаторов const, применяемых к элементам данных, параметрам, возвращаемым значениям и методам.

Эти идеи лежат в основе базовой техники программирования, которая выражает­ ся в "самодокументируемом" объектно-ориентированном коде. Такой исходный код прост в понимании и его легко сопровождать. С помощью данных идей можно полностью реализовать потенциал C+ + , Без них программа будет состоять из сильно связанных друг с другом фрагментов с большим числом зависимостей. Такой программный код труден в понимании и модификации, причем независимо от того, на каком языке он написан — на C+ + , Java, COBOL или FORTRAN.

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

Еще одним методом кооперации между классами является наследование, позволяющее проектировать похожие классы,— один класс дополняет элементы данных и методы другого класса. Это основной способ повторного использования программного кода в C+ + . Здесь обсуждаются вопросы проектирования и при­ менения наследования в той или иной ситуации, рассматривается множество средств языка C+ + , которые используются для поддержки наследования: синтак­ сис наследования, экземпляры объектов, передача данных для инициализации наследуемых компонентов, неоднозначность имен и правила разрешения этой не­ однозначности.

Программисты, работающие с C+ + , любят применять наследование. Многие эксперты считают, что использование наследования является основой объектноориентированного программирования. Это не совсем так. Основа объектноориентированного программирования — использование классов как фундамента объектно-ориентированной программы (для связывания данных и операций, управления доступом к компонентам и т. д.).

Наследование не является фундаментом объектно-ориентированного програм­ мирования. Это техника написания программного кода и его повторного использо­ вания. В таком качестве она очень важна в C+ + , поэтому применять ее следует корректно.

Использование объектов классов как элементов данных

Основная цель конструктора класса в С+Н

позволить программисту свя­

зать вместе логически соотнесенные данные и операции (см. главу 9).

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

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

489

значениям и числам с плавающей точкой. Он не имеет доступной извне внутренней структуры. Между тем компоненты классов могут быть более сложными, чем значения встроенных типов.

В главах 10 и 11 было показано, что в языке C + + большое внимание уделяет­ ся равноценной интерпретации встроенных типов и типов, определяемых про­ граммистом. Если данные встроенных типов можно использовать как компоненты классов, то нет никаких причин для запрещения применять в качестве элементов данных объекты некоторых других классов, также содержащие компоненты.

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

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

Например, рассмотрим класс Rectangle, содержащий координаты х w у соот­ ветственно верхнего левого и нижнего правого углов прямоугольника (общепри­ нятое соглашение в программировании графических приложений).

class

Rectangle {

 

 

int

х1, у1;

/ /

координаты верхней левой точки

int

х2, у2;

/ /

координаты нижней правой точки

int

thickness;

/ /

толщина границы прямоугольника

public:

 

 

Rectangle (int inX1, int inYl, int

inX2, int

inY2, int wiclth=1);

void move(int a, int b);

/ /

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

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)

{ x1 = inX1; y1 = inY1;

 

 

x2 = inX2; у2 = inY2;

 

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

thickness = width; }

 

void Rectangle::move(int a, int b)

 

{ x1 +=a; y1 += b;

 

// перемещение каждого угла

x2 +=a; у2 += b; }

 

void Rectangle::setThickness(int

width)

// выполнение работы

{ thickness = width; }

 

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

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

{ bool xIsBetweenBorders = (xKx

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

bool ylsBetweenBorders = (y>y1 &&у<у2) | |(y<y1 && у>у2); return (xIsBetweenBorders &&ylsBetweenBorders); }

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