лекции (2003) (Глазкова) (1160821), страница 18
Текст из файла (страница 18)
Получим t:=a[i]; a[i]=i; i:=t;
Результат тот же самый - a[i]:1,2,1; i=3;
Поменяем : swap(i,a[i])
Получим
t:=i;
i:=a[i];
a[i]:=t;
Теперь результат – a[i]:3,2,1; i=3.
На языке типа Алголе60, в котором единственный способ передачи выходных параметров - передача по имени, написать надежную процедуру swap невозможно.
Однако в некоторых интерпретируемых языках применяется способ передачи параметров по имени.
Как с точки зрения реализации выглядит передача параметров по имени?
Если мы передаем параметр по имени, то в записи активации хранится адрес специальной процедуры, которая вычисляет значение адреса этого параметра. Эта процедура называется thunk. Она каждый раз при обращении к параметру вычисляет его адрес. Процедура thunk всегда возвращает ссылку на соответствующий фактический параметр.
При каждом обращении к выходному параметру внутри процедуры происходит обращение к процедуре. Именно эта особенность сделала язык Алгол 60 очень не эффективным (в 7-10 раз менее эффективен, чем ассемблер).
Однако в динамических интерпретируемых ЯП этот способ применяется достаточно часто.
Обсудим теперь детали применения различных способов передачи параметров в разных ЯП.
В языке Ада программист только задает семантику (in, out, inout), а компилятор выбирает соответствующую реализацию.
Однако в некоторых случаях этот подход не работает: на разных компиляторах программа может работать по-разному.
Пример:
Пусть
P(...,X,...)
X:=0;
...
raize error;
Пусть значение параметра передаётся процедуре по ссылке. Внутри процедуры меняется значение параметра и после этого возбуждается исключительная ситуация. В этом случае происходит изменение и формального, и фактического параметра. Если же значение параметра передается по результату, то, поскольку копирование должно происходить только в эпилоге, изменение фактического параметра не происходит. Т.о., одна и та же программа может давать разные результаты (причем программа, несмотря на исключительную ситуацию, ошибочной не является).
В Ада95 принят следующий вариант: компилятор может выбирать только между передачей по значению и передачей по ссылке.
Интересно, что в языках C# и Java понятие указателя нет (только ссылки).
Пример.
Пусть есть функция на языке Java, и мы хотим менять значение ее параметра.
В С++ мы бы написали void f(int& i);
в Си - void f(int* i).
Что же делать в Java?
В Java есть простые переменные; а все остальные переменные (массивы, интерфейсы, классы) являются ссылочными. Для того, чтобы реализовать семантику out и inout, в Java и в C# придумали следующее (в C# введен специальный термин - boxing): для каждого простого ТД существует так называемый класс-обертка - Wropper.
Для этих классов существуют неявные преобразования (например, int <=> Int; double <=> Double).
Если мы хотим менять значение параметра, мы должны описать параметр не как объект целого типа, а как объект целочисленного класса:
void f(Int x)
{
x=0;// x=Int(0);
...
}
Заметим, что между константами и классами существует неявное преобразование
Int => const (обратного неявного преобразования нет).
Пример:
int a;
f(a); // компилятор вставляет код f(Int (a))
f(0) //ошибка, т.к. нельзя передавать ссылку на константу
В C# такой процесс тоже существует (только в пакете System). Этот процесс завертывания и развертывания примитивных значений в значения класса называется boxing.
В C# boxing также применяется при передаче структур. Понятие структуры отличается от понятия класса тем, что класс имеет ссылочную семантику, а структура - нет.
При передаче структур они всегда завертываются в объект соответствующего класса.
В C# вместо явного введения класса обертки вводится понятие ссылки (ref).
Если мы хотим передавать формальный параметр по ссылке, надо писать
void f(ref int x); //но передавать можно только простые переменные.
Есть еще ключевое слово out:
void g(ref out int x);
Из соображений надежности после описания все переменные получают неопределенные значения. Если переменная не имела никакого значения, а должна его получить, надо указать ref out.
Пример:
object о=5;
int x=(int)о;
Для единообразной работы с параметрами применяются неявные преобразования из целочисленных типов в класс object, обратные преобразования - явные.
Лекция 14
Списки параметров переменной длины.
ЯП делятся на две категории, в зависимости от того, считать или не считать списки параметров переменной длины критичной технологической потребностью.
Из всех рассматриваемых языков только Си/С++ и C# считают наличие списков параметров переменной длины критичной технологической потребностью.
Пример:
printf(string fint,…); scanf(…);
Зачем нужны списки параметров переменной длины, и почему они иногда рассматриваются как критичная технологическая потребность?
В языке Си семейство вызовов exec существует в двух формах: массив аргументов и список параметров переменной длины (но без него можно было бы обойтись).
Именно для ввода-вывода нужны списки параметров переменной длины.
Например, в языке Оберон списков параметров переменной длины нет, и поэтому там не очень удобно писать ввод-вывод. Кроме того, в Оберон необходима явная квалификация имени, и, следовательно, для каждого ТД надо писать свою процедуру вывода.
Пример вывода:
InOut.WriteString(“Cout=”);
InOut.WriteInt(i);
InOut.Writeln;
На языке Си это записывается так:
Printf (“Count=%d\n”, i);
Т.о., именно для написания компактного последовательного вывода и применяются списки параметров переменной длины.
Решение, которое применяется в языке Си, унаследовано языком С++ - это списки параметров переменной длины (vararg.h).
Общая идея: есть псевдоТД va_list (это макрос, который размещает необходимый набор переменных). Здесь компилятор языка Си явным образом пользуется механизмом передачи параметров через стек.
va_start – инициализирует объект типа va_list значением параметра.
Требование к функциям, имеющим списки параметров переменной длины: хотя бы один параметр должен быть (именно для инициализации).
va_next – по объекту типа va_list и некоторому ТД Т выбирает: нужное количество байт из стека и возвращает объект типа Т.
va_end –удаление объектов, размещенных va_start.
Заметим, что никакого способа узнать количество параметров у нас нет.
Ненадежность здесь двоякая:
-
мы можем передавать параметры одного типа, а извлекать другого (никакого контроля нет);
-
отсутствует механизм, который позволяет узнать число параметров (в противном случае эта схема не была бы такой гибкой).
Например, когда при входе в функцию printf, мы не знаем, какое количество параметров было передано. В зависимости от информации в первом параметре и происходит вызов va_next (который может вообще не произойти, если нет ни одной спецификации форматов).
В семействе exec число 0 в списке параметров означает окончание этого списка.
Как можно обойти проблему, связанную с параметрами переменной длины (поскольку сам механизм передачи параметров – это очевидная дырка в системе контроля типов)?
Первый способ – перегрузка операций.
Механизм перегрузки – одной и той же функции (с одним и тем же именем) может соответствовать несколько тел с различным числом параметров.
В современных ЯП наличие списков параметров переменной длины не считается критичной технологической потребностью, т.к. во многих случаях можно обойтись перегрузкой некоторых операторов.
Второй способ – ввести списки параметров переменной длины в стиле семейства exec.
Здесь аргументы (имя программы и аргументы командной строки) могут передаваться либо в виде списка параметров переменной длины, либо в виде массива.
Подобного рода подход (только более контролируемый) применен в языке C#. Поскольку программисты на С и С++ очень привыкли пользоваться стандартной библиотекой ввода-вывода, в C# появились более надежные списки переменной длины.
Но как тогда быть с контролем типов? Для контроля типов мы точно должны знать количество параметров и контролировать приведение типов.
Поскольку C# чисто объектно-ориентированный язык, каждый ТД происходит от ТД object. Даже примитивные ТД приводятся к ТД object.
В C# процесс приведения от структурного или примитивного ТД Т к ТД object называется boxing (упаковка) – это неявное преобразование; обратный процесс (приведение от ТД object к ТД Т) называется unboxing (распаковка) – это преобразование должно быть явным и контролируемым во время выполнения (динамическое приведение).
Для того, чтобы задать список параметров переменной длины, надо задать массив соответствующих аргументов (т.е. массив объектов типа object).
Преимущество компилятора языка C# в том, что он умеет сам упаковывать соответствующие массивы.
Пример.
void f (params object[] args)
Если стоит ключевое слово params, то при вызове f(1,2) компилятор из 1 и 2 делает объект типа int (т.е. превращает его в ТД object) и помещает их в массив объектов этого типа (автоматически).
Если ключевое слово params отсутствует, то при вызове f(1,2) компилятор выдаст ошибку.
Заметим, что во всех остальных ЯП, которые мы рассматриваем, списка параметров переменной длины нет, но зато во всех этих ЯП есть перегрузка процедур.
Перегрузка процедур.
Мы уже обсуждали, что ОД могут иметь имя. Среда ссылок в процедурах – это набор ассоциаций между именем и ОД (т.е. по имени объекта мы должны найти его объявление).
Аксиома, которой удовлетворяли все традиционные ЯП: в одной области видимости (т.е. в одной среде ссылок) одному имени не может быть сопоставлено несколько объявлений.
Все современные ЯП нарушают это правило, но только для имен процедур.
Т.е. подпрограммам на современных ЯП в одной области видимости может соответствовать несколько тел.
Содержательный смысл: когда речь идет об ОД или типах, наличие различных имен плохо, т.к. оно не способствует понятности программы.
Почему для имен процедур это требование разрешается нарушать?
В языках С и С++ базовые математические операции уже перекрыты для разных ТД. Именем операции при этом кодируется какой-то содержательный смысл, но этот содержательный смысл реализуется по-разному для разного рода объектов.
Например, для каждого объекта операция draw(…) – это отображение его на экране; однако методы реализации операции отображения draw(…) для разных фигур совершенно различны. Здесь лучше всего назвать соответствующую процедуру рисования одним именем draw(…), но они будут по-разному применяться в зависимости от типа аргумента.
Этот процесс называется перегрузкой процедур (перегрузкой операций) – overloading.
Каким же образом происходит разрешение перегрузки? Заметим, что перегрузка означает, что в одной и той же области видимости имени могут соответствовать различные объявления, и нам нужно найти соответствующую ассоциацию.
Полиморфизм («много форм») – у одного имени несколько различных форм (эти формы задаются своими объявлениями).
В зависимости от времени связывания имени с объявлением различают статический и динамический полиморфизм. Полиморфизм распространяется только на объекты процедур и функций.
Динамический полиморфизм (виртуальные методы) – это свойство объектно-ориентированных языков, но кроме этого в объектно-ориентированных языках есть еще и статический полиморфизм.
overloading – статический полиморфизм (перегрузка, перекрытие);
overriding – динамический полиморфизм (переопределение).
При статическом полиморфизме (при перегрузке) разрешение перегрузки происходит в процессе трансляции.
Здесь различают подход языка С++ и подход языка Ада (современные ЯП наследовали подход С++).
Возникает понятие – контекст разрешения перекрытий (т.е. по какой информации компилятор выбирает нужную форму имени).