И.Г. Головин, И.А. Волкова - Языки и методы программирования (1160773), страница 21
Текст из файла (страница 21)
Деструкторы и финализаторы —это специальные функции-члены класса, автоматически вызываемыепри уничтожении объекта класса. Роль деструкторов и финализаторов — освобождение захваченных объектом вычислительных ресурсов. Если объект не захватывает ресурсы или ресурсы освобождаютсяавтоматически, то нужды в деструкторе нет.Деструктор класса X в языке C++ имеет вид~Х () ;Деструкторы бывают только без параметров.В языке Java деструкторов нет, их роль (до определенной степени)играет метод void finalize() — финализатор.В языке C# формально деструкторы есть (точнее есть функции,название и синтаксис которых совпадают с деструкторами C++),однако их поведение аналогично поведению метода finalize ()в языке Java, поэтому будем также называть их финализаторами.Деструкторы по смыслу обратны конструкторам, поэтому поведение и порядок вызова деструкторов обратны поведению и порядкувызова конструкторов соответствующих объектов.
Так, например,в языке C++ момент вызова деструкторов обратен моменту вызоваконструкторов:1) деструкторы статических объектов вызываются после выходаиз функции main;2) деструкторы квазистатических объектов выполняются при выходе из блока;3) деструкторы динамических объектов выполняются при вызовеоперации delete.Как уже отмечалось, в C++ существуют также временные объекты.По правилам языка они уничтожаются при выходе из конструкции,в контексте которой создаются. Например, временные объекты, созданные при выполнении вызова функции, уничтожаются (и вызывается соответствующий деструктор) сразу после завершения вызова.Как и конструкторы, деструкторы имеют пользовательскую (телообъявленного деструктора) и системную (сгенерированный код) части.Системная часть выполняется после тела деструктора.
В системнуючасть входят вызовы деструкторов подобъектов (в порядке, обратномконструированию) и вызовы деструкторов базовых классов.101Если в классе не объявлен явный деструктор, то компилятор генерирует неявный деструктор класса, состоящий только из системнойчасти.Итак, программист на языке C++ может достаточно точно установить момент вызова деструкторов объектов. На этом основанапростая и эффективная техника гарантированного освобождениялокально захваченных ресурсов, называемая «захват ресурса — этоинициализация» (английская аббревиатура — RAII).
В соответствиис этой техникой для работы с ресурсом разрабатывается класс, в конструкторе которого происходит захват ресурса. Освобождение ресурсапроисходит в деструкторе объекта. Для работы с классом в текущемблоке объявляется локальная (в нашей терминологии квазистатическая) переменная, при создании которой происходит захват ресурса.Далее работаем с переменной, а после выхода из блока освобождениепроисходит автоматически.Многие классы из стандартной библиотеки поддерживают этутехнику, например классы ifstream и ofstream, представляющиесобой внешние файлы ввода и вывода.
В конструкторах этих классов происходит открытие файла, в деструкторах — закрытие. Схемазадачи по обработке информации из внешнего файла может иметьследующий вид:{// текущий блокifstream in ("inputfile") ; // открыли вводofstream out ("outfile") ;// открыли вывод// обработали информацию из in// и скопировали в out} // при выходе из блока файлы автоматически// закрылисьПрограммист, использующий эту технику, гарантированно освобождает ресурсы.
Подробнее эта техника рассматривается в [17].Конечно, «утечка» ресурсов происходит и в программах на C++, нопроисходит она, как правило, в объектах из динамической памяти.В языках C# и Java вместо деструктора предлагается реализоватьметод-финализатор. Проблема состоит в том, что финализатор вызывается сборщиком мусора, поэтому предсказать момент вызовафинализатора невозможно.
Более того, возможна ситуация, когдапамяти хватает, и финализатор вообще не будет вызван. Поэтому дляклассов, захватывающих ресурсы, программист должен реализоватьобычные методы, которые освобождают эти ресурсы (например,метод close () для закрытия открытых файлов). На программистапользователя возлагается ответственность за своевременный вызовтаких методов.
Кроме того, необходимо предусмотреть вызов «очищающих» методов в финализаторе, если программист-пользовательпо каким-то причинам сам их не вызвал. Таким образом, наличияавтоматической сборки мусора недостаточно для написания на102дежных программ, корректно работающих с вычислительнымиресурсами.Копирование объектов. По умолчанию языки предоставляютсредства для поверхностного копирования (иногда такое копированиеназывают побитовым).В языке C++ копирование объектов происходит при выполненииоперации присваивания и при инициализации объекта копированиемиз другого объекта такого же типа.
Первая ситуация очевидна, а вторая требует пояснений. При инициализации объекта класса всегдавызывается конструктор. В случае инициализации копированием вызывается специальный конструктор копирования, т. е. конструктор,имеющий прототип:X (const Х&), либо X (Х&)Таким образом, объект, из которого копируются данные, передается в конструктор по ссылке.Конструктор копирования вызывается в следующих контекстах.1. Объявление объекта:Х а ; // работает конструктор умолчания Х()X b = а; // работает конструктор копирования Х(а)Заметим, что эта запись полностью эквивалентна следующейзаписи:X Ь(а); // работает конструктор копирования Х(а)2.
Передача параметра функции по значению (конструктор работает при инициализации формального параметра фактическимпараметром):void f (X а);X b;f (to) ; /* работает конструктор копирования Х(Ь)формального параметра а */для3. Возврат значения функции (конструктор работает при инициализации временного объекта возвращаемым значением):X МакеХО{X а;return а;}X Ь;b = МакеХО; /* работает конструктор копированияX temp(а) для временного объекта, далее операцияприсваивания b = temp; */4. При возбуждении исключения типа X (см. гл.
9):103X а;throw а;Каждый класс имеет операцию присваивания и конструкторкопирования. Если в классе нет явно определенных операции присваивания и конструктора копирования, то они генерируются компилятором. Сгенерированные компилятором присваивание и конструктор копирования осуществляют поверхностное копирование.Генерация происходит почленно: для членов-данных базисных типовкопирование побитовое, для членов-подобъектов вызывается соответствующая операция (конструктор копирования), которая можетбыть сгенерированной, а может быть и явной.Программист-разработчик класса сам определяет, какая семантикакопирования требуется для класса.
Если стандартное поверхностноекопирование не работает, то программист должен явно определитьв классе операцию присваивания и конструктор копирования.Заметим, что компилятор требует единого подхода к реализациикопирования: операция присваивания и конструктор копированиядолжны одновременно либо генерироваться, либо реализовыватьсяявно.Приведем пример простого класса, реализующего динамическийвектор (разумеется, это модельный пример, в реальной жизни следуетиспользовать векторы из стандартной библиотеки).
Вектор содержитв себе указатель на динамически размещаемый массив и требуетглубокого копирования:class Vector {int *body;int size;public:Vector (int sz){body = new int[size = sz];}Vector (const Vector & v){body = new int [size = v.size];for (int i = 0; i < size; i++)body[i] = v.body[i];}Vector Soperator = (const Vectors v){if (this != &v) {delete [] body;body = new int [S size = v.size];for (int i = 0; i < size; i++)body[i] = v.body[i];104}return *this;}-Vector() { delete [] body; }int& operator[] (int index) { returnbody[index]; }};Обратите внимание на реализацию операции присваивания,которая делает то же, что и конструктор копирования, но предварительно она должна «очистить» объект, при этом проверив, нет липрисваивания в себя.Попробуйте ответить на вопрос, что будет, если убрать явныеоперации присваивания и конструктор копирования, т.е.
оставитьстандартную семантику копирования.В языках C# и Java операция присваивания применяется к типамзначений и ссылкам. Поэтому понятий конструктора копированияи переопределения операции присваивания там нет. Для копирования содержимого объекта следует определить метод класса Clone ()(в Java — clone ()), который возвращает ссылку на копию объекта.Реализация метода, конечно, должна учитывать семантику копирования для объекта класса. Для реализации поверхностной семантикиязык Java предоставляет вариант метода clone () , реализующегопростое копирование всех членов-данных, а язык C# — методMemberwiseClone () с аналогичной семантикой.Преобразование объектов классов.
Для реализации явных преобразований объектов классов вполне хватает средств, которые мыуже рассмотрели. Пусть X — это новый класс, а Т — произвольныйтип данных (отличный от X и необязательно класс). Тогда конструктор с прототипом X (Т) можно рассматривать как функцию явногопреобразования из типа т в класс X:ТXXbt;а = new X(t); // Java или C#b(t); // C++= X(t); // C++Аналогично нестатическая функция-член класса X с прототипомТ МакеТоможет рассматриваться как функция явного преобразования изкласса X в тип т.Однако приведенные примеры таких преобразований явные, т.е.явно указанные программистом.При этом проблема состоит в возможности сделать неявными преобразования, определяемые программистом (называемые105пользовательскими), т.
е. заставить компилятор вставлять вызовысоответствующих функций, как это происходит, например с преобразованиями арифметических типов в C++.Язык Java запрещает любые неявные преобразования междуобъектами классов (исключение составляют только неявные преобразования к стандартному типу string, разрешенные в некоторыхконтекстах).Языки C++ и C# разрешают неявные преобразования для классов,определяемых пользователем.В C++ преобразования определяются специальными функциямичленами: конструкторами преобразования и функциями преобразования.Конструктор преобразования имеет прототип видаХ(Т)(можно еще Х(Т&)и X (const Т&) )При наличии такого конструктора допустимо неявное преобразование из Т в X:Т t;X b(t); // это явный вызов конструктора!b = t; /* а вот это - уже неявное преобразование:транслятор вставляет такой код: b = X(t); */void f (X а);f(t); // еще неявное преобразование: f(X(t));void g(const X& a);g(t); // аналогично: g (X(t ));Заметим, что в процессе неявного преобразования могут появляться временные объекты.Функция преобразования имеет видclass X {operator ТО;,};У функции преобразования нет типа возвращаемого значения (оноопределяется именем функции) и нет параметра (кроме неявногопараметра this).При наличии такой функции допустимы неявные преобразования:X а;Т t;t = а; // t = a.operator Т();void f (Т t);f(a); // f(a.operator T ());void g(const T& t);g(a);106// g(a.operator T ());Неявные преобразования можно вызывать и явно, используяобычный синтаксис:Х а , b; Т t;а = (X )t ;t = (Т )b ;Ключевое слово e x p l i c i t .
Неявное преобразование, осуществляемое конструктором (преобразования), может иметь неприятноеследствие: иногда конструктор имеет синтаксис конструктора преобразования случайно, а само преобразование семантически бессмысленно с точки зрения типа данных. Например, в рассмотренном ранееклассе Vector конструктор Vector (int) является конструкторомпреобразования, поэтому следующий пример вполне корректен, хотяособого смысла не имеет:Vector v (20);v = 1; // v = Vector (1); - скорее всего, ошибка// старое содержимое v уничтожено,// v заменен новым вектором длины 1Для подавления неявного вызова конструктора преобразования,если такое действие может привести к ошибке, конструктор преобразования необходимо объявлять с использованием ключевого словаe x p l i c i t (явный):explicit Vector (int sz) ;Теперь компилятор выдаст ошибку в приведенном примере.