М. Бен-Ари - Языки программирования. Практический сравнительный анализ (2000) (1160781), страница 19
Текст из файла (страница 19)
6 ...
goto Label
Проблема, конечно, в том, что присваивание значения метке-переменной могло быть сделано за миллионы команд до того, как выполняется goto, и программы в таких случаях отлаживать и верифицировать практически невозможно.
Хотя в других языках не существует назначаемого goto, такую конструкцию совсем просто смоделировать, определяя много небольших подпрограмм и манипулируя передачами указателей на эти подпрограммы. Соотносить конкретный вызов с присвоением указателю значения, связывающего его с той или иной подпрограммой, достаточно трудно. Поэтому указатели на подпрограммы следует применять лишь в случаях хорошей структурированности, например в таблицах указателей на функции в интерпретаторах или в механизмах обратного вызова.
6.8. Упражнения
1. Реализует ваш компилятор все case-/switch-операторы одинаково, или он пытается выбирать оптимальную реализацию для каждого оператора?
2. Смоделируйте оператор repeat языка Pascal в Ada и С.
3. Первоначально в языке Fortran было определено, что цикл выполняется, по крайней мере, один раз, даже если значение low больше, чем значение high! Чем могло быть мотивировано такое решение?
4. Последовательный поиск в языке С:
C |
i++;
можно записать как
C |
; /* Пустой оператор */
В чем различие между двумя вариантами вычислений?
5. Предположим, что в языке Ada переменная индекса может существовать за рамками цикла. Покажите, как бы это воздействовало на оптимизацию цикла.
6. Сравните сгенерированный для поиска код, реализованный с помощью операторов break или exit, с кодом, сгенерированным для поиска с «часовым».
7. Напишите программу поиска с «часовым», используя do-while вместо while. Будет ли это эффективнее?
8. Почему мы помещали «часового» в начало массива, а не в конец?
9. (Шолтен) В игре Го используют камни двух цветов, черные и белые. Предположим, что у вас в коробке неизвестная смесь камней, и вы выполняете следующий алгоритм:
while Stones_Left_in_Can loop -- пока есть камни в коробке
Ada |
if Color(S1 )=Color(S2) then
Add_Black_Stone; --добавить черный камень
else
Add_White_Stone; -- добавить белый камень
end if;
end loop;
Найдите переменную, значение которой уменьшается, оставаясь неотрицательным, и тем самым покажите, что цикл заканчивается. Можете ли вы что-нибудь сказать относительно цвета последнего камня? (Подсказка: напишите инвариант цикла для числа белых камней).
Глава 7
Подпрограммы
7.1. Подпрограммы: процедуры и функции
Подпрограмма — это сегмент программы, к которому можно обратиться из любого места внутри программы. Подпрограммы используются по разным причинам:
• Сегмент программы, который должен выполняться на разных стадиях вычисления, может быть написан один раз в виде подпрограммы, а затем многократно выполняться. Это экономит память и позволяет избежать ошибок, возможных при копировании кода с одного места на другое.
• Подпрограмма — это логическая единица декомпозиции программы. Даже если сегмент выполняется только один раз, полезно оформить его в виде подпрограммы с целью тестирования, документирования и улучшения читаемости программы.
• Подпрограмму также можно использовать как физическую единицу декомпозиции программы, т. е. как единицу компиляции. В языке Fortran подпрограмма (subroutine) — это единственная единица и декомпозиции, и компиляции. В современных языках физической единицей декомпозиции является модуль, представляющий собой группу объявлений и подпрограмм (см. гл. 13).
Подпрограмма состоит из:
• объявления, которое задает интерфейс с подпрограммой; это объявление включает имя подпрограммы, список параметров (если есть) и тип возвращаемого значения (если есть);
• локальных объявлений, которые действуют только внутри тела подпрограммы;
• последовательности выполняемых операторов.
Локальные объявления и выполняемые операторы образуют тело подпрограммы.
Подпрограммы, которые возвращают значение, называются функциями (functions), а те, что не возвращают, — процедурами (procedures). Язык С не имеет отдельного синтаксиса для процедур; вместо этого следует написать функцию, которая возвращает тип void, т.е. тип без значения:
C |
void proc(int a, float b);
Такая функция имеет те же свойства, что и процедура в других языках, поэтому мы используем термин «процедура» и при обсуждении языка С.
Обращение к процедуре задается оператором вызова процедуры call. В языке Fortran он имеет специальный синтаксис:
C |
call proc(x,y)
тогда как в других языках просто пишется имя процедуры с фактическими параметрами:
C |
ргос(х.у);
Семантика вызова процедуры следующая: приостанавливается текущая последовательность команд; выполняется последовательность команд внутри тела процедуры; после завершения тела процедуры выполнение продолжается с первой команды, следующей за вызовом процедуры. Это описание игнорирует передачу параметров и их области действия, что будет объектом детального рассмотрения в следующих разделах.
Так как функция возвращает значение, объявление функции должно определять тип возвращаемого значения. В языке С тип функции задается в объявлении функции перед ее именем:
C |
int func(int a, float b);
тогда как в языке Ada используется другой синтаксис:
Ada |
function Func(A: Integer; В: Float) return Integer;
Вызов функции является не оператором, а элементом выражения:
C |
a = x + func(r,s) + y;
Тип результата функции не должен противоречить типу, ожидаемому в выражении. Обратите внимание, что в языке С во многих случаях делаются неявные преобразования типов, тогда как в Ada тип результата должен точно соответствовать контексту. По смыслу вызов функции аналогичен вызову процедуры: приостанавливается вычисление выражения; выполняются команды тела функции; затем возвращенное значение используется для продолжения вычисления выражения.
Термин «функция» фактически совершенно не соответствует тому контексту, в котором он употребляется в обычных языках программирования. В математике функция — всего лишь отображение одного набора значений на другой. Если использовать техническую терминологию, то математическая функция не имеет побочного эффекта, потому что ее «вычисление» прозрачно в точке, в которой делается «вызов». Если есть значение 3.6, и вы запрашиваете значение sin(3.6), то вы будете получать один и тот же результат всякий раз, когда в уравнении встретится эта функция. В программировании функция может выполнять произвольное вычисление, включая ввод-вывод или изменение глобальных структур данных:
int x,y,z;
C |
{
у = get(); /* Изменяет глобальную переменную */
return x*y; /* Значение зависит от глобальной переменной */
z = х + func(void) + у;
Если оптимизатор изменил порядок вычисления так, что х + у вычисляется перед вызовом функции, то получится другой результат, потому что функция изменяет значение у.
Поскольку все подпрограммы в С — функции, в программировании на языке С широко используются возвращаемые значения и в «невычислительных» случаях, например в подпрограммах ввода-вывода. Это допустимо при условии, что понятны возможные трудности, связанные с зависимостью от порядка и оптимизацией. Исследование языков программирования привело к разработке интереснейших языков, которые основаны на математически правильном понятии функции (см. гл. 16).
7.2. Параметры
В предыдущем разделе мы определили подпрограммы как сегменты кода, которые можно неоднократно вызывать. Практически всегда при вызове требуется выполнять код тела подпрограммы для новых данных. Способ повлиять на выполнение тела подпрограммы состоит в том, чтобы «передать» ей необходимые данные. Данные передаются подпрограмме в виде последовательности значений, называемых параметрами. Это понятие взято из математики, где для функции задается последовательность аргументов: sin (2piК). Есть два понятия, которые следует четко различать:
• Формальный параметр — это объявление, которое находится в объявлении подпрограммы. Вычисление в теле подпрограммы пишется в.терми-нах формальных параметров.
• Фактический параметр — это значение, которое вызывающая программа передает подпрограмме.
В следующем примере:
int i,,j;
char а;
void p(int a, char b)
C |
i = a + (int) b;
}
P(i,a);
P(i+j, 'x');
формальными параметрами подпрограммы р являются а и b, в то время как фактические параметры при первом вызове — это i и а, а при втором вызове — i + j и 'х'.
На этом примере можно отметить несколько важных моментов. Во-первых, так как фактические параметры являются значениями, то они могут быть константами или выражениями, а не только переменными. Даже когда переменная используется как параметр, на самом деле подразумевается «текущее значение, хранящееся в переменной». Во-вторых, пространство имен у разных подпрограмм разное. Тот факт, что первый формальный параметр называется а, не имеет отношения к остальной части программы, и этот параметр может быть переименован, при условии, конечно, что будут переименованы все вхождения формального параметра в теле подпрограммы. Переменная а, объявленная вне подпрограммы, полностью независима от переменной с таким же именем, объявленной внутри подпрограммы. В разделе 7.7 мы более подробно рассмотрим связь между переменными, объявленными в разных подпрограммах.
Установление соответствия параметров
Обычно фактические параметры при вызове подпрограммы только перечисляются, а соответствие их формальным параметрам определяется по позиции параметра:
Ada |
procedure Proc(First: Integer; Second: Character);
Proc(24, 'X');
Однако в языке Ada при вызове возможно использовать установление соответствия по имени, когда каждому фактическому параметру предшествует имя формального параметра. Следовать порядку объявления параметров при этом не обязательно:
Ada |
Обычно этот вариант используется вместе с параметрами по умолчанию, причем параметры, которые не написаны явно, получают значения по умолчанию, заданные в объявлении подпрограммы:
Ada |
Proc(Second => 'X');
Соответствие по имени и параметры по умолчанию обычно используются в командных языках операционных систем, где каждая команда может иметь множество параметров и обычно необходимо явно изменить только некоторые из них. Однако этот стиль программирования таит в себе ряд опасностей. Использование параметров по умолчанию может сделать программу трудной для чтения, потому что синтаксически отличающиеся обращения фактически вызывают одну и ту же подпрограмму. Соответствие по имени является проблематичным, потому что при этом зависимость объявления подпрограммы и вызовов оказывается более сильной, чем это обычно требуется. Если при вызовах библиотечных подпрограмм вы пользуетесь только позиционными параметрами, то вы могли бы купить библиотеку у конкурирующей фирмы и просто перекомпилировать или перекомпоновать программу:
Ada |
X:=Proc_1 (Y) + Proc_2(Z);