Главная страница
Навигация по странице:

  • 3.4. Переключатель switch

  • 3.7. Инструкции break и continue

  • 3.8. Инструкция goto и метки

  • Язык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents


    Скачать 2.33 Mb.
    НазваниеЯзык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents
    Дата18.09.2022
    Размер2.33 Mb.
    Формат файлаpdf
    Имя файлаBrian_Kernighan_Dennis_Ritchie-The_C_Programming_Language-RU.pdf
    ТипДокументы
    #683263
    страница9 из 31
    1   ...   5   6   7   8   9   10   11   12   ...   31
    Упражнение 3.1. В нашей программе бинарного поиска внутри цикла осуществляются две проверки, хотя могла быть только одна (при увеличении числа проверок вне цикла). Напишите программу, предусмотрев в ней одну проверку внутри цикла. Оцените разницу во времени выполнения.
    3.4. Переключатель switch
    Инструкция switch используется для выбора одного из многих путей. Она проверяет, совпадает ли значение выражения с одним из значений, входящих в некоторое множество целых констант, и выполняет соответствующую этому значению ветвь программы: switch (выражение) { case конст-выр: инструкции case конст-выр: инструкции default: инструкции
    }

    Каждая ветвь case помечена одной или несколькими целочисленными константами или же константными выражениями. Вычисления начинаются с той ветви case
    , в которой константа совпадает со значением выражения. Константы всех ветвей case должны отличаться друг от друга. Если выяснилось, что ни одна из констант не подходит, то выполняется ветвь, помеченная словом default
    , если таковая имеется, в противном случае ничего не делается. Ветви case и default можно располагать в любом порядке.
    В главе 1 мы написали программу, подсчитывающую число вхождений в текст каждой цифры, символов- разделителей (пробелов, табуляций и новых строк) и всех остальных символов. В ней мы использовали последовательность if

    else if

    else
    . Теперь приведем вариант этой программы с переключателем switch
    :
    #include main() /* подсчет цифр, символов-разделителей и прочих символов */
    { int с, i, nwhite, nother, ndigit[10]; nwhite = nother = 0; for (i = 0; i < 10; i++) ndigit[i] = 0; while ((c = getcharO) != EOF) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': ndigit[c - '0' ]++; break; case ' ': case '\n': case '\t': nwhite++; break; default: nother++; break;
    }
    } printf ("цифр ="); for (i= 0; i < 10; i++) printf (" %d", ndigit[i]); printf (", символов-разделителей = %d, прочих = %d\n", nwhite, nother); return 0;
    }
    Инструкция break вызывает немедленный выход из переключателя switch
    . Поскольку выбор ветви case реализуется как переход на метку, то после выполнения одной ветви case
    , если ничего не предпринять, программа провалится вниз на следующую ветвь. Инструкции break и return
    — наиболее распространенные средства выхода из переключателя. Инструкция break используется также для принудительного выхода из циклов while
    , for и do
    - while
    (мы еще поговорим об этом чуть позже).
    "Сквозное" выполнение ветвей case вызывает смешанные чувства. С одной стороны, это хорошо, поскольку позволяет несколько ветвей case объединить в одну, как мы и поступили с цифрами в нашем примере. Но с другой это означает, что в конце почти каждой ветви придется ставить break
    , чтобы избежать перехода к следующей. Последовательный проход по ветвям — вещь ненадежная, это чревато ошибками, особенно при
    изменении программы. За исключением случая с несколькими метками для одного вычисления, старайтесь по возможности реже пользоваться сквозным проходом, но если уж вы его применяете, обязательно комментируйте эти особые места.
    Добрый вам совет: даже в конце последней ветви (после default в нашем примере) помещайте инструкцию break
    , хотя с точки зрения логики в ней нет никакой необходимости. Но эта маленькая предосторожность спасет вас, когда однажды вам потребуется добавить в конец еще одну ветвь case
    Упражнение 3.2. Напишите функцию escape(s, t)
    , которая при копировании текста из t
    в s
    преобразует такие символы, как новая строка и табуляция в "видимые последовательности символов" (вроде
    \n и
    \t
    ).
    Используйте инструкцию switch
    . Напишите функцию, выполняющую обратное преобразование эскейп- последовательностей в настоящие символы.
    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 ((с = getchar()) == ' ' || с == '\n' || с == '\t')
    ; /* обойти символы-разделители */
    нет ни инициализации, ни пересчета параметра, поэтому здесь больше подходит while
    Там, где есть простая инициализация и пошаговое увеличение значения некоторой переменной, больше подходит цикл for
    , так как в этом цикле организующая его часть сосредоточена в начале записи. Например, начало цикла, обрабатывающего первые n
    элементов массива, имеет следующий вид: for (i = 0; i < n; i++)

    Это похоже на
    DO
    -циклы в Фортране и for
    -циклы в Паскале. Сходство, однако, не вполне точное, так как в Си индекс и его предельное значение могут изменяться внутри цикла, и значение индекса i
    после выхода из цикла всегда определено. Поскольку три компоненты цикла могут быть произвольными выражениями, организация fоr
    -циклов не ограничивается только случаем арифметической прогрессии. Однако включать в заголовок цикла вычисления, не имеющие отношения к инициализации и инкрементированию, считается плохим стилем. Заголовок лучше оставить только для операций управления циклом.
    В качестве более внушительного примера приведем другую версию программы atoi
    , выполняющей преобразование строки в ее числовой эквивалент. Это более общая версия по сравнению с рассмотренной в главе 2, в том смысле, что она игнорирует левые символы-разделители (если они есть) и должным образом реагирует на знаки
    +
    и
    -
    , которые могут стоять перед цифрами. (В главе 4 будет рассмотрен вариант atof
    , который осуществляет подобное преобразование для чисел с плавающей точкой.)
    Структура программы отражает вид вводимой информации:
    игнорировать символы-разделители, если они есть
    получить знак, если он есть
    взять целую часть и преобразовать ее
    На каждом шаге выполняется определенная часть работы и четко фиксируется ее результат, который затем используется на следующем шаге. Обработка данных заканчивается на первом же символе, который не может быть частью числа.
    #include
    /* 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[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
    /* reverse: переворачивает строку s (результат в s) */ void reverse(char s[])
    { int c, 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
    эквивалентной полной записью abc. . . xyz в s2
    . В s1
    допускаются буквы (прописные и строчные) и цифры. Следует уметь справляться с такими случаями, как a-b-c, a-z0-9 и -а-b. Считайте знак - в начале или в конце s1
    обычным символом минус.
    3.6. Цикл do-while
    Как мы говорили в главе 1, в циклах while и for проверка условия окончания цикла выполняется наверху. В
    Си имеется еще один вид цикла, do
    - while
    , в котором эта проверка в отличие от while и for делается внизу после каждого прохождения тела цикла, т. е. после того, как тело выполнится хотя бы один раз. Цикл do
    - while имеет следующий синтаксис: do
    инструкция
    while (выражение);
    Сначала выполняется инструкция, затем вычисляется выражение. Если оно истинно, то инструкция выполняется снова и т. д. Когда выражение становится ложным, цикл заканчивает работу. Цикл do
    - while эквивалентен циклу repeat
    - until в Паскале с той лишь разницей, что в первом случае указывается условие продолжения цикла, а во втором — условие его окончания.
    Опыт показывает, что цикл do
    - while используется гораздо реже, чем while и for
    . Тем не менее, потребность в нем время от времени возникает, как, например, в функции itoa
    (обратной по отношению к atoi
    ), преобразующей число в строку символов. Выполнить такое преобразование оказалось несколько более сложным делом, чем ожидалось, поскольку простые алгоритмы генерируют цифры в обратном порядке. Мы остановились на варианте, в котором сначала формируется обратная последовательность цифр, а затем она реверсируется.
    /* itoa: преобразование n в строку s */ void itoa (int n, char s[])
    { int i, sign; if ((sign = n) < 0) /* сохраняем знак */ n = -n; /* делаем n положительным */ i = 0; do { /* генерируем цифры в обратном порядке */ s[i++] = n % 10 + '0'; /* следующая цифра */
    } while ((n /= 10) > 0); /* исключить ее */ if (sign < 0) s[i++] = '-'; s[i] = '\0'; reverse(s);
    }
    Конструкция do
    - while здесь необходима или по крайней мере удобна, поскольку в s
    посылается хотя бы один символ, даже если n
    равно нулю. В теле цикла одну инструкцию мы выделили фигурными скобками

    (хотя они и избыточны), чтобы неискушенный читатель не принял по ошибке слово while за начало цикла while
    Упражнение 3.4. При условии, что для представления чисел используется дополнительный код, наша версия itoa не справляется с самым большим по модулю отрицательным числом, значение которого равняется -(2
    n-
    1
    ), где n — размер слова. Объясните, чем это вызвано. Модифицируйте программу таким образом, чтобы она давала правильное значение указанного числа независимо от машины, на которой выполняется.
    Упражнение 3.5. Напишите функцию itob(n, s, b)
    , которая переводит целое n
    в строку s
    , представляющую число по основанию b
    . В частности, itob(n, s, 16)
    помещает в s
    текст числа n
    в шестнадцатеричном виде.
    Упражнение 3.6. Напишите версию itoa с дополнительным третьим аргументом, задающим минимальную ширину поля. При необходимости преобразованное число должно слева дополняться пробелами.
    3.7. Инструкции break и continue
    Иногда бывает удобно выйти из цикла не по результату проверки, осуществляемой в начале или в конце цикла, а каким-то другим способом. Такую возможность для циклов for
    , while и do
    - while
    , а также для переключателя switch предоставляет инструкция break
    . Эта инструкция вызывает немедленный выход из самого внутреннего из объемлющих ее циклов или переключателей.
    Следующая функция, trim
    , удаляет из строки завершающие пробелы, табуляции, символы новой строки; break используется в ней для выхода из цикла по первому обнаруженному справа символу, отличному от названных.
    /* trim: удаляет завершающие пробелы, табуляции и новые строки */ int trim(char s[])
    { int n; for (n = strlen(s)-1; n >= 0; n--) if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n') break; s[n+1] = '\0'; return n;
    }
    С помощью функции strlen можно получить длину строки. Цикл for просматривает его в обратном порядке, начиная с конца, до тех пор, пока не встретится символ, отличный от пробела, табуляции и новой строки. Цикл прерывается, как только такой символ обнаружится или n станет отрицательным (т. е. вся строка будет просмотрена). Убедитесь, что функция ведет себя правильно и в случаях, когда строка пуста или состоит только из символов-разделителей.
    Инструкция continue в чем-то похожа на break
    , но применяется гораздо реже. Она вынуждает ближайший объемлющий ее цикл (
    for
    , while или do
    - while
    ) начать следующий шаг итерации. Для while и do
    - while это означает немедленный переход к проверке условия, а для for
    — к приращению шага. Инструкцию continue можно применять только к циклам, но не к switch
    . Внутри переключателя switch
    , расположенного в цикле, она вызовет переход к следующей итерации этого цикла.
    Вот фрагмент программы, обрабатывающий только неотрицательные элементы массива a
    (отрицательные пропускаются). for ( i = 0 ; i < n; i++) { if (a[i] < 0) /* пропуск отрицательных элементов */
    continue;
    /* обработка положительных элементов */
    }
    К инструкции continue часто прибегают тогда, когда оставшаяся часть цикла сложна, а замена условия в нем на противоположное и введение еще одного уровня приводят к слишком большому числу уровней вложенности.
    3.8. Инструкция goto и метки
    В Си имеются порицаемая многими инструкция goto и метки для перехода на них. Строго говоря, в этой инструкции нет никакой необходимости, и на практике почти всегда легко без нее обойтись. До сих пор в нашей книге мы не использовали goto
    Однако существуют случаи, в которых goto может пригодиться. Наиболее типична ситуация, когда нужно прервать обработку в некоторой глубоко вложенной структуре и выйти сразу из двух или большего числа вложенных циклов. Инструкция break здесь не поможет, так как она обеспечит выход только из самого внутреннего цикла. В качестве примера рассмотрим следующую конструкцию: for (…) for (…) {
    … if (disaster) /* если бедствие */ goto error; /* уйти на ошибку */
    }
    … error: /* обработка ошибки */
    ликвидировать беспорядок
    Такая организация программы удобна, если подпрограмма обработки ошибочной ситуации не тривиальна и ошибка может встретиться в нескольких местах.
    Метка имеет вид обычного имени переменной, за которым следует двоеточие. На метку можно перейти с помощью goto из любого места данной функции, т. е. метка видима на протяжении всей функции.
    В качестве еще одного примера рассмотрим такую задачу: определить, есть ли в массивах a
    и b
    совпадающие элементы. Один из возможных вариантов ее реализации имеет следующий вид: for (i = 0 ; i < n; i++) for (j = 0; j < m; j++) if (a[i] == b[j]) goto found;
    /* нет одинаковых элементов */
    … found:
    /* обнаружено совпадение: a[i] == b[j] */

    Программу нахождения совпадающих элементов можно написать и без goto
    , правда, заплатив за это дополнительными проверками и еще одной переменной: found = 0; for (i = 0; i < n && !found; i++) for (j = 0; j < m && !found; j++) if (a[i] == b[j])
    found = 1; if (found)
    /* обнаружено совпадение: a[i-1] == b[j-1] */
    … else
    /* нет одинаковых элементов */

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

    1   ...   5   6   7   8   9   10   11   12   ...   31


    написать администратору сайта