Опубликован: 05.07.2006 | Доступ: свободный | Студентов: 4679 / 885 | Оценка: 4.12 / 3.74 | Длительность: 18:59:00
Лекция 5:

Функции и структура программ

4.10. Рекурсия

В языке "C" функции могут использоваться рекурсивно; это означает, что функция может прямо или косвенно обращаться к себе самой. Традиционным примером является печать числа в виде строки символов. как мы уже ранее отмечали, цифры генерируются не в том порядке: цифры младших разрядов появляются раньше цифр из старших разрядов, но печататься они должны в обратном порядке.

Эту проблему можно решить двумя способами. Первый способ, которым мы воспользовались в "лекции №3" в функции itoa, заключается в запоминании цифр в некотором массиве по мере их поступления и последующем их печатании в обратном порядке. Первый вариант функции printd следует этой схеме.

printd(n)    /* print n in decimal */
int n;
{
  char s[10];
  int i;

  if (n < 0) {
     putchar('-');
     n = -n;
  }
  i = 0;
  do {
     s[i++] = n % 10 + '0'; /* get next char */
  } while ((n /= 10) > 0); /* discard it */
  while (--i >= 0)
     putchar(s[i]);
}

Альтернативой этому способу является рекурсивное решение, когда при каждом вызове функция printd сначала снова обращается к себе, чтобы скопировать лидирующие цифры, а затем печатает последнюю цифру.

printd(n)   /* print n in decimal (recursive)*/
int n;
 (
  int i;

   if (n < 0) {
     putchar('-');
     n = -n;
  }
  if ((i = n/10) != 0)
     printd(i);
  putchar(n % 10 + '0');
 )

Когда функция вызывает себя рекурсивно, при каждом обращении образуется новый набор всех автоматических переменных, совершенно не зависящий от предыдущего набора. Таким образом, в printd(123) первая функция printd имеет n = 123. Она передает 12 второй printd, а когда та возвращает управление ей, печатает 3. Точно так же вторая printd передает 1 третьей (которая эту единицу печатает), а затем печатает 2.

Рекурсия обычно не дает никакой экономии памяти, поскольку приходится где-то создавать стек для обрабатываемых значений. Не приводит она и к созданию более быстрых программ. Но рекурсивные программы более компактны, и они зачастую становятся более легкими для понимания и написания. Рекурсия особенно удобна при работе с рекурсивно определяемыми структурами данных, например, с деревьями; хороший пример будет приведен в "лекции №6" .

Упражнение 4-7

Приспособьте идеи, использованные в printd для рекурсивного написания itoa ; т.е. Преобразуйте целое в строку с помощью рекурсивной процедуры.

Упражнение 4-8

Напишите рекурсивный вариант функции reverse(s), которая располагает в обратном порядке строку s.

4.11. Препроцессор языка "C"

В языке "с" предусмотрены определеные расширения языка с помощью простого макропредпроцессора. одним из самых распространенных таких расширений, которое мы уже использовали, является конструкция #define; другим расширением является возможность включать во время компиляции содержимое других файлов.

4.11.1. Включение файлов

Для облегчения работы с наборами конструкций #define и описаний (среди прочих средств) в языке "с" предусмотрена возможность включения файлов. Любая строка вида

#include "filename"

заменяется содержимым файла с именем filename. (Кавычки обязательны). Часто одна или две строки такого вида появляются в начале каждого исходного файла, для того чтобы включить общие конструкции #define и описания extern для глобальных переменных. Допускается вложенность конструкций #include.

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

4.11.2. Макроподстановка

определение вида

#define tes 1

приводит к макроподстановке самого простого вида - замене имени на строку символов. Имена в #define имеют ту же самую форму, что и идентификаторы в "с"; заменяющий текст совершенно произволен. Нормально заменяющим текстом является остальная часть строки; длинное определение можно продолжить, поместив \ в конец продолжаемой строки. "Область действия" имени, определенного в #define, простирается от точки определения до конца исходного файла. Имена могут быть переопределены, и определения могут использовать определения, сделанные ранее. Внутри заключенных в кавычки строк подстановки не производятся, так что если, например, yes - определенное имя, то в printf("yes") не будет сделано никакой подстановки.

Так как реализация #define является частью работы макропредпроцессора, а не собственно компилятора, имеется очень мало грамматических ограничений на то, что может быть определено. Так, например, любители алгола могут объявить

#define then 
#define begin { 
#define end ;}

и затем написать

if (i > 0) then
   begin
           a = 1;
           b = 2
   end

Имеется также возможность определения макроса с аргументами, так что заменяющий текст будет зависеть от вида обращения к макросу. Определим, например, макрос с именем max следующим образом:

#define max(a, b) ((a) > (b) ? (a) : (b))

когда строка

x = max(p+q, r+s);

будет заменена строкой

x = ((p+q) > (r+s) ? (p+q) : (r+s));

Такая возможность обеспечивает " функцию максимума", которая расширяется в последовательный код, а не в обращение к функции. При правильном обращении с аргументами такой макрос будет работать с любыми типами данных ; здесь нет необходимости в различных видах max для данных разных типов, как это было бы с функциями.

Конечно, если вы тщательно рассмотрите приведенное выше расширение max, вы заметите определеные недостатки. Выражения вычисляются дважды; это плохо, если они влекут за собой побочные эффекты, вызванные, например, обращениями к функциям или использованием операций увеличения. Нужно позаботиться о правильном использовании круглых скобок, чтобы гарантировать сохранение требуемого порядка вычислений. (Рассмотрите макрос

#define square(x) x * x

при обращении к ней, как square(z+1). Здесь возникают даже некоторые чисто лексические проблемы: между именем макро и левой круглой скобкой, открывающей список ее аргументов, не должно быть никаких пробелов.

Тем не менее аппарат макросов является весьма ценным. Один практический пример дает описываемая в "лекции №7" стандартная библиотека ввода-вывода, в которой getchar и putchar определены как макросы (очевидно putchar должна иметь аргумент ), что позволяет избежать затрат на обращение к функции при обработке каждого символа.

Другие возможности макропроцессора описаны в "приложении А" .

Упражнение 4-9

Определите макрос swap(x, y), который обменивает значениями два своих аргумента типа int. (В этом случае поможет блочная структура ).