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

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

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

I

450

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

 

 

С помош^ью функции modifyO

нельзя увеличить длину строки. Большинство

 

 

конструкций класса String не позволяет программисту изменять содержимое

 

 

объекта String. В этом случае для другого содержимого требуется создание и ис­

 

 

пользование другого объекта. Для полномасштабных средств модификации потре­

 

 

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

 

 

небольшой функции modifyO.

 

 

 

Функция show() возвраш,ает указатель на динамически распределяемую память.

 

 

Листинг 11.2 демонстрирует два варианта использования этой функции KjmcHTOM

 

 

в main(). Первый вариант — вывод содержимого объекта String, получателя со-

 

 

обидения show(). Второй — это модификация содержимого объектов с помош,ью

 

 

возвраш^аемого функцией show() значения. Оно применяется как выходной пара­

 

 

метр в вызове функции strcpy() в клиенте. Первый вариант использования зако­

 

 

нен, а во втором случае возможности явно преувеличиваются. Сопровождающими

 

 

приложение программист только запутается.

 

 

Один из первых языков программирования высокого уровня APL (А Program­

 

 

ming Language) был очень сложным. Он использовался в основном для финансо­

 

 

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

 

 

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

 

 

операции с массивами и матричные операции. Занимаюш.иеся APL программисты

 

 

любят этот язык. Считается хорошим вкусом написать несколько строк на APL,

 

 

показать другу и спросить: "Отгадай, что это значит?".

 

 

Однако таким специалистам не следует участвовать в коллективных проектах,

 

 

где другие люди будут заниматься сопровождением их программ. Сегодня написа­

 

 

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

 

 

ся пустой тратой времени.

 

 

 

strcpy(V.show(),"Привет") ;

/ / плохая практика

Обратите внимание, что негодование автора направлено в основном на то, что сопрово>едаюш,ему приложение программисту понадобится проделать дополни­ тельную работу для понимания программы. Действия этого оператора заключаются как будто не в оценке доступной в объекте размера динамически распределяемой памяти, а в том, чтобы досадить разбирающемуся в нем программисту. Это можно исправить с помон^ью разделения обязанностей ме>кду клиентом и сервером String:

int length = strlen(v.show());

/ /

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

strncpy(v.show(),"Привет!",length)

/ /

перенос обязанностей "вверх"

U = Это тест

V = Ничего плохого

V = Давайте надеяться V = Привет!

Для объектов String, созданных вторым конструктором преобразования, значением length будет длина последней сохраненной строки. Она может быть меньше размера доступной области. Еще более важно то, что данный метод нару­ шает принцип переноса обязанностей с клиентов на серверы и сокрытия деталей операций с данными от клиента.

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

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

v.modifiC'Hi there"); / / тест доступного пространства

Рис. 11.6.

 

На рис. 11.6 показан результат программы из листинга 11.2. Он

Результат

выполнения

демонстрирует, что вызов функции modifyO защищает динамиче­

программы

из листинга 11.2

скую память от переполнения, усекая данные клиента.

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

451

Использование указателя, возвращаемого функцией show(), не защищается. Пример порчи памяти, возможной при выполнении функции String: :show().

char *ptr

= v.showO;

/ /

необдуманный метод

ptr[200]

= 'A';

/ /

порча памяти

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

v.show()[200] = 'А';

/ / необдуманный метод, порча памяти

Это плохая практика программирования.

Защита данных объекта от клиента

C++ предусматривает способ защиты внутреннего содержимого объекта от клиента, который использует указатель, возвращаемый функцией-членом. Напри­ мер, определим возвращаемое функцией show() значение как указатель на символконстанту (а не как указатель на неконстанту в листинге 11.2).

const char* String::show() const

/ /

хорошая практика:

 

/ /

возвращается константа

{ return str; }

 

 

Теперь, если клиент попытается изменить содержимое динамически распреде­ ляемой памяти через возвращаемое функцией-членом show() значение, это будет помечено как синтаксическая ошибка.

strcpy(v.show(),"Привет!");

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

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

Перегруженная операция конкатенации

Следующий шаг состоит й проектировании перегруженной операторной функ­ ции, соединяющей две строки — два объекта String. Содержимое второго объек­ та добавляется к содержимому первого объекта. Это означает, что клиент может использовать данную перегруженную операторную функцию следующим образом:

String

иС'Это тест. " ) ;

/ /

левый операнд

String

v("Ничего плохого");

