ческие и регистровые переменные имеют в этом случае неопре-
деленные значения (мусор).
· 92 -
Простые переменные (не массивы или структуры) можно ини-
циализировать при их описании, добавляя вслед за именем знак
равенства и константное выражение:
INT X = 1;
CHAR SQUOTE = '\”;
LONG DAY = 60 * 24; /* MINUTES IN A DAY */
Для внешних и статических переменных инициализация выполня-
ется только один раз, на этапе компиляции. Автоматические и
регистровые переменные инициализируются каждый раз при входе
в функцию или блок.
В случае автоматических и регистровых переменных инициализа-
тор не обязан быть константой: на самом деле он может быть
любым значимым выражением, которое может включать определен-
ные ранее величины и даже обращения к функциям. Например,
инициализация в программе бинарного поиска из главы 3 могла
бы быть записана в виде
BINARY(X, V, N)
INT X, V[], N;
{
INT LOW = 0;
INT HIGH = N - 1;
INT MID;
...
}
вместо
BINARY(X, V, N)
INT X, V[], N;
{
INT LOW, HIGH, MID;
LOW = 0;
HIGH = N - 1;
...
}
По своему результату, инициализации автоматических перемен-
ных являются сокращенной записью операторов присваивания.
Какую форму предпочесть - в основном дело вкуса. мы обычно
используем явные присваивания, потому что инициализация в
описаниях менее заметна.
Автоматические массивы не могут быть инициализированы. Внеш-
ние и статические массивы можно инициализировать, помещая
вслед за описанием заключенный в фигурные скобки список на-
чальных значений, разделенных запятыми. Например программа
подсчета символов из главы 1, которая начиналась с
· 93 -
MAIN() /* COUNT DIGITS, WHITE SPACE, OTHERS */
(
INT C, I, NWHITE, NOTHER;
INT NDIGIT[10];
NWHITE = NOTHER = 0;
FOR (I = 0; I < 10; I++)
NDIGIT[I] = 0;
...
)
Ожет быть переписана в виде
INT NWHITE = 0;
INT NOTHER = 0;
INT NDIGIT[10] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
MAIN() /* COUNT DIGITS, WHITE SPACE, OTHERS */
(
INT C, I;
...
)
Эти инициализации фактически не нужны, так как все присваи-
ваемые значения равны нулю, но хороший стиль - сделать их
явными. Если количество начальных значений меньше, чем ука-
занный размер массива, то остальные элементы заполняются ну-
лями. Перечисление слишком большого числа начальных значений
является ошибкой. К сожалению, не предусмотрена возможность
указания, что некоторое начальное значение повторяется, и
нельзя инициализировать элемент в середине массива без пере-
числения всех предыдущих.
Для символьных массивов существует специальный способ
инициализации; вместо фигурных скобок и запятых можно ис-
пользовать строку:
CHAR PATTERN[] = “THE”;
Это сокращение более длинной, но эквивалентной записи:
CHAR PATTERN[] = { 'T', 'H', 'E', '\0' };
Если размер массива любого типа опущен, то компилятор опре-
деляет его длину, подсчитывая число начальных значений. В
этом конкретном случае размер равен четырем (три символа
плюс конечное \0).
· 94 -
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');
)
· 95 -
Когда функция вызывает себя рекурсивно, при каждом обра-
щении образуется новый набор всех автоматических переменных,
совершенно не зависящий от предыдущего набора. Таким обра-
зом, в 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 является предпочтительным способом
связи описаний в больших программах. Этот способ гарантиру-
ет, что все исходные файлы будут снабжены одинаковыми опре-
делениями и описаниями переменных, и, следовательно, исклю-
чает особенно неприятный сорт ошибок. Естественно, когда ка-
кой-TO включаемый файл изменяется, все зависящие от него
файлы должны быть перекомпилированы.
· 96 -
4.11.2. Макроподстановка
Определение вида
#DEFINE TES 1
приводит к макроподстановке самого простого вида - замене
имени на строку символов. Имена в #DEFINE имеют ту же самую
форму, что и идентификаторы в “с”; заменяющий текст совер-
шенно произволен. Нормально заменяющим текстом является ос-
тальная часть строки; длинное определение можно продолжить,
поместив \ в конец продолжаемой строки. “Область действия”
имени, определенного в #DEFINE, простирается от точки опре-
деления до конца исходного файла. имена могут быть переопре-
делены, и определения могут использовать определения, сде-
ланные ранее. Внутри заключенных в кавычки строк подстановки
не производятся, так что если, например, YES - определенное
имя, то в PRINTF(“YES”) не будет сделано никакой подстанов-
ки.
Так как реализация #DEFINE является частью работы
маKропредпроцессора, а не собственно компилятора, имеется
очень мало грамматических ограничений на то, что может быть
определено. Так, например, любители алгола могут объявить
#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 для данных разных типов, как это было
бы с функциями.
· 97 -
Конечно, если вы тщательно рассмотрите приведенное выше
расширение MAX, вы заметите определенные недостатки. Выраже-
ния вычисляются дважды; это плохо, если они влекут за собой
побочные эффекты, вызванные, например, обращениями к функци-
ям или использованием операций увеличения. Нужно позаботить-
ся о правильном использовании круглых скобок, чтобы гаранти-
ровать сохранение требуемого порядка вычислений. (Рассмотри-
те макрос
#DEFINE SQUARE(X) X * X
при обращении к ней, как SQUARE(Z+1)). Здесь возникают даже
некоторые чисто лексические проблемы: между именем макро и
левой круглой скобкой, открывающей список ее аргументов, не
должно быть никаких пробелов.
Тем не менее аппарат макросов является весьма ценным.
Один практический пример дает описываемая в главе 7 стандар-
тная библиотека ввода-вывода, в которой GETCHAR и PUTCHAR
определены как макросы (очевидно PUTCHAR должна иметь аргу-
мент), что позволяет избежать затрат на обращение к функции
при обработке каждого символа.
Другие возможности макропроцессора описаны в приложении
А.
Упражнение 4-9.
Определите макрос SWAP(X, Y), который обменивает значе-
ниями два своих аргумента типа INT. (В этом случае поможет
блочная структура).
· 98 -
5.Указатели и массивы
Указатель - это переменная, содержащая адрес другой пе-