Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Курс лекций Языки программирования.doc
Скачиваний:
0
Добавлен:
04.01.2020
Размер:
1.42 Mб
Скачать

Деструкторы

Деструкторы – чуть более простая вещь, нежели конструкторы (ломать – не строить). Деструктор – это функция, двойственная к конструктору. Он имеет следующий вид:

class X{

X(); - конструктор

~X(); - деструктор

}

Также как вызов конструктора автоматически связывается с размещением объекта в памяти, вызов деструктора связывается с уничтожением объекта.

Деструкторы статических объектов вызываются после завершения работы программы, в так называемом «стандартном эпилоге», схема выполнения программы выглядит следующим образом:

Пролог – здесь инициализируются статические объекты

Main - выполнение программы (плюс создание/уничтожение динамических объектов)

Эпилог - уничтожение статических объектов

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

Для класса Complex имеет смысл писать конструкторы (причем не один), а вот деструктор – не имеет, так как класс Complex никаких ресурсов не захватывает.

Для класса же MyString деструктор писать надо - он будет удалять строку из динамической памяти, точно также для класса Stack.

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

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

Таким образом мы практически закончили раздел специальных функций. Мы не затронули функции new и delete. Но это тема для самостоятельного изучения. Напоследок стоит сказать, как можно вызывать конкретные функции классов:

имя класса :: имя функции

если функция глобальная, и для нее класс не определен, то ее вызывают:

:: имя функции

То есть, написав:

:: new

мы укажем на базовую функцию new, которую можно, например, переопределить.

Посмотрим, что же у нас получилось из концепции классов C++. А то, что фактически ничего в базисе C++ по отношению к C не прибавилось (кроме ссылочного типа), даже не был введен популярный в других языках строковый тип. Почему нет? Мы определили – потому что с помощью концепции классов можно получить тип string, гораздо более мощный, чем если бы мы ввели его в базис.

Заметим, что ни Java, ни в Delphi нельзя создать класс, аналогичный sstring, так как в этих языках нельзя, например, перекрыть операцию «+». Поэтому в Delphi появился встроенный тип string и почти то же самое было сделано в Java (в Java не разрешено перекрывать стандартные операции). Например, в Java можно написать:

S+5;

и число 5 будет переведено в строку «5», так как в Java есть функция ToString для всех объектов.

Это единственный выход, если не делать таких же средств развития, как в C++, а эти средства иногда «кусаются», так как черезчур мощны.

Но в то же время заметим, что и в Java и в C++ отсутствует понятие диапазона. Иногда оно полезно, иногда нет (это понятие есть в Pascal, Modula-2, Ada). Можем ли мы придумать какой-нибудь класс, полностью эквивалентный диапазону? Конечно, да.

class Diap {

int L,R;

int Val

Diap( int l, int r) {L=l; R=r;};

operator int () {return Val;)

int operator = (int v) { if (v<L) || (v>R) Error(); Val=v; return Val;}

};

В чем недостаток? В том, что нам приходится хранить L и R для каждого вновь создаваемого объекта. Что нужно для исправления этого недостатка? Можно объявить их статически (в этом случае придется создавать для каждого диапазона свой класс).

Приведем в пример класс Vector:

