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

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

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

360

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

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

Cylinder р* = (Cylinder*)malloc(sizeof(Cylincler));

 

/ /

нет вызова конструктора

p->setCylinder(3, 5);

/ /

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

Вызов malloc() — единственный способ в СН- + , позволяющий создать объект без помош>1 вызова конструктора. Создание других объектов (именованных и ди­ намических) сопровождается вызовом конструктора. Итак, мы незаметно мино­ вали точку возврата. Теперь невозможна ситуация, когда вы просто создаете экземпляр объекта и выделяете ему область памяти. Любое создание объекта будет сопровождаться вызовом функции-конструктора. Это опять требует опреде­ ленной смены мышления. Каждый раз, видя создание объекта, нужно напоминать себе, что это означает вызов конструктора. Но какого?

Конструкторы^ используемые по умолчанию

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

class

Cylinder {

/ /

OK,

если нет

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

double radius, height;

/ /

данные защищены от доступа из клиента

public:

 

 

 

 

void

setCylinder(double г,

double

h);

/ /

конструкторы доступны

double getVolumeO;

 

 

 

 

void

scaleCylinder(double

factor);

 

 

 

void

printCylinderO;

 

 

 

 

} ;

 

/ /

конец области действия класса

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

Cylinder с1;

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

Зачем нужно знать об этом? Весь такой конструктор ничего не делает. Но следует иметь в виду, что если класс определяет конструктор явно (конструктор с пара­ метрами), то конструктор по умолчанию не подставляется.

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

Данная версия класса Cylinder не имеет определенных программистом конст­ рукторов. Следовательно, система назначает этому классу конструктор по умолча­ нию (ничего не делающий). При создании переменной с1 был вызван такой конструктор. Откуда это известно? Но ведь какие-то конструкторы должны вызы­ ваться. (Создание объекта без вызова конструктора невозможно.) Так какой же конструктор вызывается? Зависит от числа аргументов. Переменная с1 не преду­ сматривает никаких аргументов. Это говорит о том, что вызывается конструктор без аргументов, т. е. конструктор по умолчанию. Предусматривает ли класс конст­ руктор по умолчанию? Нет. Следовательно, конструктор по умолчанию подставля­ ется системой. Он ничего не делает. Все замечательно.

Давайте рассмотрим версию класса Cylinder, который предусматривает общий определенный программистом конструктор. Это означает, что система не будет использовать конструктор по умолчанию.

Глава 9 • Классы С^-ь как вАиницы моАУ^ьност программы

361

class

Cylinder {

 

 

 

 

double radius,

height;

 

 

 

public:

 

 

/ / этого недостаточно

 

Cylinder(double

r, double

h)

 

{

radius = r; height = h;

}

 

 

.

. . } ;

 

 

 

 

Когда клиент пытается создать объекты Cylinder, возникают проблемы.

Cylinder с1(3.0,5.0);

//OK

 

Cylinder

с2, с[1000];

/ /

1001 синтаксическая

ошибка

Cylinder

*р = new Cylinder;

/ /

одна синтаксическая

ошибка

Здесь создается 1001 экземпляр объекта Cylinder без указания аргументов. Помните, что создание объекта без вызова конструктора не возможно? Поэтому компилятор пытается сгенерировать 1001 вызов конструктора. Какого именно? Так как аргументов нет, то он вызывает конструктор без аргументов, т. е. конст­ руктор по умолчанию Cylinder: :Cylinder(). Но в данной версии класса Cylinder не определен конструктор по умолчанию, а определен общий конструктор. Таким образом система вызывает общий конструктор, а конструктор по умолчанию не использует. Что будет, если клиент вызовет 1001 раз функцию-член класса и со­ здаст 1001 объект Cylinder? Поскольку эту функцию в спецификации класса найти не удается, генерируется сообщение о синтаксической ошибке. Научитесь быстро делать такие логические выводы.

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

class

Cylinder {

 

 

 

 

 

double radius,

height;

 

 

 

public:

 

 

 

 

 

 

Cylinder

()

 

/ / определенный программистом конструктор по умолчанию

{

radius

= 100.0;

height

= 0.0; }

/ /

разумные значения

Cylinder(double

г,

double h)

/ /

общий конструктор

{

radius

= г; height = h;

}

 

 

.

. . } ;

 

 

 

 

 

 

В клиенте:

Cylinder

с1(3.0,5.0);

//OK

Cylinder

с2, с[1000];

/ /

тоже OK

Cylinder

*р = new Cylinder;

/ /

нет синтаксической ошибки

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

Внимание Создание объекта в C++ всегда сопровождается

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

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

щ 362 I

Часть II« Объектно-ориентировонное орогра1\^мировани0 но O^-t

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

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

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

i nt х; Cylinder с1;

/ / неинициализированные переменные

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

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

int

х(20); Cylinder с1(50,70);

/ / объекты создаются и инициализируются

int

у=х; Cylinder с2=с1;

/ /

инициализация с помощью существующих объектов

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

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

В этом примере для инициализации объекта с2 используется один аргумент — объект с1. Он имеет тип Cylinder. Следовательно, вызываемый конструктор име­ ет один параметр типа Cylinder. Вывод ясен? Такая цепочка рассуждений должна иметь место ка>вдь1Й раз, когда вы анализируете операторы создания объекта.

Конструктор с одним параметром того же типа, что и класс, имеет специальное название — конструктор копирования. Он называется так потому, что копирует значения из имеющегося источника в поля только что созданного целевого объекта. Как видно, последняя версия класса Cylinder не содержит конструктора с одним параметром типа Cylinder. Она имеет общий конструктор с двумя параметрами double и конструктор по умолчанию без параметров. Означает ли это, что приве­ денные операторы ошибочны, подобно ситуации, когда вводилась концепция ис­ пользуемого по умолчанию конструктора? Нет, и это еще одно подтверждение, что изучение С+Н нескучное занятие.

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

Глава 9 • Классы C-i-^ кок единицы модульности программы

[ 363 р

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

class Cylinder {

double radius, height; public:

Cylinder (const Cylinder &c)

{radius = c. radius; height = c. height;

cout

« "Конструктор

копирования: " « radius « ", "

}; «

height « endl;

}

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

Cylinder

(Cylinder с)

/ /

некорректный

интерфейс конструктора

{ radius

= с.radius;

height = c.heingt;

 

 

cout

«

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

radius « ",

" « height « endl; }

При вызове такого конструктора выполняется копирование фактических аргу­ ментов — выделяется и инициализируется (значениями полей фактического ар­ гумента) память для переменной Cylinder. Но ведь в C-f-f не бывает создания объекта без вызова конструктора! Выделение и инициализация памяти для значе­ ний полей фактического аргумента означает вызов конструктора копирования для параметров конструктора копирования. Когда вызывается эта вторая версия конструктора копирования, делается копия его фактических аргументов и конст­ руктор вызывается снова. Процесс рекурсивных вызовов продолжается, пока у пользователя не лопнет терпение или у машины не исчерпается память в стеке.

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

О с т о р о ж н о ! Конструктор копирования имеет один параметр типа класса, которому данный конструктор и принадлежит. Этот параметр необходимо передавать по ссылке const, а не по значению. Передача конструктору

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

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

int

X = 20; Cylinder с1(50.70);

/ /

объекты создаются и инициализируются

int

у=х; Cylinder с2(с1);

/ /

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

Но С+Н- хочет одинаково интерпретировать объекты и переменные встроен­ ных типов. Это означает, что синтаксис инициализации в вызове конструктора можно распространить и на встроенные переменные, хотя для переменных данных типов никакие конструкторы вызываться не могут. Такой синтаксис доступен толь­ ко в C+-f, но не в языке С.

int

х(20);

/ /

создается

и инициализируется

объект

int

у(х);

/ /

создается

и инициализируется

переменная у

364

Честь И • ОбъектнО'Ориеитированное програттыроваитв на C++

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

Cylinder

с1(50,70);

/ /

вызывается общий конструктор

Cylinder

с2=с1;

/ /

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

Cylinder

= new Cylinder(50,70);

/ /

вызывается

общий конструктор

Cylinder

*q

= new Cylinder(*p);

/ /

вызывается

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

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

Cylinder

с1();

/ /

синтаксическая ошибка

 

Cylinder

с2;

/ /

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

по умолчанию

Cylinder

= new CylinderO;

/ /

синтаксическая ошибка:

круглые скобки

Cylinder

*q

= new Cylinder;

/ /

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

по умолчанию

Почему же такая несогласованность? Это чтобы легче было написать ком­ пилятор. Взгляните на первую строку последнего примера. Как узнать, что предполагается вызов конструктора, а не прототипа функции с именем с1() и возвращаемым типом Cylinder? Неизвестно. Разработчик компилятора тоже этого не знает. Один из способов избежать подобной неоднозначности состоит

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

вэтом случае синтаксическую ошибку. В Java не преследуется задача обратной

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

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

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

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

Cylinder

с1(50.0);

/ /

вызывается

конструктор

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

Cylinder

с2 = 30.0;

/ /

вызывается

конструктор

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

Вновь, несмотря на разный синтаксис, обаоператора имеют один смысл — вызов конструктора преобразования.

В отличие от конструкторов по умолчанию и конструкторов копирования, кон­ структоры преобразования системой не подставляются. Если в классе определен конструктор преобразования с одним параметром типа double, то оба приведенных выше оператора дадут ошибку. Конструктор преобразования задает, что делать, если в качестве параметра указывается только одно значение, и какие значения использовать для других полей объекта. В следующем примере класс Cylinder определяет четыре конструктора: конструктор по умолчанию, конструктор копиро­ вания, конструктор преобразования и общий конструктор с двумя параметрами.

Глава 9 • Классы C++ кок единицы модульности программы

365

class Cylinder {

 

 

 

 

 

 

 

 

 

double

radius,

height;

 

 

 

 

 

 

public:

 

 

 

 

 

 

 

 

 

 

Cylinder

()

/ /

предусмотренный

программистом конструктор

по умолчанию

{ radius =1 . 0 .

height

= 0.0; }

 

 

 

Cylinder

(const

Cylinder

&c)

 

/ /

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

{ radius = с. radius,

height

= с

height; }

 

 

Cylinder

(double r, double

h)

 

 

 

 

{ radius = r,

height

= h;

}

 

/ /

общий конструктор

Cylinder

(double

 

r)

 

 

 

 

 

 

 

{ radius = r,

height

= 0.0;

}

/ /

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

. . . . } ;

 

 

 

 

 

 

 

 

 

 

Конструктор преобразования — первый удар по системе строгого контроля типов в C++. Как уже упоминалось, все современные языки поддерживают стро­ гий контроль типов. Если в каком-то контексте ожидается значение одного типа, то подстановка значения другого типа даст синтаксическую ошибку. Рассмотрим, к примеру, такой оператор:

Cylinder с2 = 30.0; / / вызывается конструктор преобразования

Если Cylinder — это просто структура С, то такой оператор синтаксически ошибочен. Компилятор сообилает об этом и говорит, что у вас есть шанс подумать и решить, что вы хотите сделать. Если Cylinder — класс C++ без конструктора преобразования, тоже возникает синтаксическая ошибка. У вас также не будет возможности выполнить программу и проанализировать ее результаты. Когда Cylinder — класс C++ без конструктора преобразования, то синтаксической ошибки не будет. Если это сделано намеренно, то все замечательно. Если же нет, то компилятор не заидитит от такой ошибки. Система строгого контроля типов здесь дает сбой.

В качестве следуюш^его примера рассмотрим функцию CopyDataO из этой гла­ вы (предполагая, что элементы данных radius и height объявлены как public).

void CopyData(Cylinder *to,

const Cylinder &from)

 

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

{ to->radius=from.radius;

to->height=from.height; } / / запись со стрелкой

Для простой структуры с или для класса C++ без конструктора преобразова­ ния этот вызов функции в клиенте даст синтаксическую ошибку:

CopyData(&c2,70.0); / / здесь пропущено FROM Cylinder

Если доступен конструктор преобразования, компилятор будет генерировать программный код, создаюш,ий временный неименованный объект Cylinder, вызываюш,ий для этого временного объекта конструктор преобразования (с фактиче­ ским аргументом 70,0) и передаюш,ий временный неименованный объект функции CopyDataO как второй аргумент.

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

Cylinder с2 = 30;

/ /

30 преобразуется

в double

CopyData(&c2.70);

/ /

70 преобразуется

в double

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

366 Часть il • Обьектно-ориентирОБОнное программирование на C^-f

Деструкторы

Объект C++ уничтожается в конце выполнения программы (для объектов static или extern), при достижении закрывающей фигурной скобки, завершающей об­ ласть действия (для автоматических объектов), при выполнении операции delete (для динамических объектов с памятью, выделенной через new) или при вызове библиотечной функции fгее() (для объектов с памятью, выделенной через malloc()).

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

Деструктор, определяемый программистом, аналогичен конструктору. Это функция-член класса. Синтаксис деструктора еще более строгий, чем синтаксис конструктора. Указывать возвращаемый функцией тип в ее интерфейсе недопус­ тимо, а в теле функции не может присутствовать оператор return. Деструктор имеет то же имя, что и имя класса, но ему предшествует тильда (~), например ''Cylincler(). Деструкторы в отличие от конструкторов не могут иметь параметров.

Конструкторы и деструкторы — хороши для размещения операторов отладки. class Cylinder {

double

radius, height;

 

 

 

 

public:

 

 

 

 

 

 

"Cylinder

()

 

/ /

определяемый программистом деструктор:

 

 

 

 

/ /

нет возвращаемого типа

{ cout

«

"Cylinder (" «

radius

«

", " «

height

 

«

") уничтожен" «

endl;

}

/ /

нет возвращаемого значения

Когда деструктор реализуется вне области действия класса, используется опе­ рация области действия. Обратите внимание, что тильда — это часть имени функ­ ции, а не часть операции области действия.

Cylinder::"Cylinder

( )

/ /

деструктор класса: нет возвращаемого типа

{ cout «

"Cylinder

(" «

radius

« ", " «

height

«

") уничтожен" «

endl;

}

/ / нет возвращаемого типа

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

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

Рассмотрим пример класса, где может быть полезен деструктор. Класс Name содержит строку символов — фамилию человека. Конструктор инициализирует символьный массив. (Это конструктор преобразования, так как он имеет один параметр с типом, отличным от Name.) Для простоты данные объявлены как public, и предусматривается только один метод show_name(), отображающий на экране содержимое объекта.

struct Name {

// фиксированный размер объекта, открытые данные

char contents[30];

Name (char* name);

// или Name(char name[]);

void show_name();

// деструктор еще не нужен

} ;

Глава 9 « Классы C-f-f кок единицы моАудьности программы

367

Name::Name(char* name)

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

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

{ strcpy(contents, name); }

// стандартное действие: копирование

void Name::show_name()

// данных аргумента

 

 

 

 

 

{ cout « contents « "\n"; }

 

 

 

 

Клиент может определять объекты данного типа и отображать их содержимое

на экране.

 

 

 

 

Name п1("Джонс");

/ /

вызывается

конструктор

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

Name *р = new NameC'CMHT");

/ /

вызывается

конструктор

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

n1.show_name(); p->show_name();

 

 

 

 

delete р;

/ /

удаляется неименованный

объект

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

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

struct Name {

 

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

char *contents;

 

Name (char* name);

 

// память: все равно public

} ;

// или Name(char name []);

void show_name();

// теперь деструктор нужен

Name::Name(char* name)

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

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

{ int len = strlen(name);

// аргумент - число символов

contents = new char[len+1];

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

if(contents ==NULL)

// 'new' выполнена неуспешно

{ cout « "Нет памяти\п"; exit(1); }

// отказ

strcpy(contents, name); }

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

void Name::show_name()

 

 

{ cout « contents «

"\n"; }

 

 

Чтобы обсудить, что происходит при использовании новой версии класса Name, поместим программный код клиента в глобальную функцию.

void ClientO

 

 

 

 

{ Name n1("Джонс");

/ /

вызывается

конструктор

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

Name *р = new Name("CMHT");

/ /

вызывается

конструктор

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

n1.show_name(); p->show_name();

 

 

 

 

delete р; }

/ /

удаляется

неименованный объект

Когда в функции ClientO выполняется оператор delete р;, освобождается память, на которую ссылается указатель р. Эта память содержит только указатель contents. Память по указателю contents не удаляется и становится недоступной. Это — утечка памяти. Обратите внимание, что оператор delete р; не удаляет указатель р. Он удаляет то, на что этот указатель указывает. Указатель р удаля­ ется в соответствии с правилами области действия (когда завершается та область действия, где он определен). Это происходит, когда завершается выполнение функции ClientO (достигается закрываюш.ая фигурная скобка).

368

Часть II • Объектно

Создан объект: Джонс Создан объект: Смит Джонс Смит

Уничтожен объект: Смит Уничтожен объект: Джонс

Рис. 9.3.

Вывод программы из листинга 9.3

Аналогично когда завершается функция ClientO, локальный объект п1 унич­ тожается и содержимое указателя contents возвращается в стек. Память, на которую ссылается указатель contents, не возвращается системе. Это — утечка памяти.

Использование конструкторов жизненно важно для классов, где управление ресурсами происходит динамически. Для поддержания целостности программы С+ + нужны деструкторы. Деструктор вызывается при каждом уничтожении объекта по правилам области действия или по операции delete (но не с по1у10щью вызова функции freeO). Следовательно, деструктор — подходящее место для освобождения памяти (и других ресурсов), выделенной во время сущест­ вования объекта (в основном это происходит в конструкторах, но иногда динамическое распределение памяти выполняется и в других функциях-

членах).

Деструктор для класса Name очень прост, В листинге 9.3 показан класс Name с деструктором, возвращающим динамическую память. Рис. 9.3 де­ монстрирует результат выполнения программы с выводом через отладоч­ ные операторы в конструкторе и деструкторе.

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

#inclucJe <iostream> using namespace std;

struct Name {

 

 

// указатель public надинамическую память

 

char ^contents;

 

 

Name (char* name);

 

// или Name (char name[]);

 

void show_name();

 

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

 

"NameO; }

;

 

Name::Name(char* name)

 

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

{ int len = strlen(name);

 

// число символов

 

contents = new char[len+1];

 

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

 

if (contents == NULL)

 

// выполнение

'new' неудачное

 

{ cout «

"Нет памяти\п"; exit(1); }

// отказ

 

 

strcpy(contents, name);

 

// стандартные действия

 

cout «

"создан объект: " «

contents « endl;

// отладка

void Name::show_name()

 

 

 

{ cout «

contents « "\n"; }

 

 

 

Name::-Name()

 

« contents «

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

// отладка

{

cout «

"уничтожен объект:

endl;

 

delete contents; }

 

// освобождает динамическую память, а не

 

 

 

 

 

// удаляет указатель contents

void ClientO

 

 

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

{

Name n1("Джонс");

 

 

Name *p = new Name("CMHT");

 

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

 

n1.show_name(); p->show_name();

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

 

delete p;

 

 

 

}

 

 

 

 

 

int main ()

 

 

 

 

{

Client 0 ;

 

 

 

 

 

return 0;

 

 

 

 

}

 

Глава 9 • Классы C+-f как единицы 1\^одудьности програ^1^ы

г

369

 

 

 

 

Когда функция ClientO

выполняет операцию delete р;, вызывается деструктор

 

 

класса Name с оператором delete contents. При уничтожении функцией ClientO

 

 

объекта п1 вызывается деструктор, который также выполняет оператор

delete

 

 

contents. При этом устраняется утечка памяти.

 

 

 

 

Рис. 9.4 демонстрирует использование памяти функцией ClientO. Рис. 9.4А

 

 

показывает состояние памяти после создания указателей на именованный объ­

 

 

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

 

 

ется пространство стека (по правилам области действия), затем выделяется дина­

 

 

мическая память для "Джонс" (конструктором), память для неименованного

 

 

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

 

 

для "Смит".

 

 

 

 

 

 

 

На рис. 9.4В и С представлена схема уничтожения объектов. Рис. 9.4В пока­

 

 

зывает, что сначала возвращается память, выделенная для "Смит" (с помощью

 

 

деструктора), затем — память, выделенная для неименованного объекта

(через

 

 

операцию delete). Указатель р сохраняется, так как операция delete не удаляет

 

 

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

 

 

лается указатель.

 

 

 

 

 

 

Рис. 9.4С демонстрирует правила освобождения памяти в стеке для указателя р

 

 

и именованного объекта п1. "Кончина" указателя не влечет за собой никаких

 

 

событий. Уничтожение объекта п1 приводит к вызову конструктора Name, осво­

 

 

бождению динамически распределяемой памяти, выделенной для строки "Джонс"

 

 

конструктором, и освобождению пространства стека, занимаемого п1.

 

 

 

 

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

 

 

периментируйте со своим собственным программным кодом. Некоторые програм­

 

 

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

 

 

память, выделяемую для строк "Джонс" и "Смит") проще анализировать, если

 

 

рассматривать ее как часть экземпляра объекта. Динамическая память — допол­

 

 

нительный ресурс, выделяемый каждому экземпляру объекта и позднее возвраща­

 

 

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

 

 

размер, соответствующий размеру его элементов данных, а не аргументам конст­

 

 

руктора. Но это дело вкуса.

 

 

 

 

 

Заметим, что если функции ClientO не удается выполнить вызов delete р;,

 

 

то объект,

на который ссылается указатель р (его указатель contents и память

 

 

по указателю

contents),

никогда

не уничтожается и ресурсы не возвращаются

 

 

Стек

 

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

 

 

А)

 

 

область памяти

 

 

 

 

 

 

 

 

 

 

 

 

п1

 

 

 

1 L

Джонс

 

 

 

 

 

 

 

Смит

 

 

 

 

 

 

 

 

Name п1 ("Джонс");

 

 

 

 

 

 

 

 

Name *р = new Name("CMHT");

 

 

В)

п1

 

ч2

IJx

 

 

 

 

 

 

 

 

 

 

 

 

 

 

)<;

ciuiT

Джонс

 

 

 

 

 

 

 

 

delete р;

 

 

С)

ч Ш

^ Р

 

Tlf.

.2 ,

 

 

 

i2S

К

X

 

fl^cihc

 

 

 

 

 

 

 

 

 

 

JT^

} // Закрывающая фигурная скобка

Рис. 9 . 4 . Схема управления памятью для функции-клиент^а Client() из листинга 9.3

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