|
Функции и структура программ
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. (В этом случае поможет блочная структура ).