netlib.narod.ru< Назад | Оглавление | Далее >

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 в Паскале. Сходство, однако, не вполне точное, так как в Си индекс и его предельное значение могут изменяться внутри цикла, и значение индекса всегда определено после завершения цикла, независимо от способа его прекращения. Поскольку три компоненты цикла могут быть произвольными выражениями, организация циклов 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 приложения В).

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

/* shellsort: сортировка элементов массива v
              в порядке возрастания */
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 */
void reverse(char s[])
{
    int c, i, j;

    for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {
        c = s[i];
        s[i] = s[j];
        s[j] = c;
    }
}

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

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

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

Упражнение 3-3


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



netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz