Лекция 12 (Лекции (2009) (Саша Федорова))
Описание файла
Файл "Лекция 12" внутри архива находится в папке "Лекции (2009) (Саша Федорова)". Документ из архива "Лекции (2009) (Саша Федорова)", который расположен в категории "". Всё это находится в предмете "языки программирования" из 7 семестр, которые можно найти в файловом архиве МГУ им. Ломоносова. Не смотря на прямую связь этого архива с МГУ им. Ломоносова, его также можно найти и в других разделах. .
Онлайн просмотр документа "Лекция 12"
Текст из документа "Лекция 12"
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 – ссылки на объект, подготовленный на уничтожение, но еще не уничтоженный).