Языки программирования, 18 лекция (от 02 ноября)
Материал из eSyr's wiki.
ЯП 02.11.06
Определение типов с помощью механизма классов.
Специальные функции:
Конструкторы
Деструкторы
Операторы преобразования
Свойства
Спец функ – с одной стороны функции, с другой у них есть некая семантика, компилятор умеет вызывать их сам в некоторых случаях. Конструкетор вызывается при создании объекта, деструкторы при уничтожении, операторы преобразования при необходимости преобр.
У С. Были задумки, что бы были спец функции, которые вызываются при входе и выходе из блока.
Конструктор
С++ с точки зрения наиболее богатый язык, потому что сущ три класса памяти:
Статические
Квазистатические
Динамические
В остальных языках – только динамические.
Есть автоматический по умолчанию – Х()
Конструктор копирования – Х(Х&), X(const X&)
конструктор преобразования – X(T), X(T&), X(const T&)
все остальные
В Аде ещё пытались решить проблему инициализации объпектов:
record
TOP: integer = 1
Body: array (1..N) of T
end;
Значение 1 присваивается при каждом размещении объекта в памяти.
В других модульных ЯП даже такой инициализации нет
В общем случае существуют Init и Destroy, и программист-пользователь не должен знать структуру типа данных, только вызывает Init и Destroy, но нет языкового механизма, который делал бы это принудительно. И прелесть конструкторов в том, что они вызываются автоматически.
Для статич объектов инициализация до main
Для квазистатич – в момент объявления
Для динамич – в момент размещ в памяти.
В джава-шарп-дельфи только в момент размещения. Мы явно вызываем конструктор:
x = new X(); x = new X(C);
Почему надо выделять конструктор-умолчание? Какая умолчательная семантика у этих конструкторов? Ведь мы можем считать, что мы их вызываем явно. На самом деле всё не совсем так. У каждого конструктора существует стандартная семантика, которую может сгенерировать компилятор – вызвать конструкторы всех базовых классов, затем конструкторы подобъектов; после того, как выполнены инициализаторы, выполняется тело конструктора. Поэтому конструктор умолчания – если мы явным образом не указали другое, мы вызываем его.
В С++ роль констр по умолчанию более широкая – при описании. Мы не можем написать X x(), потому что это прототип, хотя написать X x(параметры) – можно. Это синтаксическое недоразумение.
Генерируемый конструктор по умолчанию делает то же самое, что и указано ранее.
Если недоступен какого-то конструктора по умолчанию, то генерируется сообщ об ошибке
В общем случае, тело конструктора имеет вид (С++, шарп):
X (...) : список инициализации { ... }
class X: public Y {
X (...) : Y(0), z(1); {...}
Для констант и ссылок конструктор – единственный способ инициализации.
Базовый класс надо в С++ указывать явно, потому что их может быть несколько.
В шарп для базового класса есть ключевое слово base.
Если вызываются конструкторы подобъектов, нужно ли вызывать конструкторы подобъектов? Нет, их можно вызывать в списке инициалищаторов.
В Джаве нет списка инициализации, вместо этого мы конструируем его при объявлении: Y y = new Y();
Единственный контекст, когда мы должны явно вызывать конструктор явно – вызов конструктора базового класса: super(...). Если не пишем, вызывается super()
Копирование:
Два вида:
Deep copy – полное копирование структуры данных. Если там содержатся указатели, то можно ли говорить, что глубокое копирование обязательно? Нет. Только программист может дать ответ на этот вопрос. Можно сделать операцию Copy, но нельзя заставить её исользовать (с простых языках). В Аде есть некий способ, который позволяет заставить программиста выполнить операцию Copy. В С++ какой способ управления – мы можем переопределить смысл операции присваивания и конструктор копирования. С++ - единственный язык, который позволяет переопределить операцию присваивания.
Shallow copy - копируется только верхнии уровень, и если структура содерит ссылки на объекты, то это может быть неадекватно.
Во время выполнения конструкции
копирования два этапа:
1. Инициализация объектов
Копирование
В С++ делано так - нужно одновременно переопределять и операцию копирования, и операцию присваивания. Конструктор копир есть у каждого класса, и программист или определяет явно, или они генерироваются.
Еслди конструктор копирования отсутствуетЮ то конструктор копирования генерируется почленно.
Операция присваивания и копирования – разные операции. Люди, которого этого не понимают – страдают.
X a = b – инициализация
a = b – присваивание, единственны контекст, когда присваивание
Инициализация
Присваиваине – отслеживание присваивания, самого себя, очистка ресурсов
Все эти слова относятся только к С++
В остальных языках всё просто:
Присваивание – копирование ссылки.
Семантика присваивание – просто копирование ссылки.
В Джаве, если класс поддержит интерфейс clonable, то там есть операция clone(), которая выдаст копию объекта.
Преобразование типов
Если тип включается в тот, в который преобразовываетсЯ то может быть неявное преобразование
В Аде это поставлено на хорошую ногу.
Если типы разные, то неявны преобразования запрещены.
Явные преобразования разрешаются довольно широко.
Неявные преобр разрешаются между подтипами.
Вначвале неявные преобр были запрещены и в Си с классами. Но новые типы должны быть равномощными стандартным.
При переопределении операции (например, сложение), возникает гигантское количество необходимых вариантов (по 20 на каждую операцию).
/*педедыв*/
Возникает технологическая потребность неявного преобр их одного типа в другой. Она является критичность. Неявные преобр усложняют компилятор и порождают много тонких ошибок. Неслучайно создатели ЯП отвечают по разному. В С++ и шарп пользовательские неявные преобр разрешены, в Дельфи и джаве – запрещены. Потребность неявных преобр для некотороых типов явл критичной. Неслучайно в джаве появился метод ToString() у типа Object. И если операция плюс требует строкового типа, то неявно вызывает ToString(). Это ничто иное, как неявно преобразование. То же самое умеет и компилятор Delphi.
В С++ возникают некоторые проблемы. Каким образом можно пользователю давать возможность определять преобр? Как метод класса. Например, есть преобр из класса 1 в класс 2. И такое преобр может выполняться двумя путями: оперделить конструктор T2(T1) – конструктор преобразования.
Если X v(10), то всё нормально. Если v=5, то типы не совместимые, компилятор начинает искаьб преобразование, находит v(5) и вставляет неявное преобразование, которое определяет программист.
В книге С. Рассмотрен класс вектор,
у которого переопределяется операция индексирования и конструктор
создания с указанием дллины. При этом одновременно считается, что
определён конструктор преобразования из типа int. Такое поведение
неприемлемо.
В вариант С++ появилось ключевое слово explicit, которое говорит, что этот конструктор преобразования должен вызываться явно. Беда в том, что мы должны писать явно explicit.
Тем не менее, конструкторов преобразования недостаточно. Некоторые пробр не выполнить. Например, нельзя выполнить преоб из класса в стандартный ТД, из базового класса в производный. Его нельзя задать с помощью конструктора, его можно задать только с помощью специальных операторов преобр:
operatorT() {} - такая функция может быть только членом класа, и параметров у неё нет.
Когда компилятор встречает
a=b
он начинает искать преобразование, и находи т для оператора b преобразование в а, и вставляет это неявное преобр. Тут видна опасность преобразования:
Когда расм ТД String – одна из причин, в котором String можно описать как библоитечный – в С++ есть неогр неявные преобразования, есть преобр из const char * и обратно. Типичная ошибка – если написать неявный оператор для строки, то можно получить неко торые неприятности. Типичный пример: как проще всего... в строке есть поле char *, наивная реализация метода – возвращения char *, что неверно, ибо ссылка const char
- уже не будет валидна.
//пример добавить
В шарп немного другой синтаксис: существуют только операторы преобразования, причём разрешеныы проебразования только пользовательских типов.
OperatorT1
Сразу возникает вопрос, чьим членом должно быть соотв преобразование: решение – оператор должен быть статической функцией, и вид у неё operator T1(T2 x), при этом в языке C# два ключевых слова – explicit и implicit/ В первом случае говорится, что преобр только явное:
T1 t1;
T2 t2;
t1 = t2; - ошибкк, можно только t1=(T1)t2;
во втором – неявное. По умолчанию – explicit.
Последняя спецфункция – свойства
Идеология свойств – проявляет дуализм. С ТЗ польхователя – данные, с точки зрения реализации – операция set и поерация get. В совр ЯП считается хорошим тоном не экспортировать данные, только операции. Это сделано для чего -
С точки зреня совр ЯП данные должны быть представлены только в виде свойств.
В Обероне для экспорта только на чтение можно написать *- (дщля чтения-записи просто звёздочка).
Механизм get-set позволяет программисту-пользователю не затрагивать реализации.
В общем случае механизм свойств очень и очень гибкий.
Механизм свойств есть в двух языках – шарп и дельфи.
Class X {
private T y;
public T x { //фигурная скобка – речь идёт о свойстве
get() { ... return y; }
set() { ... y = value; }
}
как это выглядит внешне:
X a = new X();
T y;
a.y = y; - на самом деле вызывется метод set для свойства setx(y)
y = a.x – вызывается get
Достаточно простой и прозрачный синтаксис.
Запрет на чтение или запись делается ненаписанием соотв метода.
Дельфи:
type Y = class ...
property X:T
read указывается имя члена-данного либо прототип функции, функция обязана иметь фид function getX():T;
write указывается имя члена-данного либо прототип процедуры, которая обязана иметь dид procedure setX(value:T);
Z : Y;
a:T;
z.x:=a;
В отличие от шарп, имя произвольное.
Что интересного есть в дельфи – свойства по умолчанию. В конце может стоять слово default.
A:T; b:Y;
a:=b; - обычно надо генерировать сообщ об ошибке, но компилятор смотрит и видит свойство по умолчанию.
Для коллекций есть индексеры.