Лекция 12 (1160845)
Текст из файла
13
Лекция 12.
Специальные функции
-
Конструктор
-
Деструктор
-
Оператор преобразования
-
Дополнительный возможности механизма классов
Важнейший класс специальных функций – конструктор.
Мы уже знаем, вызов любого конструктора происхдит , когда объекты отводится память. В языках с референциальной моделью в момент вызова конструктора происходит ссылки с объектом.
В С++, как мы знаем, объекты создаются при помощи конструктора. В других языказ за вопросы инициализации, копирования и удаления объекта также овечают именно специальные функции.
КОНСТРУКТОРЫ
Виды конструктора:
-
Конструктор умолчания(есть в C#, Java, C++, нету в Delphi).
-
Конструктор копирования(вызывается, когда происходит копирование объектов)
-
Конструктор преобразования(в С++, C#, Java конструктор не наследуется)
-
Иные конструкторы
Почему конструктор умолчания выделяется в особый класс? В чем его особенность?
В некоторых случаях объект создается автоматически. В Delphi генерации конструкторов нет, все объекты являются наследниками одного объекта, у которого уже есть конструктор.
В Java, C# и Delphi есть дерево объектов, и все объекты, созданные программистом, «торчат» из класса Object(C#). В классе Object есть конструктор Create() и деструктор Destroy(), из чего следует, что каждый объект в Delphi имеет хотя бы один конструктор и хотя бы один деструктор.
В С++ автоматически могут генерироваться конструктор умолчания и конструктор копирования.
Конструктор умолчания
Х а; //подразумевается вызов конструктора по умолчанию
X a(1); //явный вызова конструктора
X a(); //нельзя
А в случае с указателями можно двумя способами:
X * px = new X;
X * px = new X(); //В C# и Java можно только так
Существует еще 2 ситуации, котгда конструктор умолчания вызывается автоматически:
-
Наследование: конструктор умолчания для базового и производного классов
class X {
X();
X (int);
};
class Y: public X{
Y(); //если его нет, то явно сгенерируется конструктор умолчания(вначале перед ним, естественно, каждый раз будет вызываться конструктор класса Х.)
Z z;
};
Представим, что в класса Х конструктора нет. Возникают 2 вопроса:
-
как вызывается конструктор базового класса
-
как вызывается конструктор подобъекта
Вызов любого конструктора состоит из двух фаз:
-
вызов конструктора базовой части
-
вызов конструктора подобъекта
В любой из этих фаз может присутствовать ползовательская часть(то, что программист указал явно)
Пример «явного написания» конструктора в общем случае:
Заголовок [инициализация] тело
Например:
Y(): X(); z() {……………}
Можно и так:
Y(int I, int j): X(i); z(j) {/* пользовательская часть */};- пользовательская часть может отсутствовать
В C# и Java по синтаксису разрешены инициализации объектов:
class X{
Z z = new Z();
int i=0;
//простые инициализации можно выполнять непосредственно в коде самого класса. Более того, если для С++ критичен вызов конструкторов подобъектов, то в C# и Java мы работает со ссылками, и всегда можем их доопределить. Если вызов тривиален, то мы, конечно, можжем определить все сразу.
Синтаксис С# напоминает синтаксис С++, но в нем присутствует ключевое слово base, возвращающее ссылку на объект базового класса:
Y(): base(0) {………….}
А в Java для подобных целей есть ключевое слово super(аналог base в C#)
Вызов конструктора базового класса в Java может быть только первым оператором тела конструктора:
Y()
{ super(1); ………………….. }
Если первый оператор отличен от вызова super, то компилятор автоматически вставляет super();//вызов конструктора умолчания базового класса.
Замечание. В Ада для конструктора и деструктора использовались функции Init() и Destroy(). Для определения собственного конструктора и деструктора достаточно было переопределить эти функции(что, кстати, чатсо забывали делать, и это служило причиной многих ошибок)
Конструктор копирования
В С++ возможно несколько контекстов копирования:
-
передача параметров по значению
void f(X a); // передача фактического параметра функции по значению
X g() { return X(); }
-
«Инициализация присваиванием»
X a=b;//синтаксический сахар. Аналог строчкой ниже:
X a(b);
int a=-1;//Внимание! Это инициализация, а не присваивание!
Особенно это важно для статических объектов.
Следует заметить, что при переопределении оператора присваивания нам надо «освобождать» предыдущие ресурсы перед копированием, чего не надо делать в конструкторе коопирования.
Как мы помним, существует 2 семантики копирования:
глубокое и поверхностное(Deep/Shallow), поверхностное – побитовое, или неглубокое(если копируется ссылка – в C#, Java, Delphi, где бъекты имеют ссылочную природу). Поверхностное копирование линейного списка – это копирование ссылок на первый его элемент. В языках со ссылочной природой копирование по определению поверхностное: вот, например, в таком случае:
int [ ]a;
int [ ]b.
a = new int [N];
b=a;
Итак, проблем в копировании:
-
Могут быть с поверхностным копированием
-
В том, что копирование не всегда хорошо разрешать(и поверхностное, и глубокое – всегда могут присутствовать данные, доступ к которым лучше запретить.)
Поэтому, если отдельно есть конструкции, отвечающие за копирование(конструктор копирования и оператор присваивания), всегда можно сделать их приватными.
(1) X(X&);
(2) operator=(X&)
Если мы не копируем объект, то конструкции (1) и (2) не нужны. Описав прототип этих функций, но не определив их, мы запрещаем копировать объект данного класса. Когда отсутствует и прототип, и объявление, конструктор копирования генерируется автоматически.
Минимум объект может иметь один конструктор: конструктор копирования.
Конструктор копирование генерируется так:
Если класс верхнего уровня, конструктор генериурется почленно: для каждого члена объекта:
class X{
int a;
Z z;
};
class X{
X();
X(X&);
};
class Y: public X{
//как будет сгенерирован конструктор и какой конструктор он будет вызывать?
Если ничего не написать:
Генерируется конструктор копирования с учетом конструктора копирования базового класса.
А если написать: Y(Y&){………………}, то перед {………………}, очевидно, вызовется конструктор умолчания базового класса.
Если нас устраивает стандартная семантика копирования по умолчаию, можно ничего не писать...
Мораль: в инициализаторе конструктора копирования производного класса надо написать конструктор копирования. «Ручками».
Y(Y& ): X(y){……………….}
Эта же проблема присутствует и в других языках.
С#
В классе Object(общего предка для всех классов), есть защищенный метод MemberwiseClone, возвращающий копию объекта.
Java
В этой точки зрения наиболее адекватно проблема решена в Javа. Там существует 4 уровня поддержки копирования.
Интерфейс-маркер – по определению пустой интерфейс(не содержит членов).
Интерфейс – это просто набор методов. Он определяет некий контракт, говорящий о том, что если класс поддерживает некий интерфейс, он должен реализовывать определенный набор методов. А если интерфейс пустой, то все его члены-методы «зашиты» в компилятор.
Интерфейс называется сloneable, когда он пустой.
В Java был введен пустой интерфейс cloneable, содержащий метод Clone(), осуществляющий побитовое поверхностное копирование. Возможны 4 ситуации:
-
Полная поддержка копирования – возможность явной реализации. Класс X реализует интерфейс Cloneable:
Сlass X: Cloneable{
//Тут мы должны написать:
public X Clone();
//Допускается также:
public Object Clone();
..........................................
Метод Clone() может использовать любые члены класса(и приватные тоже.)
};
-
Возможна и другая ситуация: полный запрет кпированияя: при поопытке скопировать объект выбрасываем исключение. Подменяем соответсвующий защищенный метод clone():
class X{
protected Object Clone(){ throw CloneNotSupportedException; }
………………………………….
};
-
Условная поддержка: элементы, которые копируются, могут быть под полным запретом.
Пример: коллекция умеет себя копировать, а элементы, из которых она состоит – нет.
class X: Cloneable{
public X Clone throwing CloneNotSupportedException
{
//Для каждого элемента коллекции вызывается метод Clone();
};
…………………………………………………..
};
-
Еще одна ситуация – когда мы не наследуем метод Clone()
О статических членах
Как мы знаем, статические члены должны инициализироваться 11 раз. Но когда?
Рассмотрим статические члены класса в различных языках.
C++
class Z{
static X a;
…………..
};
X Z::a(…);
C#
Существует статический конструктор, который вызывается 1 раз до первого использования и до первого обращения к любым членам класса.
static X() {…………}; //полная форма статического конструктора по умолчанию в языке C#
Java
static{…………….}; //аналог статического конструктора в Java
Деструктор
Вообще говоря, к деструкторам применимы те же правила. Деструкторы ненаследуются, а, следовательно, будут сгенерированы автоматически.
Деструкторы начинают работать в начале уничтожения объектов.
Напомним - в C# и Java есть сборщик мусора. В С++ и Delphi сборщика мусора нет, потому возникает необходимость явного освобождения памяти.
C++
delete p;
Отличие С++ от Delphi – в нем происходит автоматический вызов деструктора.
Общая проблема - в процессе функционирования объекты получают некий ресурс.
В С++ и Delphi мы всегда контролируем, когда ресурсы освобождаются.
Специальные функции в Delphi:
X:=I.Create();
X.Create();
X.Free
В С# и Java за счет присутствия сборщика мусора момент уничтожения объектов невозожно отловить. Но иногда программа может кончиться раньше, чем сборщик заметит мусор
C# Пример
Класс Image
Статический метод FromFile
Image im=Image.FromFile(fname);
//обработка im
//изменение im
im.SaveToFile(filename);//файл захвачен. Будет освобожден только тогда когда уничтожится объект im.
Вот тут сборка мусора вредит! Мы не будем иметь доступа к захваченному файлу вплоть до момента, когда сборщик мусора уничтожит наш im.
O блоке try и finally а также о Dispose
В C#, Java, Delphi существует конструкция
try
блок
finally
{
………………..
}
Delphi:
try
операторы
finally
операторы
end
.Такая вещь, как finally, очень важна. Она будет выполнена независимо от того, как кончился блок.(Это необходимо, так как в C# и в Delphi нету вызова конструктора по умолчанию в конце блока)
В C# для решения подобных проблем необходим общий интерфейс IDisposable с методом Dispose(). Данный метод вызывает финализатор объекта и ставит его в очередь на уничтожение, обеспечивая выполнение деструктора.
Вот так:
try
{
//…….
}
finally
{
im.Dispose();
}
Вводится специальная конструкция:
using(инициализатор) //инициализатор – T x=expr; x=expr;
блок
Который эквивалентен
try{
инициализатор
}
finally{
x.Dispose();
}
Dispose тоже надо писать хитро (ведь все сборщики мусора находятся в другом потоке, и это, естественно, должна учитывать реализация Dispose).
Учитывая то, что в Java есть сборщик мусора, там не существует деструктора. В классе Object существует защищенный метод
protected void finalize();
который вызывается, когда объект перестает существовать. После вызова такого метода объъект становится недоступен.
Однако! Существуют методики, позволяющие возродить к жизни уже убитый объект. Конечно, это не самая лучшая техника и следует ее по возможности избегать.
В случае, если класс на протяжении своего существования должен освобождать ресурсы не один раз, он обязан содержать метод Close(), который будет это делать. Метод Dispose() вызывается один раз , а close должен быть запрограммирован таким образом, чтобы можно было вызывать его много раз.
В Java метод finalize() вызывается сборщиком мусора. В C# существует деструктор – тонкая обертка для финализзатора finalize().
Динамическая сборка мусора
Динамическая сборка мусора вызывает множество проблем. Простейший алгоритм сборки мусора – mark and sweep – алгоритм, основанный на подсчете числа ссылок на каждый объект. Как только это чсло становится равным нулю, объект уничтожается. Но в С# и Java возникает проблема кольцевых ссылок.
В любом случае, алгоритм сборки мусора должен делить объекты на «живые» и «мертвые».
Как работает сборщик мусора?
В программе у нас существуют
-
статические объекты классов
-
стек у main
Существует таблица ссылок на объекты. Помечаем все «живые» объекты(объекты верхнего уровня), а потом у них(внутри них) ищем другие ссылки, и таким образом рекурсивно обходим все. Получаем глобальную таблицу всех объектов. Все НЕживые объекты по определению есть мертвые. Уничтожаем их и радуемся
Замечание. Объект может быть подготовлен к уничтожению, но еще не уничтожен. Пример – работа с файлами. Пусть нам необходимо прочитать некоторый файл. Образ его уже подготовлен на цуничтожение, а данные еще остались. Возникает понятия сильной ссылки(ссылки на живой объект) и слабой ссылки(weak reference – ссылки на объект, подготовленный на уничтожение, но еще не уничтоженный).
Характеристики
Тип файла документ
Документы такого типа открываются такими программами, как Microsoft Office Word на компьютерах Windows, Apple Pages на компьютерах Mac, Open Office - бесплатная альтернатива на различных платформах, в том числе Linux. Наиболее простым и современным решением будут Google документы, так как открываются онлайн без скачивания прямо в браузере на любой платформе. Существуют российские качественные аналоги, например от Яндекса.
Будьте внимательны на мобильных устройствах, так как там используются упрощённый функционал даже в официальном приложении от Microsoft, поэтому для просмотра скачивайте PDF-версию. А если нужно редактировать файл, то используйте оригинальный файл.
Файлы такого типа обычно разбиты на страницы, а текст может быть форматированным (жирный, курсив, выбор шрифта, таблицы и т.п.), а также в него можно добавлять изображения. Формат идеально подходит для рефератов, докладов и РПЗ курсовых проектов, которые необходимо распечатать. Кстати перед печатью также сохраняйте файл в PDF, так как принтер может начудить со шрифтами.