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

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

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

юнструкторы^ ^ / ^ и деструкторы:

потенциальные проблемы

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

^Передача объектов по значению

%^ Перегрузка операций для нечисловых классов

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

*^ Перегрузка операции присваивания ^ Практические аспекты: способы реализации |/ Итоги

^^^^^анее рассматривались проблемы, связанные с одинаковой интерпре-

J ^ ^ ^ Г тацией в программе C + + встроенных типов и типов, определяемых

Шпрограммистом.

Вглаве 10 обсуждались вопросы конструирования числовых классов, таких как Complex и Rational. Объекты данных классов — это полноценные экземпляры объектов. К ним применимо все, что возникает при работе с объектами, объявле­ ние класса, управление доступом к компонентам класса, проектирование функцийчленов, определение объекта, его инициализация и передача объекту сообщений

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

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

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

441

Объекты определяемых программистом классов могут обрабатываться в коде клиента аналогично переменным встроенных типов. Таким образом, имеет смысл поддерживать для них перегруженные операции присваивания. Принцип С++, предусматриваюш^ий интерпретацию встроенных и определяемых программистом типов, работает и для этих классов. В данной главе рассматриваются перегружен­ ные операции д/ш классов, объекты которых можно складывать, перемножать, вы­ читать или делить. Например, для операций со строками текста в памяти может использоваться класс St ring. Из-за нечислового характера таких классов перегру­ женные операторные функции для них становятся искусственными. Например, с помош^ью перегруженной операции сложения можно либо реализовать конкате­ нацию строк, либо сравнить строки. Однако сложно найти способы для сложения или деления объектов String. Тем не менее, нужно знать, как работать с перегру­ женными операторными функциями для нечисловых классов.

Эти нечисловые классы имеют важное отличие: объекты одного класса могут использовать разные объемы данных. Объекты числовых классов всегда применя­ ют одни и те же объемы памяти. Например, в классе Rational присутствуют два элемента данных одного размера — числитель и знаменатель.

В классе String объем текста, хранимый в одном объекте, отличается от объе­ ма текста в другом объекте. Ес/ш класс зарезервирует для каждого объекта один и тот же объем памяти, то вы столкнетесь с такими проблемами: непроизводи­ тельной тратой памяти (когда фактический, используемый объем памяти меньше зарезервированного места) или переполнения памяти (когда объект должен хранить слишком много текста). Эти две опасности подстерегают разработчиков классов, выделяюш,их для каждого объекта один и тот же объем памяти.

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

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

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

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

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

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

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

442

Часть I! о Объектно-ориентировонное програ1^1У1ирование на С-^^

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

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

Всоответствии с данным подходом, передача по значению должна быть

ограничена передачей встроенных типов как входных параметров функций и возвратом из функций параметров встроенных типов. Почему это приемлемо для входных значений встроенных типов? Их передача по указателю усложняет программу и может ввести в заблуждение при ее чтении (можно подумать, что данный параметр изменяется в функции). Передача параметров по ссылке (с ключевым словом const) не очень трудна. Таким образом, для встроенных типов используйте передачу параметров по значению.

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

Кроме того, ранее говорилось об инициализации и присваивании. Хотя в том

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

Из всех функций класса Rational оставим только эти три: normalizeO, show()

иoperator+(). Обратите внимание, что перегруженная операция operator+() не является функцией-членом класса Rational. Это "дружественная" функция. Именно поэтому в начале абзаца говорится: "Из всех функций класса Rational",

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

Листинг 11.1. Пример передачи параметров-объектов по значению

#include <iostream.h>

 

 

 

 

 

 

 

 

 

 

 

