Смекни!
smekni.com

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 = '&bsol;”;

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', '&bsol;0' };

Если размер массива любого типа опущен, то компилятор определяет его длину, подсчитывая число начальных значений. В этом конкретном случае размер равен четырем (три символа плюс конечное &bsol;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 имеют ту же самую форму, что и идентификаторы в “с”; заменяющий текст совершенно произволен. Нормально заменяющим текстом является остальная часть строки; длинное определение можно продолжить, поместив &bsol; в конец продолжаемой строки. “Область действия” имени, определенного в #DEFINE, простирается от точки определения до конца исходного файла. имена могут быть переопределены, и определения могут использовать определения, сделанные ранее. Внутри заключенных в кавычки строк подстановки не производятся, так что если, например, YES - определенное имя, то в PRINTF(“YES”) не будет сделано никакой подстановки.

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

#DEFINE THEN #DEFINE BEGIN { #DEFINE END ;}