Глава 18
Конструкторы и деструкторы — это специальные функции, предназначенные для инициализации объектов и освобождения выделяемой для них памяти. И эта глава
посвящена именно им,
Вверх
Каждый из нас любит вкусно поесть, но вот процесс приготовления пищи и последующее мытье посуды способны испортить общий "праздник жизни". В программировании та же картина: создавать хорошие программы— одно удовольствие, если не учитывать всяческую подготовительную работу и последующую уборку программного мусора.
Однако не стоит потакать своей лени, так как подготовительный и завершающий этапы имеют при создании программы очень важное значение. Если, например, вы забудете инициализировать переменную (т.е. не присвоите ей начальное значение), это может привести к таким последствиям, как отображение на экране не того цвета, не тех данных или зависание программы. Аналогично, если вы забудете написать коды, освобождающие выделяемую память, это может привести к тому, что программа будет работать все медленнее и медленнее, пока вовсе не зависнет.
К счастью (а вы ведь знали, что не может все быть настолько ужасно), в С++ есть две встроенные возможности — конструкторы и деструкторы, заботящиеся о том. чтобы переменные инициализировались, а ненужная память вовремя освобождалась. Конструктор — это процедура, которая автоматически вызывается в момент создания объекта (он же — экземпляр класса). Вы можете помещать инициализирующую процедуру внутри конструктора, чтобы иметь гарантию, что объект будет настроен должным образом, когда вы приступите к использованию. Можно даже создать несколько конструкторов, чтобы иметь возможность инициализировать объект разными способами.
Когда объект становится более не нужным (это происходит, если использующая его
Вверх
Конструктор — это функция, вызываемая каждый раз в момент создания
нового объекта.
Если объект содержит члены данных, которые должны быть инициализированы.
Совет:
Конструктор имеет такое же имя, как и соответствующий класс. Так, конструктор класса DisplayObject будет называться DisplayObject::DisplayObject. Конструктор класса Dog будет называться Dog::Doc.
Конструктор никогда не возвращает значения в качестве результата.
Далее нужно написать коды для объявленного конструктора. В программе Draw4 класс DisplayObject имеет функцию-член Initialize, которая присваивает указателю адрес списка ArrayList. Этот же код можно набрать внутри конструктора DisplayObject:
Каждый раз при создании экземпляра класса DisplayObject этот конструктор будет
Вверх
Иногда возникает необходимость по-разному создавать объекты одного и того же класса. Возможно, например, что при создании объекта LineObject вам потребуется либо задать начальные координаты для первой фигуры, либо установить ограничение на количество отображаемых фигур. Для этого можно создать сразу несколько конструкторов. Один будет принимать значения параметров и создавать обычный объект LineObject. Другой будет также принимать значения параметров и использовать их для инициализации или изменения объекта.
Можете создать столько конструкторов, сколько будет необходимо. Каждый конструктор должен иметь свойственный только ему набор параметров. Например, один может требовать число типа integer, второй — два числа типа double и т.д.
Класс с несколькими конструкторами прост в применении. В действительности его можно многократно использовать в кодах программы. Каждый раз, когда нужно создать новый объект и передать ему значения параметров, вы вызываете параметризованный конструктор. Вот, например, код, взятый из программы Draw4, в котором используется один из нескольких конструкторов класса Реп:
При этом для создаваемого объекта Реn уже будет определен цвет. Однако одновременно можно задать и толщину отображаемой линии, набрав такой код:
В данном случае также создается объект Реn, но при этом уже используется другой конструктор.
Создается класс с несколькими конструкторами также довольно просто. Все, что вам
//Определение конструктора
//Конструктор, вызываемый, если указывается цвет
Совет:
Помните, что каждый конструктор должен иметь уникальный набор параметров.
Совет:
Вверх
Деструктор — это функция-член, автоматически вызываемая в момент ликвидации объекта.Объекты могут ликвидироваться по разным причинам. Например объект, создаваемый и используемый какой-нибудь функцией, уничтожается тогда, когда эта функция заканчивает работу. Или же объект, созданный командой new, может быть впоследствии удален командой delete. Или, допустим, программа завершает свою работу и все созданные ею объекты подлежат удалению.
Компилятор вызывает деструктор автоматически. Вам вовсе не обязательно беспокоиться о том, чтобы он был вызван, а также вы никак не сможете передать ему значения каких-то параметров. Конструктор имеет то же имя, что и класс, перед которым стоит символ тильды (~). Класс может иметь только один деструктор, и он должен быть открытым (public).
Вот как выглядят коды деструктора:
Вверх
Если вы создаете управляемую программу (используя возможности .NET), среда
Однако при написании неуправляемых программ возможность сборки мусора и
Если вам сейчас кажется, что на самом деле можно обойтись и без деструкторов, давайте и вернемся к программе Draw5. Классы LineObject.Container.ArrayList и DispLayObject включают в себя функцию Delete - для очистки памяти. Программа Draw5 проходит длинный путь, чтобы в нужной последовательности вызвать процедуры Delete для всех объектов, которые были созданы в процессе выполнения этой программы.
Использование деструкторов значительно упрощает этот процесс. Вы можете перепоручить им всю выполняемую функциями Delete работу, а сами функции Delete отбросить за ненадобностью. Более того, при этом упрощается и процедура очистки памяти. В программе Draw5 функция LineObject.Container.ArrayList.
Как вы помните, эта функция вызывается тогда, когда программа завершает работу. Называется она вначале для первого элемента связанного списка, а затем поочередно обращается к каждому последующему элементу. (Ну хорошо, вы, скорее всего, об этом не помните! на самом деле, когда вы просматривали эти коды в главе 17. то наверняка думали о чем-то совсем другом или просто считали овечек.)
С деструкторами все становится намного проще. Когда вы удаляете обьект, соответствующий
Этот код очень прост. Первый объект списка ArrayList освобождает занимаемую память
и затем удаляет следующий объект списка, который также является объектом класса LineObjectConiainer. Следовательно, такой же деструктор вызывается и для этого объекта. Что он делает? Освобождает занимаемую этим объектом память и удаляет следующий объект. И так далее, пока не будет удален последний объект списка.
Каким образом будет вызван деструктор для первого объекта в списке ArrayList? Сделает это деструктор класса ArrayLisт.:
Ну и теперь осталось выяснить, каким образом будет вызван деструктор для объекта
Поскольку деструкторы работают именно так. как описано выше, вы можете писать коды для очистки памяти небольшими порциями отдельно для каждого создаваемого класса. Вам не нужно ждать, пока программа завершит всю работу, и затем набирать коды для освобождения всей выделенной памяти, пытаясь вспомнить каждый созданный объект.
Смысл использования деструкторов можно пояснить примером из обычной жизни: если
Лучший способ реализации такого деструктора — включение в него команды, удаляющей следующий элемент связанною списка. Таким образом, стоит вам только удапить первый элемент списка, все остальные элементы будет удалены автоматически. Полная аналогия с эффектом домино.
Не забывайте также, что при написании управляемых программ вам обычно не придется слишком беспокоиться о таких вещах, поскольку возможность сборки мусора сделает всю грязную работу автоматически.
Вверх
По мере приобретения опыта создания объектно-ориентированных программ вы неизбежно придете к выводу, что очень удобно создавать классы, содержащие в себе другие классы. Например, внутри класса Семья вполне логичным было бы использование класса Жена (или Теща).
При создании экземпляра класса первым делом выделяется память для всех его членов данных. Если внутри класса есть члены данных, которые сами являются классами (например, такой член данных, как Теща), для них вызываются конструкторы.
Таким образом, если класс содержит в себе другие классы, первыми вызываются конструкторы для этих классов, поскольку они должны быть созданы до того, как будет создан основной класс.
Когда экземпляр класса уничтожается, вызывается деструктор. Затем вызываются деструкторы для всех членов данных, которые сами являются классами. Например, когда удаляется объект класса Семья, вызывается соответствующий ему деструктор, а после него вызывается деструктор, уничтожающий класс Теща.
Возможно, вам будет легче это понять, если вы просмотрите код, приведенный ниже. В этой программе создается класс fоо, который содержит в себе другой класс — bar. Если вы запустите программу, то увидите, что вначале вызывается конструктор класса bar, а затем конструктор класса foo. Когда программа заканчивает работу, вначале вызывается деструктор класса foo и только потом деструктор класса bar:
//Простой класс, состоящий из конструктора и деструктора
//Пусть все знают, что создается объект класса bar
//Пусть все знают, что объект класса bar уничтожается
//foo — это класс, внутри которого содержится другой класс,
//Пусть весь мир знает, что рождается объект класса foo
//Пусть весь мир знает, что объект класса foo уничтожается
//Это функция main, которая всего лишь создает объект класса
Вверх
Вы уже заметили, что объектно-ориентированные программы могут состоять из множества классов. Каждый класс должен быть объявлен для того, чтобы сообщить компилятору, из чего он будет состоять, и затем должны быть определены все его функции-члены. Коды, которыми класс объявляется, обычно лаконичны и не занимают много места, но вот определение функций-членов может растянуться не на одну страницу. Поскольку вам придется читать коды функций-членов, чтобы точно понять, что они делают, изучение работы класса может потребовать переходов от одного фрагмента кодов к другому, набранному совершенно в другой части файла.
Приведем несколько советов, которые смогут облегчить процесс изучения работы класса.
Назад |
Начало урока |
Вверх |
Вперед
Работа до и после
функция заканчивает работу или вы набираете для него команду delete, автоматически вызывается функция, называемая деструктором. Если по какой-то причине вас не устраивает тот способ, которым среда .NET автоматически освобождает выделяемую намять, можете подкорректировать его, воспользовавшись деструктором.
Подготовительный этап
Добавьте в конструктор соответствующие коды.
которых можно было бы обойтись. Всегда желательно, чтобы коды конструктора
были краткими, простыми и понятными.
Чтобы создать конструктор, объявите его вначале внутри класса, Ниже, например, показан код объявления класса DisplayObject, для которого будет создан конструктор:
__gc class DisplayObject
{
void Draw(Graphics *poG);
DisplayObject();
DisplayObject::DisplayObject()
{
вызываться автоматически. Теперь необходимость в функции Initialize отпадает, и более не нужно беспокоиться о том, чтобы она вызывалась для каждого нового объекта. Этот простой прием позволит вам избавиться от лишней головной боли.
быть закрытым (private), открытым (public) или защищенным (protected).
Много конструкторов — много возможностей
Значения параметров передаются в момент создания объекта. В зависимости от передаваемого набора параметров, вызывается тот или иной конструктор. Например, если при создании объекта вы передаете одно число типа integer, вызывается тот конструктор, которому требуется значение одного параметра типа integer. Если передаются два числа типа double, вызывается конструктор, которому нужны два числа типа double. Если компилятор не может найти соответствие между передаваемым набором параметров и наборами параметров существующих конструкторов, он выдает сообщение о синтаксической ошибке.
Реn *роРеn = new Pen(Color::Red);
Pen *роРеn = new Pen
(Color::Red, 5);
нужно сделать, — это объявить эти несколько конструкторов и затем не забыть написать для каждого из них коды. Ниже, например, приведена одна из версий класса DisplayObject, включающего в себя два конструктора:
gc class DisplayObject
{
void Draw(Graphics *poG);
DisplayObject() ;
//В следующей строке объявляется еще один конструктор
DisplayObject(Color *poInitalColor);
Color *m_poInitalColor;
DisplayObject::DisplayObject
{
DisplayObject::DisplayObject(Color *poInitalColor)
{
m poInitalColor = poInitalColor;
Например, хотя приведенные ниже конструкторы выглядят по-разному и используют
аргументы с разными именами, количество аргументов и их тип совпадают.
Это будет воспринято компилятором как синтаксическая ошибка.
Window::Window(int Left, int Right);
Window::Window(int Width, int Color);
Открытые и закрытые конструкторы
Заметание следов
//Класс для создания связанного списка
class LineObjectContainer
{
LineObjectContainer *poNext;
-LineObjectContainer
();
//Деструктор: освобождение памяти, занимаемой списком
LineObjectContainer::~LineObjectContainer()
{
cout << "Удаление связанного списка\n";
//Удаление линии
delete poLine;
//Удаление следующего элемента списка
if (poNext)
delete poNext;
Когда объект больше не нужен
.NET автоматически освобождает выделяемую память или по крайней мере делает
это для тех объектов, для которых активизирована возможность сборки мусора.
Поэтому в .NET не обязательно всегда собственноручно создавать деструкторы.
поэтому создание деструкторов становится жизненно необходимой задачей.
Поскольку у каждого класса есть свой деструктор, каждый из них в состоянии самостоятельно освободить память, выделяемую для его объектов. Поэтому вам не нужно в теле программы набирать коды для освобождения сразу всей выделенной памяти. Можно делать это для каждого класса отдельно и тогда, когда это нужно.
например, выглядит довольно внушительно:
void LineObjectCorntainer::Delete()
{
poCur = this;
(poCur)
poTemp = poCur;
//Освобождение памяти, выделенной для объекта LineObject
delete poCur->poLine;
poCur = poCur->poNext;
//Освобождение выделенной памяти для объекта LineObjectContainer
delete poTemp;
ему деструктор вызывается автоматически, а функцию LineObjectContainer()::Delete
можно преобразовать к более совершенному виду:
//Деструктор: освобождение памяти, выделенной для списка
LineObjectContainer::LineObjectContainer()
{
cout << "Удаление связанного списка\п";
//Удаление линии
delete poLine;
//Удаление следующего элемента списка
if (poNext)
delete poNext;
ArrayList ::~ArrayList()
А каким образом будет вызван этот деструктор? Список ArrayList используется классом DisplayObject. Поэтому, когда удаляется объект класса Display Object, соответствующий ему деструктор удаляет объект класса ArrayList:
{
//Все, что нужно сделать, — удалить первый элемент списка
Delete m__poFirst ;
DisplayObject::~DisplayObject()
{
DisplayObject. Это сделает строка функции main, набранная в самом конце программы:
delete poDisplay;
вы чем-то воспользовались и уже забыли об этом, например проехали в трамвае, прочитали книгу или выпили банку пива, то вам не нужно таскать за собой прокомпостированные билеты, старые книги или какие-то жестянки.
Не забывайте также освобождать динамически выделяемую память
Если вы создаете объект или переменную динамически, т.е. используете для этого ключевое слово new, вы также должны удалить их, когда потребность в них отпадет. Предположим, например, что вы создаете класс для хранения информации, представляющей фотоснимки, и при выделении памяти для объектов этого класса используете команду new. Чтобы освободить эту память, нужно будет применить команду delete . В противном случае ее нельзя будет использовать для хранения другой информации. (Если вы просто удалите указатель на этот фрагмент памяти, он останется занятым старыми данными, создавая таким образом эффект "утечки памяти".)
Или предположим, что у вас есть класс, включающий в себя связанный список, состоящий из набора воспроизводимых музыкальных записей. Удаляя объект этого класса вы должны также освободить память, которая выделяется для хранения этого списка. Иными словами, нужно позаботиться о том. чтобы был вызван деструктор этого связанного списка.
Вложенные классы
//ConstruetorDestructor
//Программа демонстрирует порядок, в котором вызываются
//конструкторы и деструкторы
#include "stdafx.h"
#using <mscorlib.dll>
using namespace System;
class bar
{
bar();
-bar();
bar::bar()
{
-bar::bar()
{
//в данном случае — bar. При создании и удалении объекта
//класса foo также вызываются соответствующие ему конструктор
//и деструктор
class foo
{
foo();
~foo о;
foo::foo()
{
foo::foo()
{
//foo, которому присваивается имя oTemp. При этом автоматически
//вызываются конструкторы. Когда программа завершает свою
//работу, объект oTemp автоматически уничтожается и вы можете
//видеть, в каком порядке вызываются деструкторы
#ifdef _UNICODE
int main(void)
#else
int main(void)
#endif
{
return 0;
Чтение кодов объектно-ориентированных программ
информацией.
вы могли видеть, как некоторые конструкторы используются функцией main.
данных на потом.
заголовочному файлу, чтобы найти коды объявления классов. Вначале посмотрите, подключает ли исходный файл к себе какие-либо заголовочные файлы. (Эту операцию выполняют строки, которые начинаются с ключевого слова include.)
Обычно класс самого верхнего уровня объявляется последним, поэтому лучше начинать читать коды с конца. Начните с функции main. Далее поднимайтесь вверх, чтобы найти объявление класса. (Пропустите коды, которыми определяются функции-члены этого класса.) Просмотрите это объявление. Затем поднимайтесь вверх, чтобы найти объявление следующего класса. Вообще, если у вас возникают с чем-то трудности, попробуйте сделать это задом наперед. Возможно, это не решит проблему, но по крайней мере вы развеселите себя и своих коллег.
Содержание