Лекция 11 (лекции (2002)), страница 2
Описание файла
Файл "Лекция 11" внутри архива находится в папке "лекции (2002)". Документ из архива "лекции (2002)", который расположен в категории "". Всё это находится в предмете "языки программирования" из 7 семестр, которые можно найти в файловом архиве МГУ им. Ломоносова. Не смотря на прямую связь этого архива с МГУ им. Ломоносова, его также можно найти и в других разделах. .
Онлайн просмотр документа "Лекция 11"
Текст 2 страницы из документа "Лекция 11"
Int top;
…
void Push(int x) {..}
int Pop() {..}
};
Классический пример с доступом: пусть у нас есть какая-то переменная, например, в том же самом стеке в некоторых случаях бывает интересно узнать размер стека. Переменная top. Её значение как раз и даёт нам количество элементов в стеке (например, указывает на индекс первого свободного элемента в массиве). Значение top можно открыть top только для чтения, так как любое изменение её значения приводит к разрушению целостности структуры данных. Альтернативная и более хорошая ситуация- сделать, например, функцию “int GetSize();”, которая и возвращает размер. Вданном случае она венёт top и всё хорошо:
Class Stack {
Int body[50];
Int top;
…
void Push(int x) {..}
int Pop() {..}
int GetSize() {return top;}
};
Но просто для того, чтобы прочитать значение переменной мы каждый раз должны обращаться к функции, а с точки зрения современных архитектур любая операция перехода (а вызов функции- это переход) ломает конвеер комманд (В среднем на современных Пентиумах машинная команда выполняется 1,3 такта). И с точки зрения внутренней микропроизводительности, заменять обращение к переменной на вызов функции- это слишком накладно.
В Обероне-2 можно заводит обычные переменные:
X* INTEGER;
И переменные, доступные только на чтение:
X*- INTEGER;
И нам не надо писать специальную функцию, но это, понятно, частно решение.
Подход Страуструба: в С++ введено понятие inline-функции (возможность вставки функций была и в другхЯП). Это функция (так называемая “встраиваемая”), вместо которой компилятор подставляет её тело, следовательно, никаких операций перехода не будет.
inline int Stack::GetSize() {return top;}
При этом inline- это лишь подсказка компилятору, во-всех языках (Си, …) , причём не на уровне, возможно, языков, а на уровне компиляторов был соответствующий прагмат “inline”. Страуструб решил внести это не на уровень опций компилятора, а на уровень языка. При этом это лишь рекомендация компилятору, где возможно использовать решим inline функций.
Страустуб: для упрощения процесса, если функция-член описана внутри класса, то она inline. В некоторых случаях компилятор будет игнорировать это правило (например, если внутри есть циклы, то они всё-равно ломают конвеер команд).
Тут была поддержка разделения O,P и U (это была аксиома старых ЯП). В современных ЯП, таких как С# или Java дела обстоят несколько иначе: в определении необходимо указывать реализацию (сразу за прототипом метода). Только для абстрактные методов и классов (перед ними должно стоять ключевое слово abstract) мы можем не указывать реализации. Это связано с тем, что соответствующий интерфейс класса должен генерироваться средой программирования с помощью утилит типа javadoc, с помощью интегрированной среды и так далее. То есть один из основополагающих принцыпов современных ЯП- это то, что О и Р сливаются воедино.
Ещё одна интересная особенность языков с классовой структурой, а именно, то, что какие-то странные у нас получаются функции. Если бы мы писали методы Push и Pop на языке Ада или Модула-2 у нас бы появлялся один параметр, как правило первый, это, собственно тип данных стеки и которым мы имеем дело. Ада:
Type Stack is record
body: array (1..50) of integer;
top: integer := 1; //можем сразу инициализировать
End;
Procedure Push(s: inout Stack; x: integer);
Procedure Pop(s: inout Stack; x: out integer);
Pop на Аде нельзя описать в виде функции, так как у функции должны быть параметры только in, а тут первый параметр всегда стек, состояние которого при этом меняется (а то, что компилятор будет, как всегда передовать это в виде ссылки- это уже его проблемы), а у функции на языке Ада все параметры должны быть типа in все зависимости от способа передачи. Можно написать процедуру типа Pick, которая просто считывает первое значение, и её уже можно переделать в функцию. Но самое главное- это то, что первый параметр- это всегда переменная типа стек. В том же стиле мы бы написали стеки на языке Модула-2 и на языке Оберон, поскольку там есть модуль и модуль, как раз, соединяет вместе определение типа стек и соответствующие процедуры.
В языках с класами ситуация немножко другая, а именно, все функции-члены считаются локализованными внутри этого класса, и при этом каждой функции-члену неявно будет передаваться параметр, к которому, кстати, можно обращаться. В языке C++:
Class X {
void f() {…}
};
Любой функции-члену этого класса неявно передаётся параметр (указатель на объект этого класса):
X* this;
Откуда он берётся? Если, на языке Ада мы будем писать:
Uses Stacks;
s Stack;
Push(s,5); Pop(s,x);
Мы явным образом указываем объект этого класса
На языке C++ и иже с ними, мы бы написали так:
Stack S;
S.Push(…); x = S.Pop();
То есть функции мы вызываем всегда через вид имя_объекта.имя_функции-члена. И адрес этого объекта и передаётся в качестве этого неявного параметра. При этом, опять же, при написании функции-члена (int GetSize();) мы пишем просто return top;, а можно было написать return this->top, но писать это не нужно, посколько не только имена функций, но и имена любых членов (данные или функции) локализуются внутри самого класса и внутри тел функций-членов этого класса мы можем обращаться к этим именам непосредственно. Все имена членов класса непосредственно видимы внутри функций-членов этого класса и потенциально видимы извне этого класса. Внутри функций-членов мы можем писать Push и Pop без всякой квалификации, а извне мы обязаны уточнять через имя объекта. В результате функции получаются значительно более компактными. С другой стороны хорошо это или плохо. Вообще говоря, подобного рода нотация (классовая нотация) удобна с точки зрения программиста, но это вопрос вкуса. Но далее мы увидим, что так как в ООЯП есть понятие динамического связывания методов класса, то все языки основанные на такой парадигме класса, они, как раз, используют привязку только к одному объекту, и динамический вызов метода- это как раз вызов метода в зависимости от типа объекта, который стоит в левой части. (апример, “s.Draw”). Все рассматриваемые нами ООЯП используют динамическое связывание метода только по одному объекту. Страуструб обсуждал возможность мультиметодов, возможность динамического выбора по нескольким объектам, например, когда мы определяем операцию “+” для типов Т1 и Т2. Так вот, если мы хотим динамически привязать операцию “+”, то мы должны её привязать, соответственно, либо к типу Т1, либо к типу Т2. А вот динамический выбор на основе Т1 и Т2- это новая особенность и на С++ её смоделировать практически невозможно или, по крайней мере, очень тяжело. Как ариант- необходимо синтаксически расширять базис языка. А вот в Аде’95, где изначально (в Аде) не было привязки к отдельному объекту. Мы вполне можем перечислить здесь несколько объектов. Мы увидим, что в объектном расширении языка Ада (Ада’95 – это новый ЯП, снизу-вверх полностью совместимый с Адой’83), там как раз мульти методы достались нам практически за бесплатно.
То есть однозначно говорить какая схема лучше или хуже- нельзя, скорее всего это просто дело вкуса, но, тем не менее, как мы видим, современные ЯП так или иначе ориентируются на схему, которую ввёл Страуструб в языке С++, и которая, на самом деле, была скопирована с языка Симула’67, то есть члены-функции, члены-данные и привязка к одному объекту.
Другие ЯП. Общий синтаксис очень похож: ключевое слово class, далее имя соответствующего типа класса и далее мы описываем соответствующие члены, причём синтаксис, поскольку так или иначе он ориентирован на С++ (C# и Java), всё очень похоже. С той только разницей, как уже было подчёркнуто, О и Р слито в одно понятие класса. Есть, конечно, небольшая разница, даже чисто синтаксическая, например, и в С# и в Jave у нас не может речь идти о том, что this являетмся указателем на соответствующий класс, хотя естественно этот неявный параметр есть, но он является ссылкой (адрес) на объект этого класса. И функциям будет передоваться, соответственно “X this;”:
Class X {
Void f(); - ей передаётся X this;
};
В Delphi ситуация таже самая только синтаксис немножка другой:
Type Stack =
Class
Body: array [0..50] of integer;
Top: integer;
Procedure Push(x: integer);
Function Pop(): integer;
End;
В Delphi принцып разделения О, Р и U проведён полностью и мы вообще не имеем право писать реализацию функций Push и Pop внутри самого класса. Мы обязаны писать это только в разделе implementation [Delphi опирается на Турбо Паскаль, который сам опирался на объектный Паскаль- не тот, которым называют Delphi, а старый объектный Паскаль (70-е годы)]:
Procedure Stack.Push(x: integer);
… -self передаётся по умолчанию
Begin
…
End;
Похоже на запись на языке С++, но немножко другие правила квалификации, немножко другой синтаксис использования. Процедурам и функциям – членам класса на языке Delphi передаётся параметр self, который является ссылкой на объекты соответствующего класса, поскольку Delphi тоже ссылочный язык (все классы имеют ссылочную семантику). Это же слово применяется и в Java (эта терминалогия идёт от SmallTalk’a), так как оно более естественно, а Страуструп не стал вводить его (а ввёл this), так как программисты часто в ранних программах использовали это слово как идентификатор, а он хотел совместимости снизу-вверх языка С++ с языком Си (по крайней мере с объявлениями). Например, по тем же причинам, возбуждение исключения не raise (Delphi, Ada), a throw (оно позже перешло и в Java и в C#), так как оно уже было занято стандартном вызывом из библиотеки сигналов в системе Unix. А this программисты если и использовалииспользовали, то в основном в качестве идентификаторов переменных, а это не так страшно, как идентификатор типа или функции, так как переменные можно переименовать. This перешёл в С#. Но с точки зрения семантики смысл один и тот же. Более того, с точки зрения реализации первый компилятор С++ (а именно Си фран) был разработан именно как компилятор с языка С++ на стандартный Си. И все эти функции транслировались как обычные глобальные функции, которым приписано какое-то уникальное имя и в качестве первого параметра как раз указатель на объект этого класса.
Специфичность определения типов с помощью
классов от обычного определения типов
1)Функции-члены класса (которым неявно передаётся параметр this) привязывается к имени объекта и работают через это имя объекта:
S.Pop();
2)В чистых (они изначально проектировались как ОО языки, без всяких отсылок к старым ЯП, без требований совместимости) ООЯП, таких как С# и Java: в них программа- это последовательность определений классов (если забыть про раздельную трансляцию). А в Delphi и C++ смешенение различных парадигм программирования (классическая модульная структура и классовая структура и многие другие механизмы, которые остались от старых парадигм). В языке Java вообще отсутствует понятие глобальных переменных и глобальных функций. В C# кроме этого могут быть структуры и перечислимый тип.Следовательно, не существует глобальных переменных и глобальных функций. И поэтому в этих языках значительно шире, чем в языке С++ используется понятие статических членов классов- они не имеют привязки к самому объекту- похожи на глобальные переменные или функции, но только имя соответсвующей переменной или функции связано областью видимости с соответствующим классом.
Smalltalk: переменная класса- это статический член класса. А нестатические члены- это переменные экземпляра.
В С++ и других языках, основанных на нём, понятие переменной класса/экземпляра обобщено на произвольный объект, в том числе процедуры и функции. Пример:
class X {
static int x;
int y;
};
С точки зрения компилятора структура этого класса выглядит так: объект класса Х состоит из единственного поля:
int y
А откуда берётся х? х размещается один раз в памяти для всех экземпляров этого класса вне зависимости от существования элементов этого класса. Это как бы глобальная переменная х. Но в отличие от глобальной переменной она доступна только по общим правилам функций-членов:
X a;
a.x - нельзя на С# (можно на С++)
Альтернатива:
X::x - предпочтительнее (в Java или C# такая форма записи- единственная)
Java, C#: X.x – указываем явно, через имя класса, чтобы обратиться к этому элементу. Статическая переменная- это глобальная переменная, локализованная внутри класса. Тут класс начинает работать как просто некоторый модуль. Статическая функция- аналогично. Она соответствует глобальной функции, имя которой локализовано внутри класса. Им никакой указатель/ссылка на объект не передаётся (this, self). А обращение к ней через имя класса.
А с чего начинает работу программа в Java или С#? В Си/С++ у нас есть глобальная функция main(). Тут в одном из классов должна существовать статическая функция main с прототипом:
public class X {
public static int main(string [] args);
…
};
Имя программы совпадает с именем этого класса. Тут мы передаём в main просто аргументы командной строки, а количество элементов не надо передавать, так как у массива в C# и в Javе есть его свойство- длина (динамический атрибут любого массива). Без понятие статических функций не смогли бы написать main, так как теперь она существует вне зависимости от того, есть объекты класса Х или нет. В Java и C# статические члены классов используются значительно чаще, чем в С++.
Считается, что глобальные объекты вредны (с точки зрения стиля программирования)- они провоцируют ошибки, так как мы некое состояние объекта делаем доступным всем. Понятие класса уничтожают необходимость в глобальных переменных и функциях вообще (вместо них существуют статические функции и статические члены класса). Классический пример необходимости статических переменных- счётчик объектов (сколько элементов класса Х у нас есть в программе) - попробуйте реализовать это на Паскале с помощью традиционных методик. А в классовых языках мы заводим некоторую статическую переменную counter- это количество экземпляров. В начале она имеет значение 0, и в конструкторе каждого элемента увеличиваем её значение на единицу.
Или, например, ТД, элементы которого нельзя ничтожать, а отводить можно только в динамической памяти (В С# и Java все объекты классов заводятся только в динамической памяти, и мы их явно не уничтожаем, их уничтожает только сборщик мусора, а вот на Паскале такой ТД написать нельзя [к которому неприменима процедура uncheck deallocation]):
Class X {
Private:
X();
~X();
Public:
Static X* Make() {return new X;}
};
X* p = X::Make(); - единственный способ создать объект
X a; - ошибка, так как конструктор умолчания
(функция, которая всегда работает при
инициализации объекта класса) приватный и
объявление вне класса Х
delete p; - нельзя, так как деструктор (функция, которая
вызывается при разрушении объекта класса)
тоже приватный
А в процедуре Make можем делать new X, так как это функция-член класса Х и она имеет доступ ко всем членам этого класса.