STATIC CHAR BUF[BUFSIZE]; /* BUFFER FOR UNGETCH */ STATIC INT BUFP=0; /*NEXT
FREE POSITION IN BUF */
GETCH() {...}
UNGETCH() {...}
то никакая другая функция не будет в состоянии обратиться к BUF и BUFP; фактически,
они не будут вступать в конфликт с такими же именами из других файлов той же самой
программы.
Статическая память, как внутренняя, так и внешняя, спе- цифицируется словом STATIC
, стоящим перед обычным описани- ем. Переменная является внешней, если она описана
вне какой бы то ни было функции, и внутренней, если она описана внутри некоторой
функции.
Нормально функции являются внешними объектами; их имена известны глобально. возможно,
однако, объявить функцию как STATIC ; тогда ее имя становится неизвестным вне файла,
в котором оно описано.
В языке "C" "STATIC" отражает не только постоянство, но и степень того, что можно
назвать "приватностью". Внутренние статические объекты определены только внутри
одной функции; внешние статические объекты /переменные или функции/ опреде- лены
только внутри того исходного файла, где они появляются, и их имена не вступают в
конфликт с такими же именами пере- менных и функций из других файлов.
Внешние статические переменные и функции предоставляют способ организовывать
данные и работающие с ними внутренние процедуры таким образом, что другие процедуры
и данные не могут прийти с ними в конфликт даже по недоразумению. Напри- мер, функции
GETCH и UNGETCH образуют "модуль" для ввода и возвращения символов; BUF и BUFP должны
быть статическими, чтобы они не были доступны извне. Точно так же функции PUSH,
POP и CLEAR формируют модуль обработки стека; VAR и SP тоже должны быть внешними
статическими.
4.7. Регистровые переменные
Четвертый и последний класс памяти называется регистро- вым. Описание REGISTER
указывает компилятору, что данная пе- ременная будет часто использоваться. Когда
это возможно, пе- ременные, описанные как REGISTER, располагаются в машинных регистрах,
что может привести к меньшим по размеру и более быстрым программам. Описание REGISTER
выглядит как
REGISTER INT X;
REGISTER CHAR C;
и т.д.; часть INT может быть опущена. Описание REGISTER мож- но использовать
только для автоматических переменных и фор- мальных параметров функций. В этом последнем
случае описания выглядят следующим образом:
F(C,N)
REGISTER INT C,N;
{
REGISTER INT I;
...
}
На практике возникают некоторые ограничения на регистро- вые переменные, отражающие
реальные возможности имеющихся аппаратных средств. В регистры можно поместить только
нес- колько переменных в каждой функции, причем только определен- ных типов. В случае
превышения возможного числа или исполь- зования неразрешенных типов слово REGISTER
игнорируется. Кроме того невозможно извлечь адрес регистровой переменной (этот вопрос
обсуждается в главе 5). Эти специфические огра- ничения варьируются от машины к
машине. Так, например, на PDP-11 эффективными являются только первые три описания
REGISTER в функции, а в качестве типов допускаются INT, CHAR или указатель.
4.8. Блочная структура
Язык "C" не является языком с блочной структурой в смыс- ле PL/1 или алгола;
в нем нельзя описывать одни функции внутри других.
Переменные же, с другой стороны, могут определяться по методу блочного структурирования.
Описания переменных (вклю- чая инициализацию) могут следовать за левой фигурной
скоб- кой,открывающей любой оператор, а не только за той, с кото- рой начинается
тело функции. Переменные, описанные таким об- разом, вытесняют любые переменные
из внешних блоков, имеющие такие же имена, и остаются определенными до соответствующей
правой фигурной скобки. Например в
IF (N > 0) {
INT I; /* DECLARE A NEW I */
FOR (I = 0; I < N; I++)
... }
Областью действия переменной I является "истинная" ветвь IF; это I никак не связано
ни с какими другими I в програм- ме.
Блочная структура влияет и на область действия внешних переменных. Если даны
описания
INT X;
F() {
DOUBLE X;
... }
То появление X внутри функции F относится к внутренней пере- менной типа DOUBLE,
а вне F - к внешней целой переменной. это же справедливо в отношении имен формальных
параметров:
INT X; F(X) DOUBLE X; {
... }
Внутри функции F имя X относится к формальному параметру, а не к внешней переменной.
4.9. Инициализация
Мы до сих пор уже много раз упоминали инициализацию, но всегда мимоходом , среди
других вопросов. Теперь, после того как мы обсудили различные классы памяти, мы
в этом разделе просуммируем некоторые правила, относящиеся к инициализации.
Если явная инициализация отсутствует, то внешним и ста- тическим переменным присваивается
значение нуль; автомати- ческие и регистровые переменные имеют в этом случае неопре-
деленные значения (мусор).
Простые переменные (не массивы или структуры) можно ини- циализировать при их
описании, добавляя вслед за именем знак равенства и константное выражение:
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, которая начиналась с 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).
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 является предпочтительным способом связи описаний в больших
программах. Этот способ гарантиру- ет, что все исходные файлы будут снабжены одинаковыми
опре- делениями и описаниями переменных, и, следовательно, исклю- чает особенно
неприятный сорт ошибок. Естественно, когда ка- кой-TO включаемый файл изменяется,
все зависящие от него файлы должны быть перекомпилированы.
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 для данных разных типов, как это было бы с функциями.
Конечно, если вы тщательно рассмотрите приведенное выше расширение MAX, вы заметите
определенные недостатки. Выраже- ния вычисляются дважды; это плохо, если они влекут
за собой побочные эффекты, вызванные, например, обращениями к функци- ям или использованием
операций увеличения. Нужно позаботить- ся о правильном использовании круглых скобок,
чтобы гаранти- ровать сохранение требуемого порядка вычислений. (Рассмотри- те макрос