Языки программирования, 17 лекция (от 31 ноября)
Материал из ESyr's Wiki pages.
ЯП
Лектор сказал: Лекций 7, 9 ноября не будет
п3. Определение типов классами.
Тип данных = Множество операций + множество значений.
До этого определяли структуры, определяли операции, и связывали это модулем. Но при этом ощущается некоторая избыточность. Например, описываем пакет Стеки, определяем там тип Стек. Уже дублирование имён. Ображение: Стеки.Стек. Класс – дуален – и структура, и обёртка.
Класс по определению предназначен исключительно для ТД.
Каким обращом множество операций связывается со множеством значений.
Основные языки: Си++, Си шарп, Джава, Дельфи.
Дельфи – экле..тичный язык, там можно работать и в классич парадигме, и в ООП.
По синтаксису Дельфи далеко отстоит от святой троицы.
Члены класса:
Члены-данные определяют структуру, множество значени
Функции (процедуры) – оперделяются множество операций
Синтаксис:
Сишный
class Name {
описание членов
};
Синтаксически похоже на описание структуры, класс без процедур – та же структура.
Описание членов:
ТД – описание переменных
ЧФ – прототип
Мы рассм класс как ТД.
Примеров несть тому числа.
Класс является и модулем. Внутри класса может описано статические члены.
Внутри класса могут быть влооженнвые опеределния типов. - по аналогии с модулем.
Отличия других ЯП от С++ - в Си шарп то же самое, только кроме классов могут быть перечислимые типы. То же самое в Джаве – опеределение членов-функций, вложенных классов.
В чём отличия Си шарп Джавы, от С++:
Кроме прототипов функцйий могут быть опеределния классов.
Class X {
vois f()int i;
void g() {}
}
Это некий синтаксический сахар. Здесь идёт речь об inline – функциях
Inline – функции – функции, тело которых может вставлять в месте вызова.
Похожи на макросы. Отличие от макросов -
Типичный пример inline-функции – максимум:
inline T Max(T x, T y) {return x<y ? y : x; }
и вместо max(a,b) будет подставлен a<b ? a : b;
Очень похоже на соотв define. В чём основная разница -
Если поытаться описать такой макрос:
#define max(x,y) ((x)<(y)) ? (y) : (x);
Скобки нужны, чтобы вместо x и y
Внешне похоже, Но инлайн предпочтительнее. В inline типы должны совпадать. При вызове инлайн будет сделано преобразование типов, если нужно, или выдана ошибка в терминах вызова. Дефайн при ошибке будет ругаться в терминах макроподжстановки.
Инлайн функции хороши для современных суперскалярны процессоров. Срывает конвейер операция перехода, а любой вызов функции – нелокальный переход, и это может серьёзно уронить производительность. Inline – программист рекоммендует компилятору провести подстановку тела функции. В случае виртуальных функции инлайнятся функции очень редко, ибо во время компиляции можно не знать, что делать. Ещё не инлайнятся циклы, ибо там есть переходы. И сущ быстродействия это не даст. Зачем это нужно было Страуструпу? Это было и в Си, но в каждом отдельном компиляторе для этого были свои прагмы, и Страуструпу не нравилось, что это не стандартизовано.
А зачем Страуструпу понадобилось повышать быстродействие подобного рода функции? Это связано с ООП, в частности, с появлением geyters-setters. Например, если мы хотим сделать переменную read-only, то делаем только getter: T getA() {return a; } И его хорошо бы инлайнить.
Впервые понятие класса появилось в Симуле, и С. Обратил внимание на её неэффективность, так как там подобные функции не инлайнились. И вместо того, чтобы решать проблему в частности, он решил её глобально.
И был введен некоторый синтаксический сахар. Чтобы вместо того, чтобы писать
class X {
T GetA();
};
innline T X::GetA() { return a; }
молжно просто определить функцию прямо внутри класса, и тогда она будет считаться inline. Это было сделано для того... Есть физическое разделение интерфейса и реализации (хедеры и цппшники). И опеределние класса помещается в хедеры, а определение фукнции в реализацию. В С++ есть принцип РОРИ.
Но для inline это не катит, ибо компилятор должен знать оперелеление inline-функции, поэтому их надо помещать в хедер. Поэтому для инлайн-функции С. Разрешил такую возможность.
В С++ реализовано РОРИ, а в Си шарп и Джаве – нет. Там если описали функцию-член, то обязаны указать её реализацию, только если это не чисто виртуальная функция. То, что это неудобно с точки зрения чтения, это да. Во время отладки это удобно. А во время чтения средства систем разработки позволяют вырезать интерфейс и смотреть на него. Сам язык не нагружается соотв понятиями. Это характерно для совр ЯП, которые проектируются с учётом того, что будут погружены в соотв среду.
Дельфи (идейно ближе всего к С++):
type X=class
объявление членов;
i:integer;
procedure P;
function f(k:integer):integer;
end;
Тут чётко РОРИ.
В объявлении (спецификации) класса только переменные и прототипы. Это в интерфейсной части. Правда, может быть и в реализации, но это уже внутренние классы, только для реализации.
Implementation
procedure X.P; begin ... end;
function X.F ... begin ... end;
Небольшие синтаксические отличия:
Мы пока не говорили, каким образом скрывается реализация. Во всех этих языках есть возможность возможность экспорта членов, для этого используется модификатор publiс, действие которого распротраняется на вс объявления после него. В Джаве-Си шарп спецификатор перед каждым объявлением. Различия в умолчаниях.
Си++: для классов по умолч прайвейт, в структурах – private
C#: для классов и структур – private
Java, Delphi: в Джаве есть пакетный, в Дельфи – для юнита – там доступ в перделах пакета-юнита по умолчанию. Сами классы могут объединяться в модули – и это модульный доступ.
Про доступ мы пока больше гововрить не буду.
Множество операций+множество значений
Множество операций – public функции-члены
Множество знач – Члены-данны, хорошим тоном счиатеется их скрытие.
Сосредосточимся неа деталях:
Синтаксис обращения.
Члены локализоканы внутри класса. Есть Класс Х, есть член х. Синтаксис доступа – Х.х. Этот синтаксис взят из синтаксиса записи. Этот синтаксис просто обобщён на класс. ФЧ отличаются тем, что им неявно передаётся ссылка/указатель на объект класса.
Claas X {
void f() {...}
};
E а будет неявный параметр – ссылка на объект. При вызове x.f(); юудет передана ссылка на х. И в теле функции будет определёна переменная:
C++ - указатель this
С# - ссылка this
Java, Delphi – сслка Self
почему Страуструп не использовал self, хотя оно использовалось в Smalltalk, который явл оперделяющим с тз терминологии: Вилдимо, у него что-то там конфликтовало. В Си шарп было взято по принципу чтобы не было похоже на Джаву.
К члену класса обращаемся снаружи только через имя объекта класса. Изнутри – можно через this/self, а можно просто через имя члена. Класс образует свою зону видимости – внутри членов класса можем обращаться к другим просто по имени. Класс образует свою область видимости. И если есть глобальное описание одноимённых переменных, то он и перекрываются. В Джаве и Си# глобальные переменны еотстусттвуют, но там есть вложенные пространства имён и та же проблема. Но this/self нужен, когда, например, класс передаёт ссылку на себя. В Си шарп this испоьлзуется в документации, когда
есть i, j как ячлен класса
void (int i) {int i; - нельзя, ибо имена формальных параметры как бы локализованы внутри тела функции
}
тело функции – вложенная по отношению класса область видимости, а что делать с объявлением в классе? Везде закрывается, кроме Delphi, в Delphi просто нельзя так делать, и это правильно. В остальных языках к члену может обратиться через this/
Попробуе мпровести параллель между определением в омдульных языках и в ОО.
Ада (Модула-2)
package M is
type T is ///;
procedure P(X:T);
procedure P;
X: Y;
end M;
package body M is
procedure G(x:T) is ... end G;
Z : Y;
ОО:
class T {
public:
void P();
а что делать с P без параметров? Он может обращаться к X, Z. Аналог – статические члены, им не передаётся никакой неявный параметр.
static void PP();
};
Как обращаться к PP? А как к P без параметров в Аду? Через имя модуля – T.P, в ОО – через имя класса – T.PP(). Но в С++, и за это язык ругают, можно ещё через объект класса – t.PP(), а через имя класса – T::PP().
Точка и два двоеточия не перегружаются.
Public:
Y X;
private
void G();
static Y Z;
};
Если брать модульный язык, где мы описываем отдельно стоящие параметры и переменные, то в ОО мы и описываем статически. Статические члены являются глобальными, но они локализованы в имени типа, и до них мы добираемся через имя класса. Класс – полный аналог модуля.
В Си шарп и Джаве употр статич членов очень часто, ибо это аналог глобальных членов и функции. У программиста на С++ есть возможность вынести подобные функции на глобаьлный уровень, а в Си шарп и Джаве эти понятия отсутствуют.
Зачем нужны статические данные:
Корнстанта.
Class Stack {
Int top;
T body[N];
const int N; - глупость, потому что будет для каждого своя, надо делать статик.
}
Статические функции нужны для того, чтобы работать со статическими данными.
Статические члены существуют вне зависимости от объектов класса.
Пример: пусть мы хотим располагать объекты класса только в динамической памяти (актуально только для С++). Можно ли это обеспечить? Легко. Оставить публичной одну функцию – static T * MakeT() {return new T(); } Если сделать все конструкторы приватными, то единственый споособ создать объект класса – вызвать эту функцию.
Статические члены есть во всех ОО-языках, так как некоторые ыещи можно проможедировать только ими.
Константы – часть объекта или класса?
В Смаллтоке есть пноятие класс, и есть понятие экземпляр. У класса есть методы, есть переменные. Переменные в См. Делились на переменные экземпляра, которые были для каждого экземпляра свои и переменнаяе класса, коотрые размещались в единственном числе. Осталось обобщить на функции – к нестатическим передаётся ссылка на объект.
Любая ли константа должна быть переменной класса? Не все. Одни принадлежат экземпляру, другие всему классу. В Джаве для констант экз есть final. Где они инициализируются? Есть кноструктор, который вызывается при создании класса - и там инициализируются константы, только один раз. В Си шарп – специали=ьный синтаксис конструктора – инициализирующая часть.
Где инициализируются статические данные – единственная возможность инициализировать статические данные – в cpp.
Class X {
static const int i; - обязаны инициализировать. В теле класса инициализацию сделать нельзя.
};
Вопрос размещения – из-за раздельной трансляции компилятор не знает, где размещается статические данные. Ибо он не знает, откуда взялся класс – из инклюда или из текста. Поэтому, если описана статическая переменная, программист должен указать её размещение.
Поэтому в цппшнике надо написать int X::i = 0; таким образом явно указали размещение. А если в хедере – то досявые-виндовые редакторы связей будут ругаться? Ld – выдадет ворнинг.
Аналогом размещения функции члена для фугкции – оперделение.
Пока мы не вызываем функции-не обращаемся к переменным, ошибки нет, как только обратимся – ошибку найдёт редактор связей.
Где будут инициализироваться статические члены –
в Си шарп можно гибко управлять порядком загрузкой модулей, поэтому можно предсказать порядок, в котором будут выполняться конструкторы, посему в инициализации можно писать что-то содержательное.
В Джаве есть статический блок инициализации:
class X {
static int i = exp; - немедленная инициализация справедлива и для Си шарп и дляДжавы
static { ... }
Одна из самых интересных созможностец – наличие понятие конструктора. Есть понятие специальная функция – про них есть дополнительная семантика, и компилятор знает, когда их вызывать. Наиболее популярная – конструктор, в Си-подобных языках синтаксис – имя функции совпадает с именем класса X().
Статические конструкторы – видно, что это специальная функцияю Когда модуль загружается в память, происходит вызов статических конструкторов (Си шарп-Джава), при этом сверху вниз выполняется инициализация, потом статик-конструкторы. Статический конструктор генерируется. И он обладает неукоторой семантикой.
Стандартная семантика – вызыыаются конеструкторы базовых классов, инициализация (конструкторы) подобъектов, после этого выаолняется тело конструктора. У статических конструторов то же самое – вызовет конструкторы базового класса, выполнены статические инициализаторы членов, и потом тело.