Смекни!
smekni.com

Методические указания к выполнению контрольных работ по дисциплине "Основы программирования" (стр. 25 из 40)

В языке «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 или указатель.

5.8. Блочная структура

Язык «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 относится к формальному параметру, а не к внешней переменной.

5.9. Инициализация

Мы до сих пор уже много раз упоминали инициализацию, но всегда мимоходом, среди других вопросов. Теперь, после того как мы обсудили различные классы памяти, мы в этом разделе просуммируем некоторые правила, относящиеся к инициализации.

Если явная инициализация отсутствует, то внешним и статическим переменным присваивается значение нуль; автоматические и регистровые переменные имеют в этом случае неопределенные значения (мусор).

Простые переменные (не массивы или структуры) можно инициализировать при их описании, добавляя вслед за именем знак равенства и константное выражение:

int x = 1;

char squote = '&bsol;'';

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

Если размер массива любого типа опущен, то компилятор определяет его длину, подсчитывая число начальных значений. В этом конкретном случае размер равен четырем (три символа плюс символ «окончание строки» – &bsol;0).

5.10. Рекурсия

В языке «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.

5.11. Препроцессор языка «C»

В языке «С» предусмотрены определенные расширения языка с помощью простого макропредпроцессора. одним из самых распространенных таких расширений, которое мы уже использовали, является конструкция #DEFINE; другим расширением является возможность включать во время компиляции содержимое других файлов.

5.11.1. Включение файлов. Для облегчения работы с наборами конструкций #DEFINE и описаний (среди прочих средств) в языке «С» предусмотрена возможность включения файлов. Любая строка вида: