Керниган и Ритчи - Язык программирования Си (793773), страница 23
Текст из файла (страница 23)
Если же функцияпомечена словом static, то ее имя становится невидимым вне файла, в котором она определена.Объявление static можно использовать и для внутренних переменных. Как и автоматические переменные,внутренние статические переменные локальны в функциях, но в отличие от автоматических они не возникаюттолько на период работы функции, а существуют постоянно. Это значит, что внутренние статическиепеременные обеспечивают постоянное сохранение данных внутри функции.Упражнение 4.11. Модифицируйте функцию getop так, чтобы отпала необходимость в функции ungetch.Подсказка: используйте внутреннюю статическую переменную.4.7. Регистровые переменныеОбъявление register сообщает компилятору, что данная переменная будет интенсивно использоваться.Идея состоит в том, чтобы переменные, объявленные register, разместить на регистрах машины,благодаря чему программа, возможно, станет более короткой и быстрой.
Однако компилятор имеет правопроигнорировать это указание.Объявление register выглядит следующим образом:register int x;register char с;и т. д. Объявление register может применяться только к автоматическим переменным и к формальнымпараметрам функции. Для последних это выглядит так:f( register unsigned m, register long n){register int i;…}На практике существуют ограничения на регистровые переменные, что связано с возможностями аппаратуры.Располагаться в регистрах может лишь небольшое число переменных каждой функции, причем толькоопределенных типов.
Избыточные объявления register ни на что не влияют, так как игнорируются вотношении переменных, которым не хватило регистров или которые нельзй разместить на регистре. Крометого, применительно к регистровой переменной независимо от того, выделен на самом деле для нее регистрили нет, не определено понятие адреса (см. главу 5).
Конкретные ограничения на количество и типырегистровых переменных зависят от машины.4.8. Блочная структураПоскольку функции в Си нельзя определять внутри других функций, он не является языком, допускающимблочную структуру программы в том смысле, как это допускается в Паскале и подобных ему языках. Нопеременные внутри функций можно определять в блочно-структурной манере. Объявления переменных(вместе с инициализацией) разрешено помещать не только в начале функции, но и после любой левойфигурной скобки, открывающей составную инструкцию. Переменная, описанная таким способом, "затеняет"переменные с тем же именем, расположенные в объемлющих блоках, и существует вплоть досоответствующей правой фигурной скобки. Например, вif (n > 0) {int i; /* описание новой переменной i */for (i = 0; i < n; i++)…}областью видимости переменной i является ветвь if, выполняемая при n>0; и эта переменная никакогоотношения к любым i, расположенным вне данного блока, не имеет.
Автоматические переменные,объявленные и инициализируемые в блоке, инициализируются каждый раз при входе в блок. Переменныеstatic инициализируются только один раз при первом входе в блок.Автоматические переменные и формальные параметры также "затеняют" внешние переменные и функции стеми же именами. Например, вint x;int у;f(double x){double у;…}х внутри функции f рассматривается как параметр типа double, в то время как вне f это внешняяпеременная типа int. То же самое можно сказать и о переменной y.С точки зрения стиля программирования, лучше не пользоваться одними и теми же именами для разныхпеременных, поскольку слишком велика возможность путаницы и появления ошибок.4.9.
ИнициализацияМы уже много раз упоминали об инициализации, но всегда лишь по случаю, в ходе обсуждения другихвопросов. В этом параграфе мы суммируем все правила, определяющие инициализацию памяти различныхклассов.При отсутствии явной инициализации для внешних и статических переменных гарантируется их обнуление;автоматические и регистровые переменные имеют неопределенные начальные значения ("мусор").Скалярные переменные можно инициализировать в их определениях, помещая после имени знак = исоответствующее выражение:int x = 1;char squote = '\'';long day = 1000L * 60L * 60L * 24L; /* день в миллисекундах */Для внешних и статических переменных инициализирующие выражения должны быть константными, приэтом инициализация осуществляется только один раз до начала выполнения программы.
Инициализацияавтоматических и регистровых переменных выполняется каждый раз при входе в функцию или блок. Длятаких переменных инициализирующее выражение — не обязательно константное. Это может быть любоевыражение, использующее ранее определенные значения, включая даже и вызовы функций. Например, впрограмме бинарного поиска, описанной в параграфе 3.3, инициализацию можно записать так:int binsearch(int x, int v[], int n){int low = 0;int high = n - 1;int mid;а не так:int low, high, mid;low = 0;high = n - 1;В сущности, инициализация автоматической переменной — это более короткая запись инструкцииприсваивания.
Какая запись предпочтительнее — в большой степени дело вкуса. До сих пор мы пользовалисьглавным образом явными присваиваниями, поскольку инициализация в объявлениях менее заметна идальше отстоит от места использования переменной.Массив можно инициализировать в его определении с помощью заключенного в фигурные скобки спискаинициализаторов, разделенных запятыми. Например, чтобы инициализировать массив days, элементыкоторого суть количества дней в каждом месяце, можно написать:int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};Если размер массива не указан, то длину массива компилятор вычисляет по числу заданныхинициализаторов; в нашем случае их количество равно 12.Если количество инициализаторов меньше числа, указанного в определении длины массива, то для внешних,статических и автоматических переменных оставшиеся элементы будут нулевыми.
Задание слишкомбольшого числа инициализаторов считается ошибкой. В языке нет возможности ни задавать повторенияинициализатора, ни инициализовать средние элементы массива без задания всех предшествующих значений.Инициализация символьных массивов — особый случай: вместо конструкции с фигурными скобками изапятыми можно использовать строку символов. Например, возможна такая запись:char pattern[] = "ould";представляющая собой более короткий эквивалент записиchar pattern[] = {'о', 'u', 'l', 'd', '\0'};В данном случае размер массива равен пяти (четыре обычных символа и завершающий символ '\0').4.10.
РекурсияВ Си допускается рекурсивное обращение к функциям, т.е. функция может обращаться сама к себе, прямо иликосвенно. Рассмотрим печать числа в виде строки символов. Как мы упоминали ранее, цифры генерируются вобратном порядке — младшие цифры получаются раньше старших, а печататься они должны в правильнойпоследовательности.Проблему можно решить двумя способами. Первый — запомнить цифры в некотором массиве в том порядке,как они получались, а затем напечатать их в обратном порядке; так это и было сделано в функции itoa,рассмотренной в параграфе 3.6.
Второй способ — воспользоваться рекурсией, при которой printd сначалавызывает себя, чтобы напечатать все старшие цифры, и затем печатает последнюю младшую цифру. Этапрограмма, как и предыдущий ее вариант, при использовании самого большого по модулю отрицательногочисла работает неправильно.#include <stdio.h>/* printd: печатает n как целое десятичное число */void printd(int n){if (n < 0) {putchar('-');n = -n;}if (n / 10)printd(n / 10);putchar(n % 10 + '0');}Когда функция рекурсивно обращается сама к себе, каждое следующее обращение сопровождаетсяполучением ею нового полного набора автоматических переменных, независимых от предыдущих наборов.Так, в обращении printd(123) при первом вызове аргумент n = 123,при втором — printd получаетаргумент 12, при третьем вызове — значение 1.
Функция рrintd на третьем уровне вызова печатает 1 ивозвращается на второй уровень, после чего печатает цифру 2 и возвращается на первый уровень. Здесь онапечатает 3 и заканчивает работу.Следующий хороший пример рекурсии — это быстрая сортировка, предложенная Ч. А. Р. Хоаром в 1962 г. Длязаданного массива выбирается один элемент, который разбивает остальные элементы на два подмножества— те, что меньше, и те, что не меньше него.
Та же процедура рекурсивно применяется и к двум полученнымподмножествам. Если в подмножестве менее двух элементов, то сортировать нечего, и рекурсия завершается.Наша версия быстрой сортировки, разумеется, не самая быстрая среди всех возможных, но зато одна изсамых простых.
В качестве делящего элемента мы используем серединный элемент./* qsort: сортирует v[left]...v[right] по возрастанию */void qsort(int v[], int left, int right){int i, last;void swap(int v[], int i, int j);if (left >= right) /* ничего не делается, если */return; /* в массиве менее двух элементов */swap(v, left, (left + right)/2); /* делящий элемент */last = left; /* переносится в v[0] */for(i = left+1; i <= right; i++) /* деление на части */if (v[i] < v[left])swap(v, ++last, i);swap(v, left, last); /* перезапоминаем делящий элемент */qsort(v, left, last-1);qsort(v, last+1, right);}В нашей программе операция перестановки оформлена в виде отдельной функции (swap), посколькувстречается в qsort трижды./* swap: поменять местами v[i] и v[j] */void swap(int v[], int i, int j){int temp;temp = v[i];v[i] = v[j];v[j] = temp;}Стандартная библиотека имеет функцию qsort, позволяющую сортировать объекты любого типа.Рекурсивная программа не обеспечивает ни экономии памяти, поскольку требуется где-то поддерживать стекзначений, подлежащих обработке, ни быстродействия; но по сравнению со своим нерекурсивнымэквивалентом она часто короче, а часто намного легче для написания и понимания.