class Vector {

int *body

int size;

explicit Vector (int)

~Vector() {delete [ ] body; }

( После delete мы пишем квадратные скобки по следующей причине, если есть:

p = new X[10];

то для каждого из 11 объектов будет вызван конструктор умолчания. Когда мы делаем

delete [ ] p;

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

int& operator [ ] (int i) { if (i<0 || i>size) Error();

return a[i];

}

Это примерно то же самое, что делает компилятор, например, Ada.

Лекция 13

Несмотря на то, что в языке С++ появились принципиально новые возможности, они, кроме преимуществ, влекут за собой и проблемы. Основная проблема – возросшая сложность языка. Интересно посмотреть, как языки, похожие на С++, борются с этой проблемой. Речь идет, прежде всего, о языках Java и Delphi (Delphi – это не язык, а система визуального программирования, использующая язык Borland Pascal with Objects, однако для краткости будем называть этот язык Delphi).

Java.

У языка Java есть ряд преимуществ по сравнению с С++ и Delphi, потому что, при создании, этот язык не был связан рамками совместимости с другими языками. Создатели этого языка взяли из С++ только то, что им показалось удобным, все что им показалось не удобным, они вынесли за скобки. Естественно, Java - классово-ориентированный язык, второй плюс языка Java – это наследование. Объектно-ориентированность пронизывает этот язык сверху до низу, в отличие от С++ и Delphi, которые связаны с понятием совместимости.

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

В Java есть несколько встроенных классов, и прежде всего, класс Object. Все классы являются наследниками класса Object. Кроме того, если мы говорим об исполняемой программе на языке Java, то должно быть некоторое "яйцо" из которого эта программа рождается (в С++ это функция main). В некотором классе обязательно должна быть описана статическая функция main:

public static void main (String[] args);

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

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

class X {

static int x;

};

int X::x = 1;

В отличие от С++, Java не поддерживает принцип РОРИ (Разделение Определения, Реализации, Использования). Однако, как и во всех языках, в Java определение и реализация отделяется от использования. В языках Модула-2 и Ада определение отделено от реализации, в языке Оберон наоборот – определение совмещено с реализацией. В языках с разделенными определением и реализацией, после изменения реализации не требуется перекомпилировать определение. Если определение и реализация объединены, то изменения в реализации требуют перекомпиляции и определения. Однако, сейчас перекомпиляция уже не так долго выполняется, и современные языки (Java, Оберон) используют подход, объединяющий определение и реализацию. Вывод интерфейса соответствующего класса без реализации осуществляет среда программирования.

В Java также присутствует понятие конструктора. Есть безаргументные конструкторы (в С++ это конструкторы по умолчанию) и конструкторы с аргументами. Однако, семантика конструкторов упрощена. В конструкторах нет системной части. Если конструктор вызывает конструкторы базовых классов, то он должен вызывать именно безаргументные конструкторы, иначе компилятор выдаст сообщение об ошибке. Что делать, когда нужно вызывать конструкторы базовых классов с каким-то списком параметров? Для этого существует специальное ключевое слово super, которое означает ссылку на базовый класс (через super(<список параметров>) мы можем вызывать нужные конструкторы). Если компилятор находит слово super в первой стороке, то он выполняет соответствующий конструктор, иначе он пытается вставить вызов безаргументного конструктора, и если его нет, то выдает сообщение об ошибке. Такая схема несколько упрощает синтаксис.

В языке С++ был специальный синтаксис для вызова конструктора подобъектов, и по умолчанию вызывался конструктор умолчания (если его нет – ошибка). Какой подход в Java? Во-первых, в Java можно инициализировать в описании класса значения по умолчанию:

class X {

static int x=1; //инициализация переменной

Y y = new y(…); // инициализация подобъекта

int[] a = new int[3]; //Это только отведение памяти!

//Нетривиальную инициализацию можно

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

int b[] = {1,2,3}; //Это тривиальная инициализация!

};

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

В Java фактически есть только два типа конструкторов – без аргументов и с аргументами. Особая роль безаргументного конструктора заключается только в том, что компилятор вставляет их в конструкторах подобъектов, если первым в конструкторе подобъекта не стоит вызов super(…). У других конструкторов вообще нет особой роли.

В Java неявные преобразования запрещены (поэтому и нет конструкторов преобразования), вспомним, что язык Java предназначен для программирования для среды Internet, а это ужесточает требования переносимости и безопасности. Язык С++ в некотором смысле, более мощный, чем Java. С++ можно расширить практически любым классом, и этот класс интегрируется с языком очень мягко. Программа на Фортране легко переводилась в С++, но на Java эту программу перевести уже не так легко.

Также отсутствует конструктор копирования. Конструктор копирования очень полезен, но только вместе с перекрытием операции присваивания. В Java запрещено перекрытие знаков стандартных операций вообще. Как же решена проблема копирования объектов? Наличие специального класса конструкторов и возможности переопределения заменены некоторым набором функций, который позволяет клонировать объекты. Операция присваивания в Java означает побитовое копирование, как для простых, так и для сложных объектов. Если один массив присваивается другому, то в результате, оба массива будут иметь одно тело (проблема висячих ссылок не возникает, потому что есть динамическая сборка мусора). Программист должен помнить, что присваивание сложных объектов – это только переброска ссылок.

Для копирования объектов используется метод класса Object, который называется clone(). Если мы хотим в массив А скопировать массив В, то мы должны писать A=B.clone(). Но по умолчанию функция clone() осуществляет побитовое копирование. Для глубокого копирования нужно переопределить эту функцию. Основные цели, которых достиг Страуструп, введя в язык С++ механизмы копирования и преобразования, были также достигнуты и создателями языка Java.

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

Для того, чтобы выполнять более нетривиальные действия по освобождению системных ресурсов предусмотрен метод void finalize(), который несколько похож на деструктор. Этот метод вызывается тогда, когда объект заканчивает свое существование, а конец существования объекта зависит от сборщика мусора, и предсказать этот момент нельзя. Неопределенность этого метода – это плата за динамическую сборку мусора. Если вы понаоткрывали файлов в некотором объекте, и прекращаете работу с объектом, то сборщик мусора может рассуждать так: свободной памяти много, и с удалением объектов можно подождать. При этом может переполнится таблица индексных дескрипторов открытых файлов. Еще одно отличие от деструктора – отсутствие системной части, т.е. в методе finalize() нужно обязательно вставлять метод super.finalize().

Delphi.

Язык Delphi развивался эволюционно и является несколько эклектичным. Ближним предком этого языка является Turbo Pascal. Все больше и больше язык Delphi начинает напоминать такой язык, как Java, оставаясь, при этом, компилируемым языком, сохраняя некоторую преемственность с Turbo Pascal. Интересно, что в языке Delphi появилось ключевое слово class, которое примерно соответствует ключевому слову object в Turbo Pascal. Класс в языке Delphi похож на класс Java, т.е. это всегда ссылка, и все классы являются наследниками одного класса TObject.

После определения объекта класса необходимо этот объект инициализировать. В классе TObject есть конструктор Create(…), который отводит память под объект и заполняет объект нулями. Этот конструктор надо вызывать явно. Если требуется более нетривиальная инициализация, то в классе можно определить свой конструктор, который затем и вызвать.

type T = class

constructor Init(…);

destructor Destroy(…);

end;

var X : T; //определение объекта

X = T.Init(…); // инициализация объекта

Все элементы классов размещаются в динамической памяти.

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

Системной семантики у конструктора нет, т.е. это обычная функция, которая инициализирует объект. Если в конструкторе нужно вызвать конструктор базового класса, то это можно сделать в самом начале, с помощью ключевого слова inherited: inherited Create(…). В языке Delphi за программиста компилятор ничего в конструктор подставлять не будет.

Поскольку все объекты класса размещаются в динамической памяти, то требуется освобождать память. Поскольку динамической сборки мусора в Delphi нет, то освобождать память нужно явно, с помощью понятия деструктора. У деструкторов также нет никакой системной семантики. Кроме того, у класса TObject есть метод Free(), который вызывается так: x.Free(). Этот метод смотрит, равен ли x константе nil (аналог NULL), если равен, то не делает ничего, а если не равен, то вызывается деструктор, и освобождается память. Но Free() не присваивает объекту nil, об этом должен позаботится программист.

Аналогично как и в конструкторах, в деструкторе нужно вызывать деструкторы базовых классов. Разумеется, эти деструкторы надо вызывать в конце данного деструктора.

Язык Delphi нужен только для программирования под Windows, и вне этой системы он не имеет смысла.