/ /

правый операнд

U += v;

 

/ /

выражение: операнд, операция, операнд

После выполнения этого фрагмента программы содержимое объекта v должно остаться прежним, а содержимое объекта и будет заменено на "Это тест. Ничего плохого".

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

u.operator+=(v);

/ / смысл и += v, -> и - это цель, а v - параметр

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

void operator += (const String s);

/ /

конкатенация параметра

 

/ /

с целевым объектом

452 Часть II • Объектно-ориентированное г1рогра1^^ирование на C-f4-

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

Алгоритм для конкатенации строки String включает следующие шаги:

1.

Сложение длин обоих символьных массивов

 

для определения обш,егочисла символов.

2 .

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

 

для размещения символов и завершающего нуля.

3 .

Проверка распределения памяти.

 

Отказ, если в системе не хватает памяти.

4 .

Копирование символов из целевого объекта

 

в выделенную в динамической области память.

5 .

Конкатенация символов из объекта-параметра

 

и размещение их в выделенной памяти.

6.

Установка указателя str целевого объекта

 

на выделенную область памяти.

Эти шаги (за исключением отказа от попытки выделить память в случае ее не­ хватки) вместе с реализующими их операторами C + + показаны на рис. 11.7. Чтобы было проще отслеживать события, здесь используются короткие строки.

В верхней части рисунка показаны два объекта типа String — объект и (с со­

держимым "Hi") и объекту (с содержимым

"there!"). Часть А демонстрирует

оба объекта

после модификации поля

1еп первого объекта, вьщеления памяти

str

П

 

w

Hi\0

str

П

w

1 there!\0

 

^

^

ien

I

1

 

 

Ien

1

 

Client code;

 

3 1

 

 

 

1^ 1

 

 

 

 

 

 

 

J

u += v;

u

 

 

 

 

s

 

 

 

А)

П

 

 

 

str

П

w

 

str

 

W

Hi\0

there!\0

 

 

^

Ien

 

1

 

 

Ien

1

 

 

 

1^ 1

 

 

1^ 1 Server соde;

 

 

 

П-- > | Hi \0

 

 

J

Ien += s.len;

 

 

 

 

 

 

p = newchar[len+1]

 

 

 

pU-

 

 

 

 

strcpy(p,str);

u

 

 

 

 

s

 

 

 

В)

П

 

 

Hi\0 1

 

П

 

 

str

1

w

str

w

there!\0

Ien

I

 

 

ien

1

J ^

strcat(p,s.str);

 

9 1

 

 

 

1^ 1

- > | Hi therel\0

1

u

 

s

С)

Hi \0 1

 

str

str

tл

 

Ien

Ien ^

 

1 Ч]

> « - к

 

—w-] H inere'vu

 

П1 1 ^ 1

^" therel\0 | str = p;

Рис. 1 1.7. Диаграмма памяти для

операторной

функции конкатенации

объектов String

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

453

в динамически распределяемой области и копирования настояш.его содержимого объекта и в динамическую память (шаги 1 —4 алгоритма). Часть В показывает со­ стояние динамически распределяемой памяти после шага 5. Часть С изображает состояние объектов после установки указателя str целевого объекта и на выде­ ленную в динамической области память (шаг 6).

Если собрать все вместе, то получится:

void

String::operator += (const String

s)

/ /

параметр-объект

{ char* p;

 

/ /

локальный указатель

len

= strlen(str)

+ strlen(s . str);

/ /

общая длина

 

p = new char[len

+ 1 ] ;

/ /

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

i f

(p==NULL) exit(1);

/ /

проверка на успех

strcpy(p,str);

 

/ /

копирование

первой части результата

strcat ( p . s . s t r ) ;

 

/ /

конкатенация

второй строки

str

= р; }

 

/ /

str указывает на новую память

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

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

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

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

Предотвращение ^^утечек памяти'^

Как уже упоминалось, на рис. 11.7 показано, что есть проблема с возвратом символьного массива в динамически распределяемой области памяти, на который ссылается целевой указатель str в начале вызова функции. Когда указатель str устанавливается на вновь выделенный сегмент памяти (куда ссылается локальный указатель р), массив становится недоступным. "Утечка памяти" — распростра­ ненная ошибка при управлении памятью. Чтобы предотвратить ее, нужно вернуть символьный массив до того, как указатель str будет установлен на новый выде­ ленный массив.

void

String:-.operator += (const String s)

/ /

параметр-объект

{ char* p;

/ /

локальный указатель

len

= strlen(str) + s t r l e n ( s . s t r ) ;

/ /

общая длина

p = new char[len + 1];

/ /

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

i f

(p==NULL) exit(1);

/ /

проверка на успех

strspy(p,str);

/ /

копирование первой части результата

strcpy(p,s . str);

/ /

конкатенация второй строки

delete str;

/ /

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

str

= р; }

/ / s t r указывает на новую память

Рис. 11.8 аналогичен рис. 11.7. Он показывает, что динамически распределя­ емый символьный массив, на который указывает целевой элемент данных str, исчезает в результате операции delete. Только после этого указатель str устанав­ ливается на новый массив в динамически распределяемой области памяти.

Поговорим об "ут^ечке памяти". Покажем, какие опасности связаны с исполь­ зованием различных средств. При написании программы С+-Н никогда не нужно забывать о них. Как часто бывает, источником проблемы здесь является передача объектов в качестве значений параметров.

454

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

sir len

П

1 w | Li: \f\ 1

str

|_...,..^i ^^1 yj 1

1

 

len

1^ 1

 

П1

1

w

there!\0

1

^

1^ 1

 

Client cocie;

 

J

 

u += v;

u s

А)

 

П

 

 

 

str

П

1

 

w

there!\0

Sir

 

^ j

Ml \U 1

 

len

 

1

 

 

 

len

 

1

1

 

 

 

 

1' 1

 

 

 

1^ 1 Server code;

 

 

MH'^O

 

 

 

J

 

 

len += s.len;

 

 

*l

 

 

 

 

 

 

p = newchar[len+1]

 

 

 

 

 

 

 

 

 

 

 

 

strcpy(p,str);

u

 

 

 

 

 

s

 

 

 

 

 

 

В)

 

П

 

 

 

 

П

1

 

^

 

sir

1

w 1 i j :

\л 1

str

 

there!\0

^

1II

\U 1

1

 

"

len

 

1

 

 

 

len

 

1

 

 

 

strcat(p,s.str);

 

 

1^ 1

 

 

 

1^ 1

 

 

 

^ 1

I~"^| Hi there!\0

 

 

 

 

 

 

 

 

 

1

 

 

 

 

 

 

 

 

 

 

u

 

 

 

 

 

s

 

 

 

 

 

 

С)

1 П

N

 

^

 

П

 

 

w

 

str

w^l |_Ьдп 1

str

1

 

there!\0

 

 

 

 

 

 

1'1 1

 

 

 

 

 

 

 

 

 

len

 

[ - > | HI there!\0

len

1 6 1

 

 

 

delete str;

 

 

1

 

 

 

 

 

 

 

 

 

\

 

 

 

 

 

 

 

 

 

 

u

 

 

 

 

 

s

 

 

 

 

 

 

D)

 

 

 

 

 

 

П

 

 

^1

 

sir J

 

1

Hi \0 1

str

 

 

there!\0

1 ^ П

 

 

 

 

11^ 1

1

 

len

 

 

 

 

len

 

str = p;

 

 

I. 1

h

^

"' there!\0

 

 

 

 

 

 

 

Рис. 11-8. Диаграмма памяти для исправленной операторной

 

 

функции

конкатенации

объектов

String

Защита целостности программы

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

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

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

Когда при передаче по значению создается фактический объект-аргумент, вы­ зывается подставляемый системой конструктор копирования. Он копирует элемен­ ты данных фактического аргумента в соответствующие данные своей локальной копии — формального параметра-объекта. При копировании элемента данных str указатель в формальном параметре-объекте получает значение, хранимое в указателе фактического объекта-аргумента, т. е. адрес динамической памяти, выделенной ддя фактического аргумента.

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

 

 

455

str

п

W

Mi \П i

1

 

 

 

 

 

 

 

 

 

^

П1 \и

 

 

 

 

 

 

 

 

 

ten

11^ 1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Server code;

 

 

 

 

А)

 

 

 

 

 

 

 

Ien += s.len;

 

 

 

 

 

Н

Hi \0

 

str

 

p = new char[len+1];

 

 

 

 

 

 

 

 

 

1 ^

strcpy(p,str);

 

 

 

 

 

 

 

 

 

 

ien

 

 

 

 

 

 

 

 

 

 

strcat(p,s.str);

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

["]—>-[ Hi there!\0

 

 

 

 

 

 

 

 

 

8)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

