лекция 14 (Языки программирования (лекции) (2008))
Описание файла
Файл "лекция 14" внутри архива находится в папке "Языки программирования (лекции) (2008)". Документ из архива "Языки программирования (лекции) (2008)", который расположен в категории "". Всё это находится в предмете "языки программирования" из 7 семестр, которые можно найти в файловом архиве МГУ им. Ломоносова. Не смотря на прямую связь этого архива с МГУ им. Ломоносова, его также можно найти и в других разделах. .
Онлайн просмотр документа "лекция 14"
Текст из документа "лекция 14"
Языки программирования.
Лекция 14.
Классы (продолжение).
Мы говорили о специальных функциях-членах. Многие привлекательные черты, обусловленные наличием классов, связаны именно с наличием специальных функций.
Си++ отличается от Си несколькими плюсами, причём двумя ():
-
Первый плюс - наличие механизма классов. Класс - языковая конструкция как тип и как контейнер для данных других типов, функций и т.п.
-
Второй плюс – наличие объектно-ориентированных свойств: наследования, динамического полморфизма и т.п.
В прошлый раз мы разобрали конструкторы (вызывается автоматически компилятором, когда объект создается) и деструкторы (вызывается автоматически компилятором, когда объект уходит). Конструктор решает проблемы инициализации, копирования и преобразования.
3).Оператор преобразования.
-
Преобразование - оператор небезопасный, явные преобразования никаких проблем никогда не вызывают:
a b // разных типов
a = T(b)// явное преобразование
Вспомним конструктор преобразования X(T), который делает соответственно из объекта Т объект класса Х.
T a;
X b;
пример1.
int i;
double d;
d = i;// трактуется, как d = (double)i; если i соответственно типа int, а d типа double
В Си++ есть понятие стандартных неявных преобразований, тут мы видим стандартное преобразование, которое определено в языке, и семантика которого подробно расписана. Аналогично в Си++ есть обратные преобразования – из double в int (может привести к потери информации).
пример2.
Vector(int) // int - количество элементов в векторе (см.предыдущую лекцию)
V v;
v = 5; //трактуется как неявное преобразование
Компилятор не находит никакого стандартного преобразования из int в vector, оно не может быть стандартным (встроенным в компилятор, поскольку vector принадлежит не языку, а стандартной библиотеке). Компилятор ищет пользовательское преобразование, которое может быть соответствующим конструктором Vector(int) - и он его находит (никакого осмысленного преобразования из int в vector нет, это преобразование случайно).
В общем случае эта конструкция не безопасна. Это решается специальным указанием explicit (явный), ставится перед соответствующим конструктором (Х(Т), Х(Т&), X(const T&)), это означает, что этот конструктор теряет свойства конструктора-преобразования. Он может вызываться только явно. По умолчанию этого указания нет и это плохо.
В Си++ появились соответствующие преобразования, потому что некоторые типы данных ввести в язык совершенно безопасным образом, и так, чтобы они не отличались от стандартных – это без неявных операций преобразований сделать не возможно. В большинстве случаев операторы преобразования нужны, когда есть устоявшийся тип данных, у которого есть набор операций, который частично пересекается с набором стандартных операций ЯП.
Примеры:
-
Тип данных complex
-
Тип данных множество
Наиболее частые преобразования – это преобразования из строк языка Си: char* (ASCIZ) в String и обратно.
-
Посмотрим, как ещё организованы преобразования.
Неявные преобразования задаваемы пользователем есть только в Си++ и в Си#. В Дельфи и Java есть неявное преобразование в строковой тип. В Java для базового типа Object есть преобразование toString, которое может быть переопределено для любого класса. Другие неявные преобразования, определяемые пользователем, отсутствуют. Как следствие в Java тип данных complex нельзя ввести таким же удобным способом как в Си++.
Конструкторы преобразования не могут служить универсальным средством преобразования, потому что они всегда из какого-то тд Т (стандартного или пользовательского) делают новый тд Х, тд который мы описываем и описываем его конструктор.
char* T -> String X
А для обратного преобразования мы уже можем не написать преобразование. Если объект Т не является классом, либо он является классом, который мы не можем модифицировать (библиотечный, без исходного текста). Мы можем использовать специальные функции, которые называются операторами преобразования.
В Си++:
class X {
operator type(); //возвращаемого тип нет, так как соответствующее имя типа само по себе говорит какой тип возвращается
//нет аргумента, точнее есть один – this
//обязательно не статическая функция-член
};
пример: для string – char*
class String {
operator char* ();
}
Стандартная ошибка начинающего программиста – наивная реализация тд String, когда возвращает char*, если речь идет о временном объекте (неявное преобразование), то компилятор запомнит char*, дальше она будет использоваться, а объекта уже нет. Так как по временные объекты уничтожаются не при выходе из блока (как было в проекте языка), как только объект не нужен. Рассмотри пример. Пусть есть некоторая функция void f(X); заметим, что Х передается по значению. Есть объект T t; мы хотим вызвать f(t) – формально типы параметра t и Х не совпадают. Но компилятор прежде, чем выдать сообщение об ошибке поищет соответствующий конструктор X(T). Если он его нашел то, надо создать временный объект, который передается по значению: f(X(T)). Где нужно уничтожать этот временный объект? f(t) здесь ; (до точки-с-запятой)
пример:
Пусть f(char* s);
String s;
f(s); = f((char*) s); //с помощью операторов преобразования
С помощью оператора-преобразования мы можем
1. преобразовывать наши новые классы в стандартные тд
2. можно сделать следующее:
Base – базовый класс,
Derived - выведен из Base,
Derived -> Base // такое преобразование у нас есть
Base -> Derived // конструктором быть задано не может, может быть задана оператором преобразования внутри класса Derived
В Си#:
В языке Cи# тоже есть операторы неявного преобразования. Существует ряд ограничений:
1. Конструкторы преобразования здесь отсутствуют.
2. static implicit/explicit operator тип1 (тип2 имя)
а) Операторы преобразования задаются в виде статических членов
б) класса:тип1 и тип2 не могут быть стандартными типами (+ типами из стандартной библиотеки).
в) наличие implicit/explicit
Если преобразование (Т1)Е; объявлено как explicit, то:
T1 x;
T2 y;
x = y; // ERROR, но если было бы implicit то разрешалось – был бы сгенерирован временный объект, и ссылка на него присваивалась х
x = (T1)y; // ok
г) explicit указан по умолчанию
4).Свойства.
Ещё одна языковая конструкция – свойства (property). Они наиболее ярко иллюстрируют дуализм операций и данных, иногда сказать, где данные, где операции – очень тяжело, все зависит от уровня абстракции. Свойства относятся только к классам. С точки зрения пользователя свойства проявляют себя как член-данное (переменная, нестатический или статический член данных):
X a;
a.x = e;
b = a.x;
С точки зрения программиста - это две функции: get() и set(value)
a.x = e; // a.setX(e);
b = a.x; // b = a.getX();
Иначе говоря, для каждого свойства определена пара функций – одна читает значение, другая его устанавливает.
Свойства удобны:
1. сложная семантика свойств не видна для пользователя
2. в случае интеграции компонент (сред программирования)
Свойства есть в языках Дельфи и Cи#.
-
Дельфи
В Дельфи обычный член выражается
имя: тип;
Свойство выражается:
property
имя: тип;
read считыватель; // может быть имя другого члена-данного, либо имя функции-члена
function rd:тип
write писатель; // может быть имя другого члена-данного, либо имя процедуры-члена
procedure wr(value:тип);
После этого всего может стоять слово default (о свойствах по умолчанию позже).
Функция считыватель - возвращает значение. Функция писатель - записывает. Зачем вводить дополнительные члены?
-
пусть у нас есть:
m_I:integer;
-
мы вводим свойство:
property i:integer;
read m_i;
write m_i;
Если какой-нибудь смысл в таком объявлении? В таком коде особого смысла нет. Но надо иметь ввиду, что либо писать, либо читатель может быть опущен. Тогда мы можем, например, m_I закрыть на запись и открыть на чтение, и такой подход (использование свойств) очень удобен.
Ситуация, когда переменная закрывается на запись, очень распространена. В Обероне (где практически нет никаких излишек) был введен специальный механизм экспорта для членов-данных:
RECORD
X*-:INTEGER; //Х – является вообще-то публичным членом записи, но доступен только на чтение, но не на запись
В Си++ механизма свойств нет, и никто не спешит его вводить, так как пара функций get() и set() вполне решает проблему. Но в других языках, например в Обероне, необходим специальный механизм позволяющий закрывать переменную на запись. Почему так происходит? Обычно, значение переменной используется часто, если мы используем функцию типа get() она соответственно будет вызываться очень часто. Эта функция в простейшем случае возвращает закрытую или приватную переменную. В архитектуре современных процессоров большую роль играет процесс конвейеризации, когда мы спараллеливаем выполнение многих операций. Отметим, что сейчас повышение быстродействия процессора происходит не за счет увеличения гигагерц, а в результате усовершенствования их внутренней структуры и распараллеливания операций. Любой вызов функции, как и любой нелокальный переход, полностью гробит весь конвейер. В языке Си++ это решается при помощи inline-функций. Так если функция описана внутри класса, то она по умолчанию считается inline. inline - это указание компилятору, что вызов функции можно заменить ее телом, и никаких накладных расходов не будет. В Обероне это понятие отсутствует, поэтому ввели специальное решение для частной проблемы.
Свойства по умолчанию:
Если после свойства стоит ключевое слово default, то это означает, что в случае, если мы не знаем какое свойство на нужно, то компилятор подставляет это свойство. Что означает – мы не знаем, какое свойство нам нужно? Рассмотрим ситуацию:
class X
property I:T,
read … //в данный момент нас конкретные считыватели и писатели не интересуют
write…//в данный момент нас конкретные считыватели и писатели не интересуют
default;
end;
пусть у нас
Х а;
Т j;
а := Х.create();
j := a; // у нас переменные разных типов данных, отметим что в таком контексте нам от Х требуются данные типа Т. Вообще-то в такой ситуации компилятор дожжен ругаться на несоответствие типов данных. Но данном случае, если Х является классом, и у класса Х есть свойства типа Т, и это свойство является свойством по умолчанию, все это приводит к тому, что компилятор вставляет следующий код:
j := a.i; // получается, что тип данных слева и справа
один и тот же – Т
Это похоже на то, что мы рассматривали ранее j := (T)a. Это очень похоже на неявные преобразования. Таким образом, создатели языка Дельфи предусмотрели возможность неявных преобразований. Из класса Х всегда можно преобразовать в другой класс У, если в соответствующем классе опишем свойство типа У по умолчанию. Чаще всего такой механизм неявных свойств используется, если, например, компонента содержит в себе какой-либо контейнер, то имеет смысл делать этот контейнер свойством по умолчанию. Так класс одновременно проявляет черты некоторого контейнера, но и еще какие-то дополнительные черты. Хотя еще раз отметим: мы стоим на позиции, что никаких неявных преобразований в Дельфи нет.
Вопрос о перекрытии стандартных операций. Во все языках (кроме Оберона) есть перекрытие имён (одному и тому же имени соответствует несколько объектов). Компилятор различает эти объекты из контекста.
5)Перекрытие операций(формально не является свойством класса)
Перекрытие имен – overload (не путать с заменой), это означает, что одному имени соответствует несколько объектов в одной и той же области видимости. Решение о связывании имени с конкретным объектом всегда принимает компилятор исходя из контекста, связывание всегда статическое. В рассматриваемых нами языках понятие перекрытия есть (кроме Оберона), оно относится только к именам подпрограмм. Если речь идет об имени типа, переменной, константы – то правило: в одной области видимости одному имени – одна сущность.
С функциями по-другому, имя функции обычно обозначает некоторую обобщенную операцию и этой операции может соответствовать несколько конкретных реализаций. Во всех языках есть полиморфные (многоформные) операции. Например, это операция +. (встроенные операции являются полиморфными). В большинстве случаев встроенные операции являются полиморфными, почему бы и программистам не разрешить создавать свои полиморфные операции?
Многие языки допускают перекрытие на имена функций и процедур, при этом требование такое: из контекста компилятор должен догадываться, о какой подпрограмме идет речь. Что такое контекст?
-
Для языков, основанных на понятии выражения (Си++), это только сам вызов функции ничего более (имя и параметры).
Стандартные операции выглядят так: operator(arg1, arg2);