Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции_СП_2004_1_00.doc
Скачиваний:
69
Добавлен:
04.11.2018
Размер:
882.69 Кб
Скачать

3. Рекурсивные вызовы функций. Реализация рекурсивных алгоритмов

Всякая функция в языке Си имеет реентерабельный (повторно входимый) код, что позволяет ей обращаться к самой себе непосредственно или через другие функции. Такие обращения называются рекурсивными вызовами или рекурсией. При каждом очередном рекурсивном вызове создается новая копия параметров функции, а также определенных в ее теле автоматических и регистровых переменных. Внешние и статические переменные, имеющие глобальное время существования, сохраняют при этом свои прежние значения и размещение памяти. Несмотря на то, что ни стандарт языка Си, ни компилятор IBM C/2 формально не налагают никакого ограничения на количество рекурсивных обращений, тем не менее оно практически всегда существует для любых типов ЭВМ, ибо каждый новый вызов требует дополнительной памяти из ресурса программного стека. Если количество вызовов излишне велико, возникает переполнение сегмента стека и операционная система уже не может создать очередного экземпляра локальных объектов функции, что ведет, как правило, к аварийному завершению программы. В качестве примера реализации рекурсивного алгоритма рассмотрим функцию printd(), печатающую целое число в виде последовательности символов ASCII (т. е. цифр, образующих запись этого числа):

printd(num)

int num; /* Печатаемое число */

{ int i;

if (num < 0)

{ putchar('-'); num = -num; }

if ((i = num/10) != 0)

printd(i); /* Рекурсивный вызов функции */

putchar(num % 10 + '0')

}

Если значение переменной value равно 123, то в случае вызова

printd(value);

эта функция дважды обратится сама к себе для печати цифр заданного числа.

4. Препроцессор языка Си

Препроцессор языка Си предназначен для внесения изменений в исходный текст программы непосредственно перед ее компиляцией, а также для установки значений параметров, используемых компилятором. Обращение к препроцессору осуществляется при помощи набора директив, начинающихся с символа номера (#), и позволяет добиться изящества и мобильности Си-программ. В этом параграфе рассматриваются некоторые наиболее важные и часто используемые директивы препроцессора, работающего в составе компилятора IBM C/2.

4.1. Директива #define

Эта директива, позволяющая присваивать текстовым строкам символические имена, может иметь один из следующих форматов:

#define identifier <text>

или

#define identifier(parameter-list) <text>

где угловыми скобками (<>) обозначена необязательная часть конструкций. Первая форма интерпретируется препроцессором как простая литеральная подстановка, выполняемая перед фактическим обращением к компилятору. Вторая же из них служит для создания макроопределений, семантически подобных определению обычных функций. Здесь identifier есть произвольное, правильное в смысле грамматики языка, имя (см. Лекцию 1, $ 3), а text представляет собой строку символов, замещающую всякое вхождение этого имени в исходный текст программы. Заметим, что такая замена выполняется лишь для отдельно стоящих имен и не имеет места в тех случаях, когда identifier входит в состав текстовых строк или является составной частью более длинного идентификатора. Список аргументов parameter-list включает в себя один или большее число формальных параметров, разделенных запятыми и имеющих уникальные имена. В этом случае препроцессор заменяет имена параметров, входящие в text, именами соответствующих фактических аргументов, и лишь после этого будет выполнена реальная подстановка текста на место идентификатора. Символьная строка text может иметь произвольную длину, возможно даже превосходящую длину одной строки экрана. Для формирования длинного текста необходимо набрать символ \, нажать клавишу Enter и продолжить ввод символов с начала новой экранной строки. С другой стороны, параметр text может вообще отсутствовать в составе директивы, что соответствует определению идентификатора identifier без присвоения ему какого-либо конкретного значения. Приведем несколько примеров использования директивы #define для задания имен текстовых цепочек и формирования макроопределений функций.

1. Наиболее характерным применением этой директивы является назначение символических имен констант:

#define WIDTH 100

#def1ne LENGTH (WIDTH + 30)

Здесь идентификатор WIDTH объявлен числовой константой, равной 100, а имя LENGTH определено при помощи WIDTH и целого числа 30, причем круглые скобки во втором случае являются существенными.

2. Программисты, желающие видеть свою программу синтаксически поможет, например, на исходный текст программы на языке Алгол-60, могут воспользоваться директивой #define для замены ключевых слов и специальных символов, принятых в языке Си:

#define begin {

#define end ;}

#define integer int

#define real float

#define then

В этом примере последняя инструкция определяет идентификатор then, не присваивая ему никакого значения. Эффектом такого определения является простое извлечение из текста программы всех вхождений имени then, что позволяет записать условный оператор в таком виде

if (a >= b) then

begin

max = a;

min = b;

end

не нарушая тем самым синтаксических правил языка Си.

3. Вместо определения функции abs(x), вычисляющей абсолютную величину переменной x, в программе можно иметь семантически эквивалентное ему макроопределение вида

#define abs(x) ((x) >= 0) ? (x) : (-x)

обращение к которому внешне ничем не будет отличаться от обращения к обычной функции. Директива #define может встречаться в произвольном месте файла, содержащего исходный текст Си-программы, однако обычно их принято помещать в начало этого файла.

4.2. Директива #undef

Данная директива имеет формат

#undef identifier

и заставляет препроцессор игнорировать все последующие вхождения определенного ранее в инструкции #define имени identifier. Это позволяет ограничить область исходной программы, в пределах которой identifier обрабатывается препроцессором и имеет специальное значение. В следующем примере область действия идентификаторов WIDTH и ADD ограничена лишь частью исходного файла, в пределах которой они принимаются во внимание препроцессором языка Си:

#define WIDTH 100

#define ADD(x, y) (x) + (y)

.............................

.............................

.............................

#undef WIDTH

#undef ADD

.............................

.............................

Инструкция #undef может быть помещена в произвольное место файла, содержащего исходный текст программы.

4.3. Директива #include

Формат этой директивы в общем случае определяется следующей схемой:

#include "pathname"

или

#include <pathname>

где угловые скобки (<>) являются составной частью конструкции. Здесь pathname есть правильное имя файла в смысле операционной системы MS DOS. Директива #include позволяет включать текст файла с именем pathname в состав файла, содержащего обращение к этой директиве. Если pathname задает полное имя файла от корневого каталога, то две приведенные выше формы записи директивы эквивалентны. В том же случае, когда задано относительное имя файла, использование двойных кавычек (" ") заставляет препроцессор прежде всего искать требуемый файл в текущем каталоге, затем просматривать каталог, определенный в команде вызова компилятора и, наконец, в случае необходимости продолжить поиск в стандартном каталоге. Однако при заключении имени файла в угловые скобки текущий каталог не просматривается. Препроцессор допускает последовательные вложения инструкций #include максимально до десяти уровней. Это означает, что всякий файл, подключаемый с помощью этой директивы, может содержать внутри себя новое обращение к #include. В составе исходного текста программы директива #include может размещаться в произвольном месте содержащего ее файла.