str

^ j

0

 

 

 

str

 

Server code;

 

 

 

 

 

 

 

 

 

 

deiete str;

 

 

 

 

ien

1

 

Hi there!\0

ien

1 6

str = p;

 

 

 

 

 

 

D ^

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

П

 

 

1

^

1

 

 

 

 

 

 

 

str

1

 

N

/

C)

 

 

 

 

 

 

— ^

 

th»epe!\0

 

 

 

 

 

 

 

 

 

 

1

• •,

ЧN

1

str

F^J

 

 

 

str

ien

1^ 1

 

 

 

 

ien

 

 

 

ien

I 6 1

Server code;

 

 

 

 

 

1 Ч[Р^Ёtliere!\0

 

 

} // конец функции

 

Рис. 1 1.9. Диаграмма памяти для передачи объекта

String

 

 

 

 

 

по значению

 

 

 

 

 

 

 

 

 

 

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

Данная ситуация изображена на рис. 11.9, где показан локальный объект с эле­ ментами данных, инициализированными значениями фактического аргумента v. Рис. 11.9(A) демонстрирует, что этот локальный объекту и фактический аргу­ мент U совместно используют одну и ту же область динамически распределяемой памяти. На рис. 11.9(B) видно, что после распределения новой области динамиче­ ской памяти, инициализации и замены суидествующей области динамической па­ мяти в целевом объекте локальный объект s и аргумент и продолжают совместно использовать обшую область динамически распределяемой памяти.

Теперь — о завершении функции. Когда функция достигает закрывающей фи­ гурной скобки ее области действия и она завершает работу, локальный объект (String s) уничтожается. Обычно это означает, что исчезает (освобождается) па­ мять объекта (в данном случае указатель и целое значение). Между тем в С+ + не уничтожается объект. Каждому уничтожению объекта предшествует вызов особой функции — деструктора.

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

String:

:"'String()

 

 

{ delete

[ ] str; }

/ /

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

 

 

/ /

на которую ссылается указатель

456

Часть II • Объектно-ориентированное oporpoi^r^^i:

 

 

 

 

 

 

Рис. 11.9(C) показывает состояние локального объекта s и фактического dyiy-

 

мента V после вызова деструктора и перед уничтожением локального объекта. Он

 

демонстрирует, что локальный объект и фактический аргумент теряют свою дина­

 

мически распределяемую память. (Освобождается память, на которую ссылается

 

указатель str.) Конечно, это действие не влияет на состояние целевого объекта,

 

так как он не уничтожается. При завершении работы перегруженной операторной

 

функции целевой объект находится в том же состоянии, что и при предыдущем

 

обсуждении (см. рис. 11.8). Клиент даст корректные результаты.

 

 

 

String uC'Hi " ) ;

String v("there!");

 

 

 

 

 

 

 

 

u += v;

 

 

 

 

 

 

 

 

/ /

выводит "Hi

there!"

 

cout

«

" и = " «

u.showO

«

endl;

 

 

 

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

 

параметра s уже не принадлежит данному объекту. Она относится к фактическо­

 

му аргументу, т. е. к определенному в пространстве клиента объекту v. После вы­

 

зова функции объект клиента, который используется как фактический аргумент

 

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

 

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

 

 

 

String

U ("Hi

');

String v("there!");

 

// выводит "Hi

"

 

 

cout

«

"

u = '

«

U.showO

«

endl;

 

 

 

cout

«

"

V = '

«

v.showO

«

endl;

 

// выводит "there!"

 

u += v;

" u = '«

U.showO

«

endl;

 

// выводит "Hi there!"

 

cout «

 

 

cout «

•' V = '«

V.showO

«

endl;

 

// выводит все, что угодно

 

Не нужно проверять значение объекта v, который только что отображен на экра­

 

не и использован как г-значение в вызове функции operator+=(). Это сделано

 

здесь только потому, что проблема сданной реализацией известна заранее. Ясно,

 

что у объекта должно быть то же значение, что и при использовании в качестве

 

операнда

в выражении и += v. В большинстве случаев (но не во всех) в С+ +

 

работает интуиция программиста. Поэтому нужно выработать альтернативную

 

интуицию. Стоит еще раз повторить это. Даже

в таком

невинном

на

первый

bti Chapter

 

 

 

 

 

 

 

_^^

взгляд клиентском коде значением

 

 

 

 

 

 

 

i^*^l

(текстом) в объекте v может быть

u = Это тест.

 

 

 

 

 

 

 

все,

что угодно, и любое исполь­

V = Ничего

плохого.

 

 

 

 

 

 

 

зование данного объекта, предпо­

U = Это тест.Ничего плохого.

 

 

 

 

 

 

V = Давайте

надеяться на л1 I

I

I I

 

 

 

 

лагая,

что

он имеет

то же

 

 

 

 

состояние, что и прежде, будет

 

 

 

 

 

 

 

 

 

^mcrosoftУi%lmШ*ШovШШ

 

 

 

 

безрассудным.

 

 

 

 

 

 

 

 

 

 

 

 

Есть еще одна скобка, завер­

 

Program: .AMICROSOFT VISUAL

 

 

 

шающая область действия. Обра­

 

 

 

 

щайте внимание на все фигурные

 

STUDIO\MYPROJECTS\CHATER\DEBUG\CHAPTER.[

 

 

 

 

 

 

 

 

 

скобки,

ограничивающие

области

 

Expression: _BLOCK_TYPE_IS__VALID(pHead->nBiockUse)

действия. Они выполняют немало

 

работы.

Когда

клиент

достигает

 

 

 

 

 

 

 

 

 

 

For information on how your program can cause an assertion

закрывающей

фигурной

скобки,

 

failure, see the Visual C++ documentation on asserts.

 

и область действия завершается,

 

(Press Retry to debug the application)

 

 

 

 

 

для

всех

локальных

объектов,

 

 

 

 

 

 

 

 

 

включая объект v, который исполь­

 

 

 

 

 

 

 

 

 

зовался

как

фактический аргумент

 

 

 

 

 

 

 

 

 

при обращении к функции, вы­

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

выполпепия

программы

 

зываются деструкторы

класса.

 

Деструктор пытается освободить

 

из лист,инга

11.3

 

 

 

 

^

rj

г

 

 

 

 

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

457

память, на которую указывает элемент данных str. Между тем, эта память уже возвраш,ена системе. При разработке языка можно было бы оформить такой вызов как операцию "по ор". В С+Н- повторное использование операции delete

стем же указателем запрещено. Это ошибка.

Ксожалению, "ошибка" не означает, что компилятор сообш^ит о синтаксиче­ ской ошибке, которую можно исправить. Разработчик компилятора не следит за ходом выполнения программы и не выявляет ошибки программиста — анали­ зируется лишь синтаксическая корректность кода. Это также не означает, что программа компилируется, выполняется и дает повторяющиеся некорректные результаты. Все зависит от платформы. На поведение приложения влияет опера­ ционная система. Система может аварийно завершить работу.

Листинг 11.3 демострируетет полную программу, реализующую такую плохую архитектуру. Вывод программы показан на рис. 11.10.

Листинг 11.3. Перегруженная функция конкатенации с параметром-значением

#inclucle <iostream> using namespace std;

class String {

 

 

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

 

char

*str;

 

 

 

int

len;

 

 

 

 

public:

 

 

 

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

 

String

(int

length=0);

 

 

String(const

char*);

 

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

 

"String

0 ;

 

 

// освобождение памяти

 

void operator += (const String);

// конкатенация с другим объектом

 

void modify(const char*);

// изменение содержимого массива

 

const char* showO const;

// возврат указателя массива

String::String(int length)

 

 

 

{

len = length;

 

 

 

 

str = new char[len+1];

 

// проверка науспех

 

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

 

 

str[0] = 0; }

 

// пустая строка нулевой длины - ОК

String::String(const char* s)

// определение длины входного текста

{

len = strlen(s);

 

 

str = new char[len+1];

 

// выделение достаточной памяти вдинамической области

 

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

 

// проверка науспех

 

strcpy(str,s); }

 

// копирование входной строки в динамически

 

 

 

 

 

 

// распределяемую память

String: :"'String()

 

// возврат памяти вдинамической области (не указателя)

{ delete str; }

 

 

void String::operator += (const String s)

// передача noзначению

{

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

 

// общая длина

 

char *p = new char[len +

1];

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

 

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

 

// проверка науспех

 

strcpy(p,str);

 

// копирование первой части результата

 

strcat (p.s.str);

 

// добавление второй части результата

 

delete str;

 

 

// важный шаг

 

str = p; }

 

 

// теперь р может исчезнуть

const char* String::ShowO

const

// защита данных отизменений

{

return str; }

 

 

 

 

458

Часть II • Объектно-ориентированное профоммирование на C++

void String: :mociify(const char a[])

/ /

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

{

strncpy(str,a,len-1);

/ /

защита от переполнения

 

str[len - 1] = 0; }

/ /

правильное завершение строки

int mainO

{String иС'Проверка");

String у("Ничего плохого не случится");

cout

«

'

u =

«

u.showO

«

endl;

/ /

результат OK

cout

«

'

V =

«

v.showO

«

endl;

/ /

результат OK

u += v;

 

 

 

 

 

 

/ /

u.operator+=(v);

cout

«

''

u =

«

U.showO

«

endl;

/ /

результат OK

cout

«

''

V =

«

V.showO

«

endl;

/ /

результат не OK

v.modify("Давайте

надеяться на лучшее.");

/ /

порча содержимого памяти

cout

«

" V = "

«

V.showO

«

endl;

/ /

????

return 0;

 

 

 

 

 

 

 

}

 

 

 

 

 

 

 

 

 

Обратите внимание, что все неприятности происходят при завершении функ­ ции. Первая проблема возникает, когда завершается серверная перегруженная операторная функция operator+=() и вызывается деструктор для формального параметра — фактический аргумент v теряет свою динамически распределяемую память. Вторая неприятность случается, когда завершает работу клиент main() и объект V оказывается вне области действия. В этом случае память освоболодается повторно.

В С+Ч- повторное освобождение динамически распределяемой области памяти считается ошибкой. Удаление же указателя NULL не будет ошибкой. Это "пустая операция". Некоторые программисты пытаются решить проблему, присваивая в деструкторе указателю на динамическую память значение NULL.

String: i^StringO

 

 

{ delete

str;

/ /

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

str = 0;

}

/ /

установить в null, чтобы избежать

 

 

/ /

двойное освобождение памяти

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

Переход из пункта А в пункт В

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

В течение месяца или нескольких лет программа может работать корректно. Однако после установки каких-то других приложений или перехода на следующую версию Windows изменяется характер использования памяти, и программа за­ вершается аварийно или дает некорректные результаты. Это может остаться незамеченным, т. к. ранее программа всегда работала корректно. Что же делать? Ругать Microsoft, поскольку вы только что модернизировали операционную сис­ тему? Но Microsoft тут ни при чем! Это ошибка программиста, забывшего включить один символ & в интерфейс перегруженной операторной функции, такой как operator-b=().

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

459

Вот как должна выглядеть эта функция. Она не передает свой параметр-объект

по значению. Он передается по ссылке.

 

 

 

 

void String: .-operator += (const

String

&s)

/ /

параметр-ссылка

{ len

= strlen(str) + s t r l e n ( s . s t r ) ;

 

/ /

общая длина

char

*p = new char[len + 1];

/ /

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

объема памяти

i f (p==NULL) e x i t d ) ;

/ /

проверка

на успех

 

strcpy(p,str);

/ /

копирование первой части результата

s t r c a t ( p , s . s t r ) ;

/ /

добавить

вторую часть

результата

delete

str;

/ /

важный шаг

 

 

str - p;

}

/ /

str указывает на новую память

U = Проверка

 

 

 

На рис. 11.11 представлен результат программы из

 

не случится

 

листинга 11.3 с функцией конкатенации, передающей

V = Ничего

 

плохого

 

параметр по ссылке.

U = Проверка

Ничего

плохого не случится

Попробуйте выполнить эту программу, поэкспери­

V = Ничего

 

плохого

не случится

 

V = Давайте

надеяться на л

 

ментируйте с ней. Не поддавайтесь желанию переда­

Press any

key to continue.

 

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

 

необходимости.

Рис. 11.11.

 

 

 

Конечно, разочаровывает то, что изменить програм­

 

 

 

му можно с помошд^ю добавления или удаления всего

Результат

 

программы из листпинга 11,3

одного символа в исходном коде программы (амперсан-

с операцией

копкатпепации,

передающей

да). Обратите внимание, что обе версии синтаксически

параметр

по

ссылке

 

корректны. Компилятор не показывает, что могут воз­ никнуть проблемы.

Передавать параметры-объекты по значению — все равно, что ездить на танке. Вы попадете, куда хотите, но при этом натворите немало бед. Не спешите передавать объекты по значению, если это не является срочным решением.

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

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

если функция не изменяет состояние объекта-параметра и целевого объекта.

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

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

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

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

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

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