Kernighan, B. W. and Ritchie, D. M. "The 'C' Programming Language"; Chapter 3
Поток управления
Управляющие операторы языка определяют порядок вычислений.В приведенных ранее примерах мы уже встречались с наиболееупотребительными управляющими конструкциями языка "C";здесь мы опишем остальные операторы управления и уточним действияоператоров, обсуждавшихся ранее.
Содержание
- 3.1. Операторы и блоки
- 3.2. if - else
- 3.3. else - if
- 3.4. Переключатель
- 3.5. Циклы - while и for
- 3.6. Цикл do - while
- 3.7. Оператор break
- 3.8. Оператор continue
- 3.9. Оператор goto и метки
- 3.2. if - else
Операторы и блоки
Такие выражения, как x=0, или i++, или printf(...), становятсяоператорами, если за ними следует точка с запятой, как, например,
x = 0;i++;printf(...);В языке "C" точка с запятой является признаком конца оператора,а не разделителем операторов, как в языках типа алгола.
Фигурные скобки { и } используются для об'единения описаний иоператоров в составной оператор или блок, так что они оказываютсясинтаксически эквивалентны одному оператору. Один явный пример такоготипа дают фигурные скобки, в которые заключаются операторы, составляющиефункцию, другой - фигурные скобки вокруг группы операторов вконструкциях if, else, while и for.(на самом деле переменные могут бытьописаны внутри любого блока; мы поговорим об этом вглаве 4).Точка с запятой никогда не ставится после первой фигурной скобки,которая завершает блок.
if - else
Оператор if - else используется при необходимости сделать выбор.Формально синтаксис имеет вид
if (выражение) оператор-1else оператор-2где часть else является необязательной. Сначала вычисляется выражение;если оно "истинно" /т.е. значение выражения отлично от нуля/, товыполняется оператор-1. Если оно ложно /значение выражения равно нулю/,и если есть часть с else, то вместо оператора-1 выполняется оператор-2.
Так как if просто проверяет численное значение выражения, то возможнонекоторое сокращение записи. Самой очевидной возможностью являетсязапись
if (выражение)вместо
if (выражение != 0)иногда такая запись является ясной и естественной, но временамиона становится загадочной.
То, что часть else в конструкции if - else является необязательной,приводит к двусмысленности в случае, когда else опускается вовложенной последовательности операторов if. Эта неоднозначностьразрешается обычным образом - else связывается с ближайшим предыдущимif, не содержащим else.Например, в
if (n > 0) if (a > b) z = a; else z = b;конструкция else относится к внутреннему if, как мы и показали, сдвинувelse под соответствующий if. Если это не то, что вы хотите, то дляполучения нужного соответствия необходимо использовать фигурные скобки:
if (n > 0) { if (a > b) z = a;} else z = b;
Такая двусмысленность особенно пагубна в ситуациях типа
if (n > 0) for (i = 0; i < n; i++) if (s[i] > 0) { printf("..."); return (i); }else /* wrong */ printf("error - n is zero\n");Запись else под if ясно показывает, чего вы хотите, но компилятор неполучит соответствующего указания и свяжет else с внутренним if. Ошибкитакого рода очень трудно обнаруживаются.
Между прочим, обратите внимание, что в
if (a > b) z = a;else z = b;после z=a стоит точка с запятой. Дело в том, что согласно грамматическимправилам за if должен следовать оператор, а выражение типа z=a,являющееся оператором, всегда заканчивается точкой с запятой.
else - if
Конструкция
if (выражение) операторelse if (выражение) операторelse if (выражение) операторelse операторвстречается настолько часто, что заслуживает отдельного краткогорассмотрения. Такая последовательность операторов if являетсянаиболее распространенным способом программирования выбора изнескольких возможных вариантов. Выражения просматриваютсяпоследовательно; если какоe-то выражение оказывается истинным,товыполняется относящийся к нему оператор, и этим вся цепочказаканчивается. Каждый оператор может быть либо отдельным оператором,либо группой операторов в фигурных скобках.
Последняя часть с else имеет дело со случаем, когда ни одно изпроверяемых условий не выполняется. Иногда при этом не надопредпринимать никаких явных действий; в этом случае хвост
else операторможет быть опущен, или его можно использовать для контроля, чтобызасечь "невозможное" условие.
Для иллюстрации выбора из трех возможных вариантов приведемпрограмму функции,которая методом половинного деления определяет, находится лиданное значение х в отсортированном массиве v. Элементы массива vдолжны быть расположены в порядке возрастания. Функция возвращаетномер позиции (число между 0 и n-1), в которой значение х находится в v,и -1, если х не содержится в v.
binary(x, v, n) /* find x in * v[0]...v[n-1] */ int x, v[], n;{ int low, high, mid; low = 0; high = n - 1; while (low <= high) { mid = (low + high) / 2; if (x < v[mid]) high = mid - 1; else if (x > v[mid]) low = mid + 1; else /* found match */ return (mid); } return (-1);}
Основной частью каждого шага алгоритма является проверка, будет ли xменьше, больше или равен среднему элементу v[mid]; использованиеконструкции else - if здесь вполне естественно.
Переключатель
Оператор switch дает специальный способ выбора одного из многихвариантов, который заключается в проверке совпадения значения данноговыражения с одной из заданных констант и соответствующем ветвлении. Вглаве 1мы привели программу подсчета числа вхождений каждой цифры,символов пустых промежутков и всех остальных символов, использующуюпоследовательность if...else if...else. вот та же самая программа спереключателем.
main(){ /* count digits,white * space, others */ int с, i, nwhite, nother, ndigit[10]; nwhite = nother = 0; for (i = 0; i < 10; i++) ndigit[i] = 0; while ((c = getchar()) != 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("digits ="); for (i = 0; i < 10; i++) printf(" %d", ndigit[i]); printf("\nwhite space = %d, other = %d\n", nwhite, nother);}
Переключатель вычисляет целое выражение в круглых скобках (в даннойпрограмме - значение символа с) и сравнивает его значение со всемислучаями (case). Каждый случай должен быть помечен либо целым, либосимвольной константой, либо константным выражением. Если значениеконстантного выражения, стоящего после вариантного префикса case,совпадает со значением целого выражения, то выполнение начинается сэтого случая. Если ни один из случаев не подходит, то выполняетсяоператор после префикса default. Префикс default является необязательным,если его нет, и ни один из случаев не подходит, то вообще никакиедействия не выполняются. Случаи и выбор по умолчанию могутрасполагаться в любом порядке. Все случаи должны быть различными.
Оператор break приводит к немедленному выходу из переключателя.Поскольку случаи служат только в качестве меток, то если вы непредпримите явных действий после выполнения операторов, соответствующиходному случаю, вы провалитесь на следующий случай. Операторы break иreturn являются самым обычным способом выхода из переключателя. Как мыобсудим позже в этой главе, оператор break можно использовать и длянемедленного выхода из операторов цикла while, for и do.
Проваливание сквозь случаи имеет как свои достоинства, так и недостатки.К положительным качествам можно отнести то, что оно позволяет связатьнесколько случаев с одним действием, как было с пробелом, табуляцией иновой строкой в нашем примере. Но в то же время оно обычно приводит кнеобходимости заканчивать каждый случай оператором break, чтобы избежатьперехода к следующему случаю. Проваливание с одного случая на другойобычно бывает неустойчивым, так как оно склонно к расщеплению примодификации программы. За исключением, когда одному вычислениюсоответствуют несколько меток, проваливание следует использоватьумеренно.
Заведите привычку ставить оператор break после последнего случая (вданном примере после default), даже если это не является логическинеобходимым. В один прекрасный день, когда вы добавите в конец ещеодин случай, эта маленькая мера предосторожности избавит вас отнеприятностей.
Циклы - while и for
Мы уже сталкивались с операторами цикла while и for. В конструкции
while (выражение) операторвычисляется выражение. Если его значение отлично от нуля, то выполняетсяоператор и выражение вычисляется снова. Этот цикл продолжается до техпор, пока значение выражения не станет нулем, после чего выполнениепрограммы продолжается с места после оператора.
Оператор
for (выражение 1; выражение 2; выражение 3) операторэквивалентен последовательности
выражение 1;while (выражение 2) { оператор; выражение 3;}Грамматически все три компонента в for являются выражениями. Наиболеераспространенным является случай, когда выражение 1 и выражение 3являются присваиваниями или обращениями к функциям, а выражение 2 -условным выражением. Любая из трех частей может быть опущена, хотяточки с запятой при этом должны оставаться. Если отсутствует выражение 1или выражение 3, то оно просто выпадает из расширения. Если жеотсутствует проверка, выражение 2, то считается, как будто оно всегдаистинно, так что
for (;;) { ...}является бесконечным циклом, о котором предполагается, что он будетпрерван другими средствами (такими как break или return).
Использовать ли while или for - это, в основном дело вкуса. Например в
while ((c = getchar()) == ' ' || c == '\n' || c == '\t'); /* skip white space * characters */нет ни инициализации, ни реинициализации, так что цикл while выглядитсамым естественным.
Цикл for, очевидно, предпочтительнее там, где имеется простаяинициализация и реинициализация, поскольку при этом управляющиециклом операторы наглядным образом оказываются вместе в начале цикла.Это наиболее очевидно в конструкции
for (i = 0; i < n; i++)которая является идиомой языка "C" для обработки первых n элементовмассива, аналогичной оператору цикла do в фортране и pl/1. Аналогия,однако, не полная, так как границы цикла могут быть изменены внутрицикла, а управляющая переменная сохраняет свое значение после выходаиз цикла, какова бы ни была причина этого выхода. Поскольку компонентамиfor могут быть произвольные выражения, они не ограничиваются толькоарифметическими прогрессиями. Тем не менее является плохим стилемвключать в for вычисления, которые не относятся к управлению циклом,лучше поместить их в управляемые циклом операторы.
В качестве большего по размеру примера приведем другой вариант функцииatoi,преобразующей строку в ее численный эквивалент. Этот вариантявляется более общим; он допускает присутствие в начале символов пустыхпромежутков и знака + или -. (вглаве 4 приведена функция atof, котораявыполняет то же самое преобразование для чисел с плавающей точкой).
Общая схема программы отражает форму поступающих данных:
- пропустить пустой промежуток, если он имеется
- извлечь знак, если он имеется
- извлечь целую часть и преобразовать ее
Каждый шаг выполняет свою часть работы и оставляет все в подготовленномсостоянии для следующей части. Весь процесс заканчивается на первомсимволе, который не может быть частью числа.
atoi(s) /* convert s to integer */ char s[];{ int i, n, sign; /* skip white space */ for (i = 0; s[i] == ' ' || s[i] == '\n' || s[i] == '\t'; i++); sign = 1; if (s[i] == '+' || s[i] == '-') /* sign */ sign = (s[i++] == '+') ? 1 : -1; for (n = 0; s[i] >= '0' && s[i] <= '9'; i++) n = 10 * n + s[i] - '0'; return (sign * n);}
Преимущества централизации управления циклом становятся еще болееочевидными, когда имеется несколько вложенных циклов. Следующаяфункциясортирует массив целых чисел по методу шелла. Основная идея сортировкипо шеллу заключается в том, что сначала сравниваются удаленные элементы,а не смежные, как в обычном методе сортировки. Это приводит к быстромуустранению большой части неупорядоченности и сокращает последующуюработу. Интервал между элементами постепенно сокращается до единицы,когда сортировка фактически превращается в метод перестановки соседнихэлементов.
shell(v, n) /* sort v[0]...v[n-1] * into increasing order */ int v[], 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; }}Здесь имеются три вложенных цикла. Самый внешний цикл управляетинтервалом между сравниваемыми элементами, уменьшая его от n/2 вдвоепри каждом проходе, пока он не станет равным нулю. Средний циклсравнивает каждую пару элементов, разделенных на величину интервала;самый внутренний цикл переставляет любую неупорядоченную пару. Так какинтервал в конце концов сводится к единице, все элементы в результатеупорядочиваются правильно. Отметим, что в силу общности конструкции forвнешний цикл укладывается в ту же самую форму, что и остальные, хотя они не является арифметической прогрессией.
Последней операцией языка "C" является запятая ",", которая чаще всегоиспользуется в операторе for. Два выражения, разделенные запятой,вычисляются слева направо, причем типом и значением результата являютсятип и значение правого операнда. Таким образом, в различные частиоператора for можно включить несколько выражений, например, дляпараллельного изменения двух индексов. Это иллюстрируется функциейreverse(s),которая располагает строку s в обратном порядке на том жеместе.
reverse(s) /* reverse string s in * place */ 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; }}Запятые, которые разделяют аргументы функций, переменные в описаниях ит.д., не имеют отношения к операции запятая и не обеспечивают вычисленийслева направо.
- Упражнение 3-2.
- Составьте программу для функции expand(s1,s2), которая расширяетсокращенные обозначения вида a-z из строки s1 в эквивалентный полныйсписок abc...xyz в s2. Допускаются сокращения для строчных и прописныхбукв и цифр. Будьте готовы иметь дело со случаями типа a-b-c, a-z0-9 и-a-z. (полезное соглашение состоит в том, что символ -, стоящий в началеили конце, воспринимается буквально).
Цикл do - while
Как уже отмечалось в главе 1, циклы while и for обладают тем приятнымсвойством, что в них проверка окончания осуществляется в начале, а не вконце цикла. Третий оператор цикла языка "C", do-while, проверяетусловие окончания в конце, после каждого прохода через тело цикла; телоцикла всегда выполняется по крайней мере один раз. Синтаксис этогооператора имеет вид:
do операторwhile (выражение)Сначала выполняется оператор, затем вычисляется выражение. Если оноистинно, то оператор выполняется снова и т.д. если выражение становитсяложным, цикл заканчивается.
Как и можно было ожидать, цикл do-while используется значительно реже,чем while и for, составляя примерно пять процентов от всех циклов. Темне менее, иногда он оказывается полезным, как, например, в следующейфункции itoa,которая преобразует число в символьную строку (обратнаяфункции atoi). Эта задача оказывается несколько более сложной, чемможет показаться сначала. Дело в том, что простые методы выделенияцифр генерируют их в неправильном порядке. Мы предпочли получитьстроку в обратном порядке, а затем обратить ее.
itoa(n, s) /* convert n to * characters in s */char s[];int n;{ int i, sign; if ((sign = n) < 0) /* record sign */ n = -n; /* make n positive */ i = 0; do { /* generate digits in * reverse order */ s[i++] = n % 10 + '0'; /* get next digit */ } while ((n /= 10) > 0);/* delete it */ if (sign < 0) s[i++] = '-'; s[i] = '\0'; reverse(s);}Цикл do-while здесь необходим, или по крайней мере удобен, поскольку,каково бы ни было значение n, массив s должен содержать хотя бы одинсимвол. Мы заключили в фигурные скобки один оператор, составляющийтело do-while, хотя это и не обязательно, для того, чтобы торопливыйчитатель не принял часть while за начало оператора цикла while.
- Упражнение 3-3.
- При представлении чисел в двоичном дополнительном коде наш вариант itoaне справляется с наибольшим отрицательным числом, т.е. со значением nравным -2 в степени м-1, где м - размер слова. Об'ясните почему.Измените программу так, чтобы она правильно печатала это значение налюбой машине.
- Упражнение 3-4.
- Напишите аналогичную функцию itob(n,s), которая преобразует целое беззнака n в его двоичное символьное представление в s. Запрограммируйтефункцию itoh, которая преобразует целое в шестнадцатеричноепредставление.
- Упражнение 3-5.
- Напишите вариант itoa, который имеет три, а не два аргумента. Третийаргумент - минимальная ширина поля; преобразованное число должно, еслиэто необходимо, дополняться слева пробелами, так чтобы оно имелодостаточную ширину.
Оператор break
Иногда бывает удобным иметь возможность управлять выходом из циклаиначе, чем проверкой условия в начале или в конце. Оператор breakпозволяет выйти из операторов for, while и do до окончания циклаточно так же, как и из переключателя. Оператор break приводит кнемедленному выходу из самого внутреннего охватывающего его цикла(или переключателя).
Следующая программа удаляет хвостовые пробелы и табуляции из концакаждой строки файла ввода. Она использует оператор break для выходаиз цикла, когда найден крайний правый отличный от пробела и табуляциисимвол.
#define maxline 1000main(){ /* remove trailing blanks * and tabs */ int n; char line[maxline]; while ((n = getline(line, maxline)) > 0) { while (--n >= 0) if (line[n] != ' ' && line[n] != '\t' && line[n] != '\n') break; line[n + 1] = '\0'; printf("%s\n", line); }}
Функция getline возвращает длину строки. Внутренний цикл начинается споследнего символа line (напомним, что --n уменьшает n до использованияего значения) и движется в обратном направлении в поиске первого символа,который отличен от пробела, табуляции или новой строки. Циклпрерывается, когда либо найден такой символ, либо n становитсяотрицательным (т.е., когда просмотрена вся строка). Советуем вамубедиться, что такое поведение правильно и в том случае, когда строкасостоит только из символов пустых промежутков.
В качестве альтернативы к break можно ввести проверку в сам цикл:
while ((n = getline(line, maxline)) > 0){while (--n >= 0 && (line[n] == ' ' || line[n] == '\t' || line[n] == '\n'));...}Это уступает предыдущему варианту, так как проверка становитсятруднее для понимания. Проверок, которые требуют переплетения&&, ||, !. И круглых скобок, по возможности следует избегать.
Оператор continue
Оператор continue родственен оператору break, но используется реже; онприводит к началу следующей итерации охватывающего цикла (for, while, do).В циклах while и do это означает непосредственный переход квыполнению проверочной части; в цикле for управление передается на шагреинициализации. (оператор continue применяется только в циклах, но не впереключателях. Оператор continue внутри переключателя внутри циклавызывает выполнение следующей итерации цикла).
В качестве примера приведем фрагмент, который обрабатывает толькоположительные элементы массива а; отрицательные значения пропускаются.
for (i = 0; i < n; i++) { if (a[i] < 0) /* skip negative elements */ continue; ... /* do positive elements */}Оператор continue часто используется, когда последующая часть циклаоказывается слишком сложной, так что рассмотрение условия, обратногопроверяемому, приводит к слишком глубокому уровню вложенности программы.
- Упражнение 3-6.
- Напишите программу копирования ввода на вывод, с тем исключением, чтоиз каждой группы последовательных одинаковых строк выводится толькоодна. (это простой вариант утилиты uniq систем UNIX).
Оператор goto и метки
В языке "C" предусмотрен и оператор goto, которым бесконечнозлоупотребляют, и метки для ветвления. С формальной точки зренияоператор goto никогда не является необходимым, и на практике почтивсегда можно обойтись без него. Мы не использовали goto в этой книге.
Тем не менее, мы укажем несколько ситуаций, где оператор goto можетнайти свое место. Наиболее характерным является его использование тогда,когда нужно прервать выполнение в некоторой глубоко вложенной структуре,например, выйти сразу из двух циклов. Здесь нельзя непосредственноиспользовать оператор break, так как он прерывает только самыйвнутренний цикл. Поэтому:
for ( ... ) for ( ... ) { ... if (disaster) goto error; }...error: clean up the messЕсли программа обработки ошибок нетривиальна и ошибки могут возникатьв нескольких местах, то такая организация оказывается удобной. Меткаимеет такую же форму, что и имя переменной, и за ней всегда следуетдвоеточие. Метка может быть приписана к любому оператору той жефункции, в которой находится оператор goto.
В качестве другого примера рассмотрим задачу нахождения первогоотрицательного элемента в двумерном массиве. (многомерные массивырассматриваются вглаве 5). Вот одна из возможностей:
for (i = 0; i < n; i++) for (j = 0; j < m; j++) if (v[i][j] < 0) goto found;...found:/* found one at position i, j */...
Программа, использующая оператор goto, всегда может быть написана безнего, хотя, возможно, за счет повторения некоторых проверок и введениядополнительных переменных. Например, программа поиска в массиве приметвид:
found = 0;for (i = 0; i < n && !found; i++) for (j = 0; j < m && !found; j++) found = v[i][j] < 0;if (found) /* it was at i-1, j-1 */ ...else /* not found */ ...
Хотя мы не являемся в этом вопроседогматиками, нам все же кажется, что если и нужноиспользовать оператор goto, то весьма умеренно.