Назад | Начало урока | Вперед
Содержание

Глава 18

Конструкторы и деструкторы


В этой главе...

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

Вверх

Работа до и после

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

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

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

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

Вверх

Подготовительный этап

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

Если объект содержит члены данных, которые должны быть инициализированы.
Добавьте в конструктор соответствующие коды.

Совет:

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

Конструктор имеет такое же имя, как и соответствующий класс. Так, конструктор класса DisplayObject будет называться DisplayObject::DisplayObject. Конструктор класса Dog будет называться Dog::Doc.

Конструктор никогда не возвращает значения в качестве результата.
Чтобы создать конструктор, объявите его вначале внутри класса, Ниже, например, показан код объявления класса DisplayObject, для которого будет создан конструктор:

__gc class DisplayObject
{

public:
void Add ();
void Draw(Graphics *poG);
DisplayObject();
private:
ArrayList *m__paLines;

};

Далее нужно написать коды для объявленного конструктора. В программе Draw4 класс DisplayObject имеет функцию-член Initialize, которая присваивает указателю адрес списка ArrayList. Этот же код можно набрать внутри конструктора DisplayObject:

DisplayObject::DisplayObject()
{

m__paLines = new ArrayList ();
}

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

Конструктор объявляется внутри класса как одна из его функций-членов. Он может
быть закрытым (private), открытым (public) или защищенным (protected).

Вверх

Много конструкторов — много возможностей

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

Можете создать столько конструкторов, сколько будет необходимо. Каждый конструктор должен иметь свойственный только ему набор параметров. Например, один может требовать число типа integer, второй — два числа типа double и т.д.
Значения параметров передаются в момент создания объекта. В зависимости от передаваемого набора параметров, вызывается тот или иной конструктор. Например, если при создании объекта вы передаете одно число типа integer, вызывается тот конструктор, которому требуется значение одного параметра типа integer. Если передаются два числа типа double, вызывается конструктор, которому нужны два числа типа double. Если компилятор не может найти соответствие между передаваемым набором параметров и наборами параметров существующих конструкторов, он выдает сообщение о синтаксической ошибке.

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

Реn *роРеn = new Pen(Color::Red);

При этом для создаваемого объекта Реn уже будет определен цвет. Однако одновременно можно задать и толщину отображаемой линии, набрав такой код:

Pen *роРеn = new Pen (Color::Red, 5);

В данном случае также создается объект Реn, но при этом уже используется другой конструктор.

Создается класс с несколькими конструкторами также довольно просто. Все, что вам
нужно сделать, — это объявить эти несколько конструкторов и затем не забыть написать для каждого из них коды. Ниже, например, приведена одна из версий класса DisplayObject, включающего в себя два конструктора:

gc class DisplayObject
{

public:
void Add();
void Draw(Graphics *poG);
DisplayObject() ;
//В следующей строке объявляется еще один конструктор
DisplayObject(Color *poInitalColor);
private:
ArrayList *m_paLines;
Color *m_poInitalColor;

};

//Определение конструктора
DisplayObject::DisplayObject
{

m_paLines = new ArrayList
};

//Конструктор, вызываемый, если указывается цвет
DisplayObject::DisplayObject(Color *poInitalColor)
{

m_paLines = new ArrayList ();
m poInitalColor = poInitalColor;
};

Совет:

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

Window::Window(int Left, int Right);
Window::Window(int Width, int Color);


Открытые и закрытые конструкторы

Совет:

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

Вверх

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

Деструктор — это функция-член, автоматически вызываемая в момент ликвидации объекта.Объекты могут ликвидироваться по разным причинам. Например объект, создаваемый и используемый какой-нибудь функцией, уничтожается тогда, когда эта функция заканчивает работу. Или же объект, созданный командой new, может быть впоследствии удален командой delete. Или, допустим, программа завершает свою работу и все созданные ею объекты подлежат удалению.

Компилятор вызывает деструктор автоматически. Вам вовсе не обязательно беспокоиться о том, чтобы он был вызван, а также вы никак не сможете передать ему значения каких-то параметров. Конструктор имеет то же имя, что и класс, перед которым стоит символ тильды (~). Класс может иметь только один деструктор, и он должен быть открытым (public).

Вот как выглядят коды деструктора:

//Класс для создания связанного списка
class LineObjectContainer
{

public:
LineObject *poLine;
LineObjectContainer *poNext;
-LineObjectContainer (); //Деструктор: освобождение памяти, занимаемой списком
LineObjectContainer::~LineObjectContainer()
{
//Для наглядности отображается строка на экране
cout << "Удаление связанного списка\n";
//Удаление линии
delete poLine;
//Удаление следующего элемента списка
if (poNext)
delete poNext;
}
};

Вверх

Когда объект больше не нужен

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

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

Если вам сейчас кажется, что на самом деле можно обойтись и без деструкторов, давайте и вернемся к программе Draw5. Классы LineObject.Container.ArrayList и DispLayObject включают в себя функцию Delete - для очистки памяти. Программа Draw5 проходит длинный путь, чтобы в нужной последовательности вызвать процедуры Delete для всех объектов, которые были созданы в процессе выполнения этой программы.

Использование деструкторов значительно упрощает этот процесс. Вы можете перепоручить им всю выполняемую функциями Delete работу, а сами функции Delete отбросить за ненадобностью. Более того, при этом упрощается и процедура очистки памяти. В программе Draw5 функция LineObject.Container.ArrayList.
например, выглядит довольно внушительно:

void LineObjectCorntainer::Delete()
{

LineObjectContainer *poCur , poTemp;
poCur = this;
(poCur)
poTemp = poCur;
//Освобождение памяти, выделенной для объекта LineObject
delete poCur->poLine;
poCur = poCur->poNext;
//Освобождение выделенной памяти для объекта LineObjectContainer
delete poTemp;
};

Как вы помните, эта функция вызывается тогда, когда программа завершает работу. Называется она вначале для первого элемента связанного списка, а затем поочередно обращается к каждому последующему элементу. (Ну хорошо, вы, скорее всего, об этом не помните! на самом деле, когда вы просматривали эти коды в главе 17. то наверняка думали о чем-то совсем другом или просто считали овечек.)

С деструкторами все становится намного проще. Когда вы удаляете обьект, соответствующий
ему деструктор вызывается автоматически, а функцию LineObjectContainer()::Delete можно преобразовать к более совершенному виду:

//Деструктор: освобождение памяти, выделенной для списка
LineObjectContainer::LineObjectContainer()
{

//Для наглядности отображается строка на экране
cout << "Удаление связанного списка\п";
//Удаление линии
delete poLine;
//Удаление следующего элемента списка
if (poNext)
delete poNext;
}

Этот код очень прост. Первый объект списка ArrayList освобождает занимаемую память и затем удаляет следующий объект списка, который также является объектом класса LineObjectConiainer. Следовательно, такой же деструктор вызывается и для этого объекта. Что он делает? Освобождает занимаемую этим объектом память и удаляет следующий объект. И так далее, пока не будет удален последний объект списка.

Каким образом будет вызван деструктор для первого объекта в списке ArrayList? Сделает это деструктор класса ArrayLisт.:

ArrayList ::~ArrayList()
{

//Удаление связанного списка
//Все, что нужно сделать, — удалить первый элемент списка
Delete m__poFirst ;
}

А каким образом будет вызван этот деструктор? Список ArrayList используется классом DisplayObject. Поэтому, когда удаляется объект класса Display Object, соответствующий ему деструктор удаляет объект класса ArrayList:

DisplayObject::~DisplayObject()
{

delete m_paLines;
}

Ну и теперь осталось выяснить, каким образом будет вызван деструктор для объекта
DisplayObject. Это сделает строка функции main, набранная в самом конце программы:

delete poDisplay;

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

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

Не забывайте также освобождать динамически выделяемую память
Если вы создаете объект или переменную динамически, т.е. используете для этого ключевое слово new, вы также должны удалить их, когда потребность в них отпадет. Предположим, например, что вы создаете класс для хранения информации, представляющей фотоснимки, и при выделении памяти для объектов этого класса используете команду new. Чтобы освободить эту память, нужно будет применить команду delete . В противном случае ее нельзя будет использовать для хранения другой информации. (Если вы просто удалите указатель на этот фрагмент памяти, он останется занятым старыми данными, создавая таким образом эффект "утечки памяти".)
Или предположим, что у вас есть класс, включающий в себя связанный список, состоящий из набора воспроизводимых музыкальных записей. Удаляя объект этого класса вы должны также освободить память, которая выделяется для хранения этого списка. Иными словами, нужно позаботиться о том. чтобы был вызван деструктор этого связанного списка.

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

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

Вверх

Вложенные классы

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

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

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

Когда экземпляр класса уничтожается, вызывается деструктор. Затем вызываются деструкторы для всех членов данных, которые сами являются классами. Например, когда удаляется объект класса Семья, вызывается соответствующий ему деструктор, а после него вызывается деструктор, уничтожающий класс Теща.

Возможно, вам будет легче это понять, если вы просмотрите код, приведенный ниже. В этой программе создается класс fоо, который содержит в себе другой класс — bar. Если вы запустите программу, то увидите, что вначале вызывается конструктор класса bar, а затем конструктор класса foo. Когда программа заканчивает работу, вначале вызывается деструктор класса foo и только потом деструктор класса bar:


//ConstruetorDestructor
//Программа демонстрирует порядок, в котором вызываются
//конструкторы и деструкторы
#include "stdafx.h"
#using <mscorlib.dll>
using namespace System;

//Простой класс, состоящий из конструктора и деструктора
class bar
{

public:
bar();
-bar();
}

//Пусть все знают, что создается объект класса bar
bar::bar()
{

Console::WriteLine (S"Создается объект класса bar ")
};

//Пусть все знают, что объект класса bar уничтожается
-bar::bar()
{

Console : :WriteLine(S"Объект класса bar уничтожается") ;
}

//foo — это класс, внутри которого содержится другой класс,
//в данном случае — bar. При создании и удалении объекта
//класса foo также вызываются соответствующие ему конструктор
//и деструктор
class foo
{

public:
bar oTemp;
foo();
~foo о;
}

//Пусть весь мир знает, что рождается объект класса foo
foo::foo()
{

Console::WriteLine(S"Создается объект класса foo");
}

//Пусть весь мир знает, что объект класса foo уничтожается
foo::foo()
{

Console::WriteLine(S"0бъект класса foo уничтожается");
}

//Это функция main, которая всего лишь создает объект класса
//foo, которому присваивается имя oTemp. При этом автоматически
//вызываются конструкторы. Когда программа завершает свою
//работу, объект oTemp автоматически уничтожается и вы можете
//видеть, в каком порядке вызываются деструкторы
#ifdef _UNICODE
int main(void)
#else
int main(void)
#endif
{

foo oTemp;
return 0;
}

Вверх

Чтение кодов объектно-ориентированных программ

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

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


Назад | Начало урока | Вверх | Вперед
Содержание