class Rational {

 

 

 

 

 

 

 

 

/ /

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

long

nmr;

dnm;

 

 

 

 

 

 

 

 

void

normalizeO;

 

 

 

 

 

 

 

 

/ /

закрытая

функция-член

public:

 

 

 

 

 

 

 

 

 

/ /

конструктор: общий, преобразования по умолчанию

Rational(long

n=0, long

d=1)

 

 

 

 

{

nmr = n;

 

dnm = d;

 

 

 

 

 

 

 

 

 

 

 

 

this->normalize();

 

 

 

 

" «

dnm «

endl;

 

 

 

cout «

"

создан: "

«

nmr «

"

 

 

Rational(const Rational

&r)

 

 

 

 

 

 

 

 

 

{

nmr = r. nmr; dnm =

r.dnm;

 

 

 

 

 

 

 

 

 

 

cout «

"

скопирован: " «

nmr

«

«

dnm «

endl; }

 

void

operator

= (const Rational

&r)

 

 

 

/ /

операция

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

{

nmr = r.nmr; dnm = r.dnm;

 

 

 

«

dnm «

endl; }

 

 

cout «

"

присвоен: " «

 

nmr «

"

 

"Rationale)

 

 

 

 

 

 

 

" «

dnm «

// деструктор

{

cout «

"

уничтожен: "

«

nmr

«

endl; }

 

friend Rational operator + (const Rational x, const Rational y); void showO const;

// конец спецификации класса

 

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

I 443 1

void Rational::show() const

 

 

{ cout « " " « nmr « V " « dnm; }

 

 

void Rational::normalize()

// закрытая функция-член

{ if (nmr ==0) { dnm = 1; return; }

 

 

 

int sign = 1;

 

// сделать оба положительными

 

if (nmr < 0) { sign = -1; nmr = -nmr; }

 

if (dnm < 0) { sign = -sign; snm = -dnm;

// наибольший общий делитель (НОД)

 

long gcd = nmr, value = dnm;

 

while (value !=gcd) {

// стоп, если найден НОД

 

if (gcd > value

 

// вычесть меньшее из большего

 

gcd = gcd - value;

 

else value = value - gcd; }

// сделать dnm положительным

 

nmr = sing * (nmr/gcd); dnm = dnm/gcd; }

Rational operator + (const Rational x, const Rational y)

 

{

return Rational(y.nmr*x.dnm + X. nmr*y.dnm, y.dnm*x.dnm); }

 

int mainO

b(3,2), c;

 

 

{

Rational a(1.4),

 

 

 

cout « endl;

 

 

 

 

с = a + b;

" +"; b.showO; cout «

" ="; c.showO;

 

 

a.showO; cout «

 

cout « endl « endl; return 0;

В обобщенном конструкторе Rational добавлен отладочный оператор. Он будет выполняться при каждом создании и инициализации объекта Rational в начале функции mainO и в функции operator+().

Rational::Rational(long

n=0, long

d=1)

/ /

значения no умолчанию

{ nmr = n;

dnm = d;

 

 

/ /

инициализация данных

this->normalize();

 

 

 

 

cout «

"создан: " «

nmr « "

" « dnm «

endl; }

 

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

Rational::Rational(const Rational &r)

/ /

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

{ nmr = r.nmr; dnm = r.dnm;

/ /

копирование элементов данных

cout « " скопирован: " << nmr << "

« dnm « endl; }

Данный конструктор вызывается, когда аргументы типа Rational передаются по значению дружественной операторной функции operator+(). Между тем этот конструктор копирования не вызывается, когда operator+() возвращает значение объекта, так как перед возвратом из функции operator+() вызывается общий конструктор с двумя аргументами.

Конструктор не выполняет никаких осмысленных операций для класса Rational. Он добавлен только в целях отладки. Деструктор вызывается всякий раз при унич­ тожении объекта Rational.

Самая интересная функция здесь — это перегруженная операторная функция. Ее задача состоит в копировании элементов данных одного объекта Rational в ком­ поненты данных другого объекта Rational. Чем она отличается от конструктора

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

444

Часть II • Обьектно-ориентированное програ1м11^ирование на C-^-f

создан: 1 4 создан: 3 2 создан: О 1

скопирован: 3 2 скопирован: 1 4 создан: 7 4 уничтожен: 1 4 уничтожен: 3 2 присвоен: 7 4 уничтожен: 7 4 1/4 + 3/2 = 7/4

уничтожен: 7 4 уничтожен: 3 2 уничтожен: 1 4

копирования? На данном этапе, ничем. Отличаться будет возвращаемый тип. Конструктор копирования не должен его иметь. Здесь возвращается тип void.

void Rational:ioperator = (const Rational &r)

// прк^сваивание

{ nmr = r.nmr; dnm = r.dnm;

// копирование данных

cout « " присвоен: " « nmr « " " « dnm «

endl; }

Перегруженная операция присваивания — это операция с двумя операндами. Во-первых, она имеет один параметр типа класса, и это функция-член, а не функ­ ция friend. Она работает с двумя объектами: получателем сообщения и парамет­ ром. Во-вторых, такая двухместная операция всегда записывается между первым и вторым операндом. При сложении двух операндов записывается первый опе­ ранд, операция, затем второй операнд (например, а + Ь). При применении при­ сваивания также записывается первый операнд, операция, затем второй операнд (например, а = Ь). В случае вызова функции объект а является получателем сооб­ щения. В приведенной выше операции присваивания nmr и dnm принадлежат целе­ вому объекту а. Объект b — это аргумент вызова функции. В данной операции присваивания r.nmr и r.dnm принадлежат фактическому аргументу* Ь. Следова­ тельно, синтаксисом вызова операторной функции будет а. operator = (b).

Данная операция возвращает void, поэтому она не может продолжить цепочку присваивания в клиенте, такую как а = b = с. Следовательно, возвращаемое при­ сваиванием Ь = с (или Ь. operator = (с)) значение используется как параметр в присваивании а.operator = (b.operator(c)). Чтобы данное выражение было допустимым, операция присваивания должна возвращать значение типа класса (здесь — Rational). Поскольку операция присваивания спроектирована так, что возвращает тип void, цепочка операций будет помечена компилятором как син­ таксическая ошибка. Для первого анализа операции присваивания это не важно. Цепочка присваиваний описывается ниже.

Результат программы из листинга 11.1 показан на рис. 11.1. Первые три сообщения "создан" — результат задания и инициали­ зации трех объектов Rational в функции main(). Два сообщения "скопирован" выводятся в результате передачи потока данных пере­ груженной операторной функции operator+(). Следующее сообще­ ние "создан" появляется при вызове конструктора Rational в теле функции operator+().

Все конструкторы вызываются в начале выполнения функции. Далее следует серия событий, когда выполнение достигает закры­ вающей фигурной скобки в теле функции, уничтожаются локаль­ ные и временные объекты. Первые два сообщения "уничтожен" генерируются при уничтожении локальных копий фактических аргументов (3/2 и 1/4) и вызове деструкторов для этих объектов. Объект, содержащий сумму параметров^ не может уничтожаться перед использованием в операции присваивания. Следующее сооб­ щение "присвоен" появляется в результате вызова перегруженной операции присваивания, а сообщение "уничтожен" — после вызова деструктора для созданного в теле функции operator+() объекта.

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

Если добавить в интерфейс функции operator+() два амперсанда, последова­ тельность событий будет другой.

Rational operator + (const Rational &x, const

Rational &y)

// ссылка

{ return Rational(y.nmr*x.dnm + X. nmr*y.dnm,

y.dnm*x.dnm);

 

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

445 I

При программировании на С+4- важно, чтобы разные части программы были согласованы. Здесь изменяется интерфейс прототипа функции и обновляется объявление функции в спецификации класса. (Не важно, какая это функция — функция-член или функция friend.) В данном случае несогласованность разных частей программы не смертельна — компилятор предупредит, что она содержит синтаксические ошибки.

Результаты выполнения программы из листинга 11.1с функцией operator+() показаны на рис. 11.2. Видно, что отсутствуют четыре вызова функций: два пара­ метра-объекта не создаются и два параметра-объекта не уничтожаются.

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

если применим модификатор const.

Далее покажем разницу между инициализацией и присваива­ нием в листинге 11.1. Здесь в выражении с = а + b осуществляется присваивание переменной с. Откуда известно, что это присваива­ ние, а не инициализация? Потому что имя типа находится слева от с. Сам тип определяется ранее в начале функции main(). Данная версия mainO создает и инициализирует объект с суммой а и Ь, а не задает и присваивает значение с в отдельных операторах.

создан: 1 4 создан: 3 2 создан: О 1

создан:

7 4

присвоен:

7 4

уничтожен: 7 4 1/4 + 3/2 = 7/4

уничтожен: 7 4 уничтожен: 3 2 уничтожен: 1 4

Рис. 11.2.

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

создан: 1 4 создан: 3 2 создан: 7 4 1/4 + 3/2 = 7/4

уничтожен: 7 4 уничтожен: 3 2 уничтожен: 1 4

Рис. 11.3.

Результпат выполнения программы из листинга 11.1 при передаче парамеппров по ссылке и использование инициализации объектное

int mainO

 

 

 

 

 

{ Rational

а(1,4),

b(3,2),

с = а + b;

c.showO;

a.showO;

cout

«

" +";

b.showO; cout «

cout «

endl «

endl;

 

 

return

0;

}

 

 

 

 

Ha рис. 11.3 показаны результаты выполнения программы из листинга 11.1 с данной версией функции main() и при передаче параметров по ссылке. Как видно, операция присваивания и конст­ руктор копирования не используются. Это естественный результат перехода от передачи по значению к передаче по ссылке.

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

libС о в е т у е м Различайте инициализацию объектов и присваивание им значений. При инициализации вызывается конструктор и не учитывается операция присваивания. При присваивании вызывается операция присваивания и обходится вызов конструктора.

Рекомендуем избегать передачи параметров-объектов по ссылке, различать инициализацию и присваивание. Нужно уметь прочитать программу и сказать: "Здесь вызывается конструктор, а используется присваивание". Тренируйте свою интуицию, чтобы легко выполнять подобный анализ.

г

446

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

 

Перегрузка операций дая нечисловых классов

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

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

Сначала был интерфейс командной строки, и пользователям приходилось на­ бирать длинные команды с параметрами, ключами, переключателями и т. д. Для упрощения ввода придумали также меню с текстовыми пунктами. Выбирая пункт меню, пользователь мог инициировать команду, не набирая ее. Другой вариант — оперативные клавиши. Если пользователь будет нажимать последовательность таких клавиш, то он активизирует команду непосредственно. Ему не надо отры­ ваться от клавиатуры и проходить несколько меню с подменю. Наконец, появи­ лась инструментальная панель с графическими командными кнопками. Нажимая на такую кнопку, пользователь активизирует команду. Ему не надо запоминать комбинацию клавиш. Значки на этих кнопках понятны: Open, Close, Cut, Print. При добавлении новых подобных значков они становятся менее понятными. Появляются кнопки New, Paste, Output, Execute и т. д.

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

Класс String

Рассмотрим пример использования перегруженных операторных функций для нечисловых классов: операторную функцию для конкатенации текста.

Введем класс String с двумя элементами данных: указателем на динамически распределяемый массив символов и целым, задающим максимальное число допус­ тимых символов, которые можно вставлять в массив в динамически распределяе­ мой памяти. Библиотека C + + Standard Library содержит класс String (с первой буквой в нижнем регистре). Это более мощный класс, чем String в данных приме­ рах, однако он сложнее.

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

Первый конструктор преобразования с параметром, задающим длину строки, имеет по умолчанию нулевое значение аргумента. Если объект String создается с помощью этого значения по умолчанию (параметры не указываются), то длина текста для такого объекта будет равна нулю. В этом случае первый конструктор преобразования применяется как конструктор по умолчанию, например String s;.

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

447

Второй конструктор преобразования с задаваемым в параметре символьным массивом не имеет значения аргумента по умолчанию. Ввести его было бы не­ сложно, например пустую строку, но тогда компилятору будет трудно интерпрети­ ровать вызов функции String s;. Какой конструктор со значением по умолчанию следует вызывать: первый (с нулевой длиной) или второй (с пустой строкой)?

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

Листинг 11.2. Класс String с динамически распределяемой памятью

#inclucle <iostream> using namespace std;

class String {

 

 

 

// динамически

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

массив

 

char *str;

 

 

 

 

int len;

 

 

 

 

 

 

 

 

 

public-

(int length=0);

 

// конструктор

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

умолчанию

 

String

 

 

String

(const char*);

 

// конструктор

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

 

 

 

"String

0 ;

 

char*);

 

// освобождение

памяти

 

 

 

void modify(const

 

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

 

 

 

char* showO

const;

 

// возврат

указателя.массива

 

 

String::String

(const char* s)

 

 

 

 

 

 

{

len = strlen (s);

 

 

// размер noумолчанию равен единице

 

 

str = new char [len+1];

 

 

 

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

 

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

 

 

 

 

str [0] = 0; }

 

 

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

 

String::String(int length)

 

// определение длины

входной строки

 

{

len = length;

 

 

 

 

str = new char[len+1];

 

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

вдинамической области

 

if (str==NULL) exit(iy;

 

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

 

 

 

 

strcpy(str,s); }

 

 

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

String::~String()

 

 

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

области

(не указателя)

{ delete str; }

 

 

 

void String::modify(const char a[]) // здесь неуправления памятью

 

 

{

strncpy(str,a,len-1);

 

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

 

 

 

str[len-1] = 0; }

 

 

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

 

 

char* String::show() const

 

// плохая практика, но допустимо

 

 

{

return str; }

 

 

 

 

 

 

 

 

 

int mainO

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

 

 

 

 

 

 

 

{

String

 

 

 

 

 

 

 

 

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

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

 

 

 

cout «

" u = " «

u.showO

«

endl;

 

 

 

cout «

" V = " «

v.showO

«

endl;

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

 

 

 

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

надеяться");

// ввод усекается

 

 

 

cout «

" V = " «

V.showO

«

endl;

 

 

 

 

 

 

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

 

 

 

 

 

 

 

 

cout «

" V = " «

V.showO

«

endl;

 

 

 

 

 

return 0; }

448

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

д»

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

Первая строка первого конструктора преобразования задает значение элемента данных 1еп, а вторая — зна­ чение элемента данных str, выделяя в динамически рас­ пределяемой области память соответствующего размера. Затем проверяется, успешно ли выделена память. В на­ чало выделенной области памяти помещается нуль (сим­ вол '\0'). Для любой библиотечной функции С+Ч- это текстовое содержимое выглядит как пустое, хотя там есть место для заданного клиентом числа символов.

Если клиент определяет объект String и не задает аргументов, то данный конструктор используется как конструктор по умолчанию, выделяющий в динамической области один символ при присваивающий пустой строке значение '\0'.

На рис. 11.4 показана схема памяти при выполнении каждого оператора конструктора для выражения:

А) 1 string t(20);

ienstr

1 ^^ 1

Ien = lenght;

П ,

 

t

 

 

 

В)

П

w^

\o

str

Ien

1

 

str = new char[len+1];

 

1 ^° J1

 

str[0] = '\o':

Рис. 11.4. JXuaz'^aMMa памяти для первого конструктора в листинге 11.2

String t(20); / / 21 символ в динамически распределяемой области

На рис. 11.4(A) представлена первая фаза выполнения конструктора, а на рис. 11.4(B) — вторая фаза. Прямоугольником обозначен объект t типа String с двумя элементами данных — указателем str и целым Ien. Эти элементы данных занимают одну область памяти, но указатель на меньший прямоугольник подчер­ кивает тот факт, что он не содержит данных для вычислений. Имя объекта t и имена элементов данных изображены вне прямоугольника объекта.

MactbA показывает, что после выполнения оператора Ien = length элемент данных Ien содержит значение 20 (содержит значение), а указатель str остается неинициализированным (ссылается куда угодно). Часть В демонстрирует, что после выполнения остальной части конструктора выделяется область в динами­ ческой памяти (21 символ), на нее ссылается указатель str, а первому символу присваивается значение 0. Читателям надо иметь диаграммы для всех фрагментов программ, где выполняются операции с памятью. Таким образом вы сможете луч­ ше понять, как динамически управлять памятью.

Первая строка во втором конструкторе преобразования определяет длину за­ данной клиентом строки.и устанавливает значение элемента данных Ien. Вторая строка присваивает значение элементу данных str и копирует внесенные клиентом символы в выделенную память. Библиотечная функция strcpyO копирует симво­ лы из массива-аргумента и добавляет завершающий нуль. На рис. 11.5 показаны этапы инициализации объекта для следующего оператора:

String иС'Это тест"); // 15 символов, 16 символов в динамической области

А) string utf'Sio тест.")

Ienstr

1 ^^ 1

Ien = strien(s);

П ,

 

u

 

 

 

 

 

В)

П

1

w Это тест.Ю

str

Ien

1

1

^

str = new char[len+1];

 

1 ^^J

1

strcpy(str,s);

Рис. 11.5.

 

 

 

памяти

Диаграмма распределения

и второй

конструктор

 

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

из листинга

11.2

 

 

 

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

Третий метод не поддерживает длину строки как элемент данных, а вычисляет ее оперативно с помощью вызова функции strlen(). Рекомендуем

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

449

его использовать, если длина требуется нечасто и не хочется добавлять для каж­

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

 

Поскольку динамическая память распределяется для каждого объекта

String

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

Используйте подход, проиллюстрированный диаграммами на рис. 11.4 и 11.5. Он отражает принцип С + -Ь, согласно которому класс — это схема, шаблон экземпляра объекта. Такая схема одинакова для всех объектов String. Каждый объект String имеет два элемента данных, а размер любого объекта String будет одним и тем же. В клиенте выполняется следующий оператор:

String t(20); / / для двух элементов данных распределяется память в стеке

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

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

String *р;

/ /

нет объекта String, создается

 

/ /

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

р = new String ("Привет!");

/ /

в динамической области два

 

/ /

элемента данных, плюс 4 символа

Здесь неименованный объект String, на который ссылается указатель р, получает числовой и символьный указатели в динамически распределяемой области. После создания объекта конструктор выделяет в динамической области место еще для 4 символов и устанавливает указатель str на эту память.

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

Деструктор освобо>вдает память, выделяемую в динамической области. Он вы­ зывается непосредственно перед уничтожением объекта. Когда объект уничтожа­ ется, освобождается для повторного использования память, выделенная для его элементов данных str и 1еп. Если память для объекта выделялась в стеке, как для объектов U и V в функции main() из листинга 11.2, она возвращается в стек, если в динамически распределяемой области, как для неименованного объекта, на ко­ торый ссылается указатель р, она снова возвращается в динамическую область. Во всех случаях память, освобождаемая деструкторами (на нее ссылается указа­ тель str), возвращается в динамически распределяемую область до того, как исчезнут элементы данных 1еп и str. В противном случае оператор delete str; был бы недопустимым.

Функция modifyC) изменяет содержимое динамически распределяемой области памяти. Чтобы содержимое памяти не оказалось запорченным, она использует библиотечную функцию strncpyO. В случае переполнения strncpyO не заверша­ ет строку нулевым символом. Это делается в конце функции. Если строка короче выделенной области памяти, такая операция кажется лишней. Нужно иметь в виду, что функция StrncpyO в любом случае заполняет остаток строки нулями, и запись еще одного нуля не повлияет значительно на скорость выполнения про­ граммы.

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