В языке «C» static отражает не только постоянство, но и степень того, что можно назвать «приватностью». Внутренние статические объекты определены только внутри одной функции; внешние статические объекты (переменные или функции) определены только внутри того исходного файла, где они появляются, и их имена не вступают в конфликт с такими же именами переменных и функций из других файлов.
Внешние статические переменные и функции предоставляют способ организовывать данные и работающие с ними внутренние процедуры таким образом, что другие процедуры и данные не могут прийти с ними в конфликт даже по недоразумению. Например, функции getch и ungetch образуют «модуль» для ввода и возвращения символов; buf и bufp должны быть статическими, чтобы они не были доступны извне. Точно так же функции push, pop и clear формируют модуль обработки стека; var и sp тоже должны быть внешними статическими.
5.7. Регистровые переменные
Четвертый и последний класс памяти называется регистровым. Описание register указывает компилятору, что данная переменная будет часто использоваться. Когда это возможно, переменные, описанные как register, располагаются в машинных регистрах, что может привести к меньшим по размеру и более быстрым программам. Описание register выглядит как:
register int x;
register char c;
и т.д.; часть int может быть опущена. Описание register можно использовать только для автоматических переменных и формальных параметров функций. В этом последнем случае описания выглядят следующим образом:
f(c,n)
register int c,n;
{
register int i;
...
}
На практике возникают некоторые ограничения на регистровые переменные, отражающие реальные возможности имеющихся аппаратных средств. В регистры можно поместить только несколько переменных в каждой функции, причем только определенных типов. В случае превышения возможного числа или использования неразрешенных типов слово register игнорируется. Кроме того, невозможно извлечь адрес регистровой переменной (этот вопрос обсуждается в главе 6). Эти специфические ограничения варьируются от машины к машине. Так, например, на PDP-11 эффективными являются только первые три описания register в функции, а в качестве типов допускаются int, char или указатель.
Язык «C» не является языком с блочной структурой в смысле PL/1 или Алгола; в нем нельзя описывать одни функции внутри других.
Переменные же, с другой стороны, могут определяться по методу блочного структурирования. Описания переменных (включая инициализацию) могут следовать за левой фигурной скобкой, открывающей любой оператор, а не только за той, с которой начинается тело функции. Переменные, описанные таким образом, вытесняют любые переменные из внешних блоков, имеющие такие же имена, и остаются определенными до соответствующей правой фигурной скобки. Например, в
if (n > 0)
{
int i; // Определение «новой» пременной i
for (i = 0; i < n; i++)
...;
}
Областью действия переменной i является «истинная» ветвь if; это i никак не связано ни с какими другими i в программе.
Блочная структура влияет и на область действия внешних переменных. Если даны описания:
int x;
f()
{
double x;
...
}
то появление x внутри функции f относится к внутренней переменной типа double, а вне f – к внешней целой переменной. Это же справедливо в отношении имен формальных параметров:
int x;
f(double x)
{
...
}
Внутри функции f имя x относится к формальному параметру, а не к внешней переменной.
Мы до сих пор уже много раз упоминали инициализацию, но всегда мимоходом, среди других вопросов. Теперь, после того как мы обсудили различные классы памяти, мы в этом разделе просуммируем некоторые правила, относящиеся к инициализации.
Если явная инициализация отсутствует, то внешним и статическим переменным присваивается значение нуль; автоматические и регистровые переменные имеют в этом случае неопределенные значения (мусор).
Простые переменные (не массивы или структуры) можно инициализировать при их описании, добавляя вслед за именем знак равенства и константное выражение:
int x = 1;
char squote = '\'';
long day = 60 * 24; // Число минут в сутках
Для внешних и статических переменных инициализация выполняется только один раз, на этапе компиляции. Автоматические и регистровые переменные инициализируются каждый раз при входе в функцию или блок. В случае автоматических и регистровых переменных инициализатор не обязан быть константой: на самом деле он может быть любым значимым выражением, которое может включать определенные ранее величины и даже обращения к функциям.
Пример 5-6. Инициализация в программе бинарного поиска из главы 4 могла бы быть записана в виде:
binary(int x, int v[],int n)
{
int low = 0;
int high = n - 1;
int mid;
...
}
вместо
binary(int x, int v[],int n)
{
int low, high, mid;
...
low = 0;
high = n - 1;
...
}
По своему результату, инициализации автоматических переменных являются сокращенной записью операторов присваивания. Какую форму предпочесть – это, в основном, дело вкуса. Мы обычно используем явные присваивания, потому что инициализация в описаниях менее заметна. Автоматические массивы не могут быть инициализированы. Внешние и статические массивы можно инициализировать, помещая вслед за описанием заключенный в фигурные скобки список начальных значений, разделенных запятыми.
Пример 5-7. Программа подсчета символов (из главы 2), которая начиналась с:
main() // Подсчет цифр, пробелов и др.
{
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() // Подсчет цифр, пробелов и др.
{
int c, i;
...
}
Эти инициализации фактически не нужны, так как все присваиваемые значения равны нулю, но хороший стиль – сделать их явными. Если количество начальных значений меньше, чем указанный размер массива, то остальные элементы заполняются нулями. Перечисление слишком большого числа начальных значений является ошибкой. К сожалению, не предусмотрена возможность указания на то, что некоторое начальное значение повторяется, и нельзя инициализировать элемент в середине массива без перечисления всех предыдущих.
Для символьных массивов существует специальный способ инициализации; вместо фигурных скобок и запятых можно использовать строку:
char pattern[] = "the";
Это сокращение более длинной, но эквивалентной записи:
char pattern[] = { 't', 'h', 'e', '\0' };
Если размер массива любого типа опущен, то компилятор определяет его длину, подсчитывая число начальных значений. В этом конкретном случае размер равен четырем (три символа плюс символ «окончание строки» – \0).
В языке «C» функции могут использоваться рекурсивно; это означает, что функция может прямо или косвенно обращаться к себе самой. Традиционным примером является печать числа в виде строки символов. Как мы уже ранее отмечали, цифры генерируются не в том порядке: цифры младших разрядов появляются раньше цифр из старших разрядов, но печататься они должны в обратном порядке. Эту проблему можно решить двумя способами.
Пример 5-8. Первый способ, которым мы воспользовались в главе 4 в функции itoa, заключается в запоминании цифр в некотором массиве по мере их поступления и последующем их печатании в обратном порядке. Первый вариант функции printd следует этой схеме.
void printd(int n) // Печать n в десятичном виде
{
char s[10];
int i;
if (n < 0)
{
putchar('-');
n = -n;
}
i = 0;
do
{
s[i++] = n % 10 + '0'; // Взять следующий символ
}
while ((n /= 10) > 0); // Отбраковать его
while (--i >= 0)
putchar(s[i]);
}
Пример 5-9. Альтернативой этому способу является рекурсивное решение, когда при каждом вызове функция printd сначала снова обращается к себе, чтобы скопировать лидирующие цифры, а затем печатает последнюю цифру.
void printd(int n) // Печать 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.
Рекурсия обычно не дает никакой экономии памяти, поскольку приходится где-то создавать стек для обрабатываемых значений. Не приводит она и к созданию более быстрых программ. Но рекурсивные программы более компактны, и они зачастую становятся более легкими для понимания и написания. Рекурсия особенно удобна при работе с рекурсивно определяемыми структурами данных, например, с деревьями; хороший пример будет приведен в главе 7.
Упражнение 5-7. Приспособьте идеи, использованные в printd для рекурсивного написания itoa; т.е. преобразуйте целое в строку с помощью рекурсивной процедуры.
Упражнение 5-8. Напишите рекурсивный вариант функции reverse(s), которая располагает в обратном порядке строку s.
В языке «С» предусмотрены определенные расширения языка с помощью простого макропредпроцессора. одним из самых распространенных таких расширений, которое мы уже использовали, является конструкция #DEFINE; другим расширением является возможность включать во время компиляции содержимое других файлов.
5.11.1. Включение файлов. Для облегчения работы с наборами конструкций #DEFINE и описаний (среди прочих средств) в языке «С» предусмотрена возможность включения файлов. Любая строка вида: