лекции (2008) (Фингеров Александр_ Кононов Алексей_ Кузин Сергей) (1160833), страница 12
Текст из файла (страница 12)
};
class Y: public X{
public:
void f(){i=0;..} - ошибка
void g(X& x){x.i=0;} - ошибка
};
Если изменим пример, и напишем protected и добавим this->i вместо i, то наивное понимание говорит о том, что 2 вещи из класса Y эквивалентны, и если работает f(), то люди думают, что g() тоже сработает, то это не так и в C++, и в C#, и в Java. Если член объявлен как protected, то это значит функции члену производного класса, этот член доступен через ссылку на этот класс или производный. Каждый класс, когда объявляет ограничения доступа, он объявляет контракт. Конкретные детали указываются в реализации, но минимальные требования это когда мы говорим, что публичное, а что приватное, и оставляет по 2му случаю всю компетенцию за собой. Производным классам позволяется менять контракт, указывая это при наследовании. Чтобы мы получили доступ к i, нужно всего-навсего поменять void g(Y& x) .
п.3 Создание и уничтожение объектов (классов)
Создание => конструктор
Впервые понятие конструктора было введено в С++, это специальная функция, которая вызывается при создании объекта класса. Иногда конструктор может быть пустой. У Delphi, Java, C# - референциальная модель, то есть объект создаётся только в динамической памяти. В С++ объекты делятся на 3 вида — статические, квазистатические и динамические.
С++:
class X{
X(){..}
C#/Java:
class X{
X(){..}
Синтаксис один и тот же, но семантика различается. Экзотичен язык Delphi:
type X=class
constructor Load;
Имя конструктора может быть произвольным в языке Delphi. Это единственный язык из этой группы, в котором конструкторы наследуются.
С++:
X a; - вызывается конструктор умолчания.
Кстати, в Delphi нет таксономии конструкторов в отличие от остальных языков.
X a(); - такая вещь рассматривается как прототип функции, а не вызов конструктора в синтаксисе языка С.
X* px;
px = new X;
в то же время можно написать и
px = new X();
Объект может быть ещё объявлен как подобъект.
Объявление для статических и квазистатических объектов — тип, далее имя. Если параметры/без параметров, явно указываем. С точки зрения других языков, всё существенно проще. Единственная форма создания объектов — new X();. Возникает вопрос, а где и каким образом инициализировать подобъект. В С++ - в конструкторе и больше нигде. Если программист явно не указывается какой конструктор должен вызываться, то срабатывает конструктор по умолчанию. Рассмотрим язык C#:
class X: Base
{
Y a;
X(){}
};
В С++ в начале будет сгенерированная часть — вызов конструктора базового класса, потом конструктор умолчания подобъекта, а только после этого будет выполнено тело.
Здесь конструктор подобъекта вызван не будет, так как Y это просто ссылка. В Java то же самое. Если мы не инициализируем ссылку, туда присвоится нулевое значение. Правда если Y является структурой, то конструктор умолчания будет вызван (притом конструктор умолчания там переопределять нельзя), и будут вставлены все поля по умолчанию. Тем не менее, для Base будет вызван конструктор умолчания. В Java и C# есть удобный механизм — инициализаторы. Например, Y a=new Y(); - будет вызван непосредственно объект. Если мы не хотим или не можем вызывать конструктор по умолчанию в С++, единственный инструмент — список инициализации.
Конструктор имеет вид:
имя_класса([аргументы]):[список_инициализации] тело
X(0), Base(1), i(0), y(-1);
В начале вызывается список инициализации, и только потом отрабатывает тело. Программист может указывать Base(1), но не указывать y(-1), тогда подставится y(). В С++ роль конструктора умолчания — автоматический вызов для базового класса, потому что конструкторы подобъектов всегда указываются явно.
C#:
X():this(0) или base(-1)
Java:
class X entends Base{
X(){Super(0) или this(-1) — первый оператор конструктора
Все конструкторы вызываются начиная с базового. Если в инициализаторы поставить побочный эффект, то в каком порядке вызываются инициализаторы?
class Base{
Y y=new Y();
Base(){..}
}
class X:Base
Z z=new Z();
X();
}
В С++ сначала конструктор Base, потом конструктор Y, потом Z, потом X.
В C# сначала будет работать инициализатор, потому что объект уже построен, построена таблица виртуальных методов, и получается, что проработает конструктор Z, потом Base, но перед его вызовом проработает его инициализатор, то есть проработает конструктор Y, и лишь в самом конце будет X. Конструкторы не наследуются, поэтому представим такую ситуацию.
X(const X&x){..}
Х наследуется от Base, список инициализации опущен, оставили всё на совесть компилятору. Что же будет вызвано? Вот такой конструктор — Base() - конструктор умолчания. Если конструктора копирования нет, то он генерируется и операция копирования применяется почленно ко всем членам. Для подчленов класса X тоже будут конструкторы копирования, и очевидно, что при его генерации, вызывается конструктор копирования класса Base. А если он у нас написан, то вызовет конструктор умолчания. Если хотим явно, то и вызываем явно.
X(const X&x){..}:Base(x)
Заметим, что ни в С#, ни в Java нет конструкторов копирования и преобразования из-за референциальной модели памяти. В С++ наибольшая таксономия конструкторов.
Лекция 24.
Разберемся до конца с объектами классов.
Поговорим про язык Delphi.
Тут ситуация наиболее простая. Здесь конструкторы и деструкторы наследуются и во вторых они вызываются явно. Они могут иметь различный синтаксис:
Type X=class
Constructor load;
Destructor Destroy;
End
Они могут называться как угодно, главное, чтобы были конструктор и деструктор. И никакие конструкторы и деструкторы по умолчанию не создаются, они наследуются из базового класса для всех TObject. В Си++ объекты конструируются пошагово, поэтому в конструктор нельзя всунуть виртуальный метод, потому как не сформирована таблица виртуальных методов. А в Delphi так делать можно, потому что можно быть уверенным, что уже создана таблица виртуальных методов. Так как нет конструкторов по умолчанию, мы должны вручную вызывать конструкторы базового класса (соответственно в нашем конструкторе).
Inherited – ссылка на базовый класс. (super – Java, base – C#)
Inherited Create –вызывает конструктор базового класса.
Синтаксис языка Delphi.
В языке Delphi нет автоматической сборки мусора, в отличие от Java и C#. (но при этом если работаете с объектами через интерфейсы типа IXML Document; то здесь мусор весь собирается)
var a:X; - создается ссылка на объект типа Х;
a:=X.create; -размещаем объект в памяти.
Уничтожение объекта:
a.Free; – метод определенный в классе TObject, который уже вызывает деструктор класса Х, а потом вызывает менеджер динамической памяти и помечает память как освобожденную . (после этого желательно сделать a:=nil; )
Инициализация статических объектов:
В Си++ сделано хуже всего, мы не знаем в каком порядке и в какой момент будут инициализированы статические переменные (единственное что можно сказать что все их конструкторы будут до функции main … ), поэтому в конструкторах нельзя рассчитывать на какой-либо порядок инициализации.
В Java и C#:
Если речь идет о классе, то мы можем явным образом вызвать инициализацию.
Class X{
Static Y a = new Y ();
}
Java:
Class X{
Static int []a;
Static { a=new int [N];
For (int i=0, i<N , i++) a[i]=I;
}
Часто статические инициализации выводятся даже в другой поток, поэтому если вдруг там возникает исключение, то не понятно в какой момент времени оно придет в главный поток.
В Delphi:
Статических объектов вообще нет.
Но в общем случае unit в Delphi может иметь кроме Interface части, еще и Implementation часть , которая может быть поставлена в соответствие статическим переменным.
Деструкторов нет, но им может соответствовать блок finalization в части implementation (перед которым есть блок initialization).
В С++ нет сборки мусора, вызов конструктора и деструктора привязываются соответственно к инициализации и к удалению объекта.
Еще одна очень важная черта С++ это свертка стека, что гарантирует вызов деструкторов для локальных переменных, и утечка ресурсов происходит только при использовании динамической памяти.
Посмотрим, как обстоят дела в C#, Java.
В них есть сборка мусора, но идеального алгоритма сборки мусора не существует, и дела обстоят не так хорошо как казалось бы. Первый алгоритм сборки – счетчик ссылок, но проблема возникает при появлении кольцевых ссылок, поэтому используются более продвинутые алгоритмы (например, выбирается объект, который точно живой, потом смотрят все ссылки которые из него выходят и эти объекты тоже помечаются живыми и т.д.) Главная проблема этого алгоритма – неизвестен момент начала и если много объектов, то алгоритм занимает много времени. Но ссылки в процессе сборки мусора могут измениться, поэтому мы не можем отождествлять в этих языках ссылку с указателем. И сборщики мусора в этих языках включаются только когда начинает не хватать места.
В некоторых случаях нам нужно явное освобождение ресурсов. А языковые средства не позволяют это сделать. В Java protected void finalise(); вызывается при удалении. Во всех учебниках говорится, что надо предусмотреть свой метод close() if(!closed) close(); .
В C# позволяют писать деструкторы, но он работает не так, как мы от него ожидаем, потому что все равно вызывается функция Finalise().
Java:
c=new X();
Try{
<…>
Return;
}
Finaly{
<и этот блок будет выполнен в любом случае, как бы мы не вышли из предыдущего блока>
c.clear(); //очистка явных ресурсов
}
В языке C# пошли еще дальше. В нем ввели специальный интерфейс IDisposable(утилизируемы объекты). Метод void Dispose().