Книга: Язык программирования Си. Издание 3-е, исправленное

3.5 Циклы while и for

3.5 Циклы while и for

Мы уже встречались с циклами while и for. В цикле

while (выражение)
 инструкция

вычисляется выражение. Если его значение отлично от нуля, то выполняется инструкция, и вычисление выражения повторяется. Этот цикл продолжается до тех пор, пока выражение не станет равным нулю, после чего вычисления продолжатся с точки, расположенной сразу за инструкцией.

Инструкция for

for (выр1; выр2; выр3)
 инструкция

эквивалентна конструкции

выр1;
while (выр2) {
 инструкция
 выр3;
}

если не считать отличий в поведении инструкции continue, речь о которой пойдет в параграфе 3.7.

С точки зрения грамматики три компоненты цикла for представляют собой произвольные выражения, но чаще выр1 и выр3 - это присваивания или вызовы функций, а выр2 - выражение отношения. Любое из этих трех выражений может отсутствовать, но точку с запятой опускать нельзя. При отсутствии выр1, или выр3 считается, что их просто нет в конструкции цикла; при отсутствии выр2, предполагается, что его значение как бы всегда истинно. Например,

for (;;) {
 :
}

есть "бесконечный" цикл, выполнение которого, вероятно, прерывается каким-то другим способом, например с помощью инструкций break или return. Какой цикл выбрать: while или for - это дело вкуса. Так, в

while ((c = getchar()) == ' ' || c == 'n' || c == 't')
 ; /* обойти символы-разделители */

нет ни инициализации, ни пересчета параметра, поэтому здесь больше подходит while.

Там, где есть простая инициализация и пошаговое увеличение значения некоторой переменной, больше подходит цикл for, так как в этом цикле организующая его часть сосредоточена в начале записи. Например, начало цикла, обрабатывающего первые n элементов массива, имеет следующий вид:

for (i = 0; i ‹ n; i++)
 …

Это похоже на DO-циклы в Фортране и for-циклы в Паскале. Сходство, однако, не вполне точное, так как в Си индекс и его предельное значение могут изменяться внутри цикла, и значение индекса i после выхода из цикла всегда определено. Поскольку три компонента цикла могут быть произвольными выражениями, организация for-циклов не ограничивается только случаем арифметической прогрессии. Однако включать в заголовок цикла вычисления, не имеющие отношения к инициализации и инкрементированию, считается плохим стилем. Заголовок лучше оставить только для операций управления циклом.

В качестве более внушительного примера приведем другую версию программы atoi, выполняющей преобразование строки в ее числовой эквивалент. Это более общая версия по сравнению с рассмотренной в главе 2, в том смысле, что она игнорирует левые символы-разделители (если они есть) и должным образом реагирует на знаки + и -, которые могут стоять перед цифрами. (В главе 4 будет рассмотрен вариант atof, который осуществляет подобное преобразование для чисел с плавающей точкой.)

Структура программы отражает вид вводимой информации:

игнорировать символы-разделители, если они есть
получить знак, если он есть
взять целую часть и преобразовать ее

На каждом шаге выполняется определенная часть работы и четко фиксируется ее результат, который затем используется на следующем шаге. Обработка данных заканчивается на первом же символе, который не может быть частью числа.

#include ‹ctype.h›
/* atoi: преобразование s в целое число; версия 2 */
int atoi(char s[])
{
 int i, n, sign;
 /* игнорировать символы-разделители */
 for (i = 0; isspace(s[i]); i++)
  ;
 sign = (s[i] == '-') ? -1 : 1;
 if (s[i] == '+' || s[i] == '-') /* пропуск знака */
  i++;
 for (n = 0; isdigit(s[i]); i++)
 n = 10 * n + (s[i] - '0');
 return sign * n;
}

Заметим, что в стандартной библиотеке имеется более совершенная функция преобразования строки в длинное целое (long int) - функция strtol (см. параграф 5 приложения B).

Преимущества, которые дает централизация управления циклом, становятся еще более очевидными, когда несколько циклов вложены друг в друга. Проиллюстрируем их на примере сортировки массива целых чисел методом Шелла, предложенным им в 1959 г. Основная идея этого алгоритма в том, что на ранних стадиях сравниваются далеко отстоящие друг от друга, а не соседние элементы, как в обычных перестановочных сортировках. Это приводит к быстрому устранению массовой неупорядоченности, благодаря чему на более поздней стадии остается меньше работы. Интервал между сравниваемыми элементами постепенно уменьшается до единицы, и в этот момент сортировка сводится к обычным перестановкам соседних элементов. Программа shellsort имеет следующий вид:

/* shellsort: сортируются v[0]… v[n-1] в возрастающем порядке */
void shellsort (int v[], int n)
{
 int gap, i, j, temp;
 for (gap = n/2; gap › 0; gap /= 2)
  for (i = gap; i ‹ n; i++)
   for (j = i - gap; j ›= 0 && v[j] › v[j+gap]; j -= gap) {
    temp = v[j];
    v[j] = v[j + gap];
    v[j + gap] = temp;
   }
}

Здесь использованы три вложенных друг в друга цикла. Внешний управляет интервалом gap между сравниваемыми элементами, сокращая его путем деления пополам от n/2 до нуля. Средний цикл перебирает элементы. Внутренний - сравнивает каждую пару элементов, отстоящих друг от друга на расстоянии gap, и переставляет элементы в неупорядоченных парах. Так как gap обязательно сведется к единице, все элементы в конечном счете будут упорядочены. Обратите внимание на то, что универсальность цикла for позволяет сделать внешний цикл по форме похожим на другие, хотя он и не является арифметической прогрессией.

Последний оператор Си - это "," (запятая), которую чаще всего используют в инструкции for. Пара выражений, разделенных запятой, вычисляется слева направо. Типом и значением результата являются тип и значение правого выражения, что позволяет в инструкции for в каждой из трех компонент иметь по несколько выражений, например вести два индекса параллельно. Продемонстрируем это на примере функции reverse(s), которая "переворачивает" строку s, оставляя результат в той же строке s:

#include ‹string.h›
/* reverse: переворачивает строку s (результат в s) */
void reverse(char s[])
{
 int с, i, j;
 for (i = 0, j = strlen(s)-1; i ‹ j; i++, j--) {
  с = s[i];
  s[i] = s[j];
  s[j] = c;
 }
}

Запятые, разделяющие аргументы функции, переменные в объявлениях и пр. не являются операторами-запятыми и не обеспечивают вычислений слева направо.

Запятыми как операторами следует пользоваться умеренно. Более всего они уместны в конструкциях, которые тесно связаны друг с другом (как в for-цикле программы reverse), а также в макросах, в которых многоступенчатые вычисления должны быть выражены одним выражением. Запятой-оператором в программе reverse можно было бы воспользоваться и при обмене символами в проверяемых парах элементов строки, мысля этот обмен как одну отдельную операцию:

for (i = 0, j = strlen(s)-1; i ‹ j; i++, j--)
 с = s[i], s[i] = s[j], s[j] = c;

Упражнение 3.3. Напишите функцию expand(s1,s2), заменяющую сокращенную запись наподобие a-z в строке s1 эквивалентной полной записью аbс…хуz в s2. В s1 допускаются буквы (прописные и строчные) и цифры. Следует уметь справляться с такими случаями, как a-b-c, a-z0-9 и -a-b. Считайте знак - в начале или в конце s1 обычным символом минус.

Оглавление книги


Генерация: 1.720. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз