Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методичка ООП.doc
Скачиваний:
23
Добавлен:
08.11.2018
Размер:
1.4 Mб
Скачать

Деструкторы

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

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

destructor Destroy;

Хотя класс может иметь более чем один деструктор, рекомендуется для каждого класса иметь только один деструктор, именуемый Destroy. Деструктор всегда должен иметь область видимости public.

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

var MyObject: TMyObject; ...

//Конструктор вызываем через имя класса MyObject := TMyObject.Create; ... //Деструктор вызываем через имя уничтожаемого объекта MyObject.Destroy;

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

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

В Object Pascal для уничтожения объектов рекомендуется использовать унаследованный метод Free, который первоначально проверяет указатель (не равен ли он nil), а затем вызывает Destroy. Понятие «наследования» будет рассмотрено далее.

Приведем пример. Дополним описание класса стек из прошлых примеров деструктором.

1. Стек в массиве

TSObStack = class private Stack: array[1..1000] of TElem; Top: integer; public //конструктор по умолчанию Constructor Create; overload; //конструктор инициализации Constructor Create(e: TElem); overload; //деструктор Destructor Destroy;

procedure InStack(Elem: TElem); function OutStack(var Elem: TElem): boolean; end;

...

Destructor TSObStack.Destroy; Begin //Поскольку объект не захватил никаких ресурсов //никакие действия не нужны End;

2. Динамический стек

TDObStack = class private Top: TERef public //конструктор по умолчанию Constructor Create; overload; //конструктор инициализации Constructor Create(e: TElem); overload; //деструктор Destructor Destroy;

procedure InStack(Elem: TElem); function OutStack(var Elem: TElem):boolean; ... end;

...

Destructor TDObStack.Destroy; Var Elem: TElem; Begin //Выполняется освобождение динамической памяти //Для этого до тех пор пока стек не пуст //вызывается метод извлечения элемента из стека while OutStack(Elem) do; End;

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

Procedure Test; var DStack: TDObStack; SStack: TSObStack; a: TElem; begin //создание объектов DStack := TDObStack.Create(5); SStack := TSObStack.Create; ... //использование объектов ... //уничтожение объектов DStack.Destroy;//или DStack.Free SStack.Destroy;//или SStack.Free end.

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

interface Type

TList = class private //файловая переменная F: TextFile; //количество записей n: byte; //массив фамилий FName: array[1..100] of string; //массив оценок FMark: array[1..100] of byte; //метод чтения function GetMark(name: string): byte; //метод записи procedure SetMark(name: string; value: byte); public //Конструктор по умолчанию constructor Create; overload; //Конструктор инициализации constructor Create(FileName: string); overload; //Деструктор destructor Destroy; //свойство – количество оценок, только для чтения property Number: byte read n; //свойство - оценки property Mark[name: string]: byte read GetMark write SetMark; end;

Implementation

//Конструктор по умолчанию constructor TList.Create; begin //Вызываем конструктор инициализации, передавая ему //имя файла по умолчанию Create(‘ведомость.txt’); end;

//Конструктор инициализации constructor TList.Create(FileName: string); var VName: string; VMark: integer; begin AssignFile(F,FileName); Reset(F); while not Eof(F) do begin //пока не конец файла считываем данные из файла //и помещаем в векторное свойство ReadLn(F,VName); ReadLn(F,VMark); Mark[VName] := VMark; end; CloseFile(F); end;

//Деструктор destructor TList.Destroy; var i: byte; begin Rewrite(F); for i := 1 to n do begin //записываем данные в файл WriteLn(F,FName[i]); WriteLn(F,FMark[i]); end; CloseFile(F); end;

//реализация метода чтения function TList.GetMark(name: string): byte; var i: byte; begin result := 0; for i := 1 to n do if name = FName[i] then result := FMark[i]; end;

procedure TList.SetMark(name: string; value: byte); var i: byte; NewFam: Boolean; begin NewFam := true; for i := 1 to n do if name = FName[i] then begin NewFam := false; FMark[i] := value; end; if (NewFam) then begin n:=n+1; FMark[n] := mark; FName[n] := name; end; end;

В конструкторе по умолчанию здесь вызывается конструктор инициализации. Это делается, чтобы не дублировать код открытия файла и чтения из него данных. Конструктор инициализации в этом случае вызывается не через имя класса, а через имя объекта, точнее через указатель self на текущий экземпляр объекта (self опускается). Поэтому память под объект заново не распределяется, а производятся только действия, описанные в теле конструктора – что и требуется. Конструктор инициализации считывает данные из файла и помещает их в поля объекта FName и FMark, используя векторное свойство Mark. Поле n при этом примет значение, равное количеству записей в файле – см. реализацию метода TList.SetMark. Деструктор – помещает данные в файл.

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

  1. Объект может быть глобальной переменной;

  2. Объект может быть локальной переменной какого-либо метода или свободной подпрограммы;

  3. Объект может быть полем другого объекта.

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

if (MyObject <> nil) then MyObject.Destroy; MyObjevt := TMyObject.Create;

Это гарантирует, что не произойдет потери динамической памяти, распределенной под старый экземпляр класса – рис. 5.

Эти действия можно заменить вызовом метода Free, который сначала проверяет, не равен ли указатель nil, а затем освобождает память:

MyObject.Free MyObjevt := TMyObject.Create;

Память, распределенная под глобальный объект, должна быть освобождена, когда он становится не нужен. Для этого также рекомендуется использовать метод Free или предварительно проверять, не равен ли указатель nil.

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

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

    1. Создание и уничтожение объектов в C++

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

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

Пример класса с конструктором и деструктором:

class CSomeClass { private: ... public: СSomeClass(); //конструктор CSomeClass(); //деструктор ... };

Как уже было сказано, в языке C++ существуют как статические, так и динамические объекты. Статические объекты создаются автоматически, когда программа входит в область действия переменной. Динамические – явно с помощью оператора new. И в том и в другом случае вызывается конструктор. Также конструкторы вызываются при создании временных объектов, например при передаче статического объекта в качестве параметра в процедуру.

Уничтожаются статические объекты также автоматически при выходе из их области действия. Динамические объекты должны быть удалены явно с помощью оператора delete.

Например:

class CSomeClass { ... public: CSomeClass(); //конструктор ~CSomeClass(); //деструктор ... };

void main(void) { CSomeClass StatObject; //автоматически вызовется конструктор

CSomeClass * DynObject; //объект динамический, поэтому конструктор не вызовется //и объект не будет создан DynObject = new CSomeClass(); //вызов конструктора – создание объекта

...

delte DynObject; //вызов деструктора – уничтожение динамического объекта //перед завершением работы процедуры //автоматически вызовется деструктор объекта StatObject }

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

class CDate { int month; int day; ... public: CDate(); //первый конструктор CDate(int m, int d); //второй конструктор ~CDate(); //деструктор ... };

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

class CDate { int month; int day; ... public: CDate(); //конструктор по умолчанию CDate(int m, int d); //конструктор инициализации CDate(const CDate & c); //конструктор копирования ... };

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

class CDate { int month; int day; ... public: CDate(); //конструктор по умолчанию ... };

CDate::CDate() { month = 1; day = 1; }

...

void main(void) { CDate Date1;//статический объект создается //конструктором по умолчанию

CDate * Date2 = new CDate();//вызов конструктора //по умолчанию для создания динамического объекта

... }

Если у класса нет конструкторов, компилятор автоматически генерирует конструктор по умолчанию.

Конструктор инициализации получает в качестве аргументов значения, которыми будут инициализированы его данные. Параметры конструктора инициализации указываются в скобках после оператора new при динамическом распределении памяти или в скобках после имени переменной при статическом:

class CDate { int month; int day; ... public: CDate(int m, int d); //конструктор инициализации ... };

CDate::CDate(int m, int d) { month = m; day = d; }

...

void main(void) { CDate Date1(12,31);//статический объект создается //конструктором инициализации

CDate * Date2 = new CDate(12,31);//вызов конструктора //инициализации для создания динамического объекта

... }

Конструктор копирования имеет аргументом объект класса:

CDate(const CDate & c);

Такие конструкторы запускаются при копировании данных «старого объекта» во вновь создаваемый, например, в случае объявления с инициализацией другим классом:

class CDate { int month; int day; ... public: CDate(const CDate & c); //конструктор копирования ... };

CDate::CDate(const CDate & c) { //данные копируются month = с.month; day = c.day; }

...

void main(void) { CDate Date1(12,31);//статический объект создается //конструктором по умолчанию

CDate Date2 = Date1;//статический объект создается //конструктором копирования

CDate Date3(Date1);//статический объект создается //конструктором копирования – другой вариант

CDate * Date4 = new CDate(Date2);//динамический объект //создается конструктором копирования

... }

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

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

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

class CSomeClass { ... public CSomeClass(); }; ... CSomeClass * x = new CSomeClass [10]; ... delete[10] x;

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

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

class CSomeClass { ... public CSomeClass(int a); ~CSomeClass(); };

... CSomeClass * x = new CSomeClass(10);//создание объекта //x->CSomeClass(12); //такой вызов конструктора недопустим ... //x->~CSomeClass(); //допустимо в специальных случаях. //В данном случае недопустимо

delete x; //корректный вызов деструктора ...