Последней операцией языка "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), которая расширяет сокращенные
обозначения вида а-Z из строки S1 в эквивалентный полный список авс...XYZ в S2.
Допускаются сок- ращения для строчных и прописных букв и цифр. Будьте готовы иметь
дело со случаями типа а-в-с, а-Z0-9 и -а-Z. (Полезное соглашение состоит в том,
что символ -, стоящий в начале или конце, воспринимается буквально).
3.6. Цикл 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-WHILе, хотя это и не обязательно,
для того, чтобы торопливый читатель не принял часть WHILE за начало оператора цикла
WHILE.
Упражнение 3-3
При представлении чисел в двоичном дополнительном коде наш вариант ITOA не справляется
с наибольшим отрицательным числом, т.е. Со значением N рAвным -2 в степени м-1,
где м - размер слова. объясните почему. Измените программу так, что- бы она правильно
печатала это значение на любой машине.
Упражнение 3-4
Напишите аналогичную функцию ITOB(N,S), которая преобра- зует целое без знака
N в его двоичное символьное представле- ние в S. Запрограммируйте функцию ITOH,
которая преобразует целое в шестнадцатеричное представление.
Упражнение 3-5
Напишите вариант Iтоа, который имеет три, а не два аргу- мента. Третий аргумент
- минимальная ширина поля; преобразо- ванное число должно, если это необходимо,
дополняться слева пробелами, так чтобы оно имело достаточную ширину.
3.7. Оператор BREAK
Иногда бывает удобным иметь возможность управлять выхо- дом из цикла иначе, чем
проверкой условия в начале или в конце. Оператор BRеак позволяет выйти из операторов
FOR, WHILE и DO до окончания цикла точно так же, как и из перек- лючателя. Оператор
BRеак приводит к немедленному выходу из самого внутреннего охватывающего его цикла
(или переключате- ля).
Следующая программа удаляет хвостовые пробелы и табуля- ции из конца каждой строки
файла ввода. Она использует опе- ратор BRеак для выхода из цикла, когда найден крайний
правый отличный от пробела и табуляции символ.
#DEFINE MAXLINE 1000
MAIN() /* 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 становится отри- цательным (т.е., когда просмотрена вся строка). Советуем
вам убедиться, что такое поведение правильно и в том случае, когда строка состоит
только из символов пустых промежутков.
В качестве альтернативы к BRеак можно ввести проверку в сам цикл:
WHILE ((N = GETLINE(LINE,MAXLINE)) > 0) {
WHILE (--N >= 0
&& (LINE[N] == ' ' \!\! LINE[N] == '\T'
\!\! LINE[N] == '\N'))
;
... }
Это уступает предыдущему варианту, так как проверка стано- вится труднее для
понимания. Проверок, которые требуют пе- реплетения &&, \!\!, ! И круглых скобок,
по возможности сле- дует избегать.
3.8. Оператор CONTINUE
Оператор CONTINUE родственен оператору BRеак, но исполь- зуется реже; он приводит
к началу следующей итерации охваты- вающего цикла (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).
3.9. Оператор GOTO и метки
В языке "C" предусмотрен и оператор GOTO, которым беско- нечно злоупотребляют,
и метки для ветвления. С формальной точки зрения оператор GOTO никогда не является
необходимым, и на практике почти всегда можно обойтись без него. Мы не использовали
GOTO в этой книге.
Тем не менее, мы укажем несколько ситуаций, где оператор GOTO может найти свое
место. Наиболее характерным является его использование тогда, когда нужно прервать
выполнение в некоторой глубоко вложенной структуре, например, выйти сразу из двух
циклов. Здесь нельзя непосредственно использовать оператор BRеак, так как он прерывает
только самый внутренний цикл. Поэтому:
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;
/* DIDN'T FIND */
... 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, то весьма умеренно.
* 4. Функции и структура программ *
Функции разбивают большие вычислительные задачи на ма- ленькие подзадачи и позволяют
использовать в работе то, что уже сделано другими, а не начинать каждый раз с пустого
мес- та. Соответствующие функции часто могут скрывать в себе де- тали проводимых
в разных частях программы операций, знать которые нет необходимости, проясняя тем
самым всю программу, как целое, и облегчая мучения при внесении изменений.
Язык "C" разрабатывался со стремлением сделать функции эффективными и удобными
для использования; "C"-программы обычно состоят из большого числа маленьких функций,
а не из нескольких больших. Программа может размещаться в одном или нескольких исходных
файлах любым удобным образом; исходные файлы могут компилироваться отдельно и загружаться
вместе наряду со скомпилированными ранее функциями из библиотек. Мы здесь не будем
вдаваться в детали этого процесса, поскольку они зависят от используемой системы.
Большинство программистов хорошо знакомы с "библиотечны- ми" функциями для ввода
и вывода /GETCHAR , PUTCHAR/ и для численных расчетов /SIN, COS, SQRT/. В этой главе
мы сообщим больше о написании новых функций.
4.1. Основные сведения
Для начала давайте разработаем и составим программу пе- чати каждой строки ввода,
которая содержит определенную ком- бинацию символов. /Это - специальный случай утилиты
GREP системы "UNIX"/. Например, при поиске комбинации "THE" в на- боре строк
NOW IS THE TIME
FOR ALL GOOD
MEN TO COME TO THE AID
OF THEIR PARTY в качестве выхода получим
NOW IS THE TIME
MEN TO COME TO THE AID
OF THEIR PARTY
основная схема выполнения задания четко разделяется на три части:
WHILE (имеется еще строка)
IF (строка содержит нужную комбинацию)
вывод этой строки
Конечно, возможно запрограммировать все действия в виде одной основной процедуры,
но лучше использовать естественную структуру задачи и представить каждую часть в
виде отдельной функции. С тремя маленькими кусками легче иметь дело, чем с одним
большим, потому что отдельные не относящиеся к сущест- ву дела детали можно включить
в функции и уменьшить возмож- ность нежелательных взаимодействий. Кроме того, эти
куски могут оказаться полезными сами по себе.
"Пока имеется еще строка" - это GETLINE, функция, кото- рую мы запрограммировали
в главе 1, а "вывод этой строки" - это функция PRINTF, которую уже кто-то подготовил
для нас. Это значит, что нам осталось только написать процедуру для определения,
содержит ли строка данную комбинацию символов или нет. Мы можем решить эту проблему,
позаимствовав разра- ботку из PL/1: функция INDEX(S,т) возвращает позицию, или индекс,
строки S, где начинается строка T, и -1, если S не содержит т . В качестве начальной
позиции мы используем 0, а не 1, потому что в языке "C" массивы начинаются с позиции
нуль. Когда нам в дальнейшем понадобится проверять на совпа- дение более сложные
конструкции, нам придется заменить толь- ко функцию INDEX; остальная часть программы
останется той же самой.
После того, как мы потратили столько усилий на разработ- ку, написание программы
в деталях не представляет затрудне- ний. ниже приводится целиком вся программа,
так что вы може- те видеть, как соединяются вместе отдельные части. Комбина- ция
символов, по которой производится поиск, выступает пока в качестве символьной строки
в аргументе функции INDEX, что не является самым общим механизмом. Мы скоро вернемся
к об- суждению вопроса об инициализации символьных массивов и в главе 5 покажем,
как сделать комбинацию символов параметром, которому присваивается значение в ходе
выполнения программы. Программа также содержит новый вариант функции GETLINE; вам
может оказаться полезным сравнить его с вариантом из главы 1.
#DEFINE MAXLINE 1000 MAIN() /* FIND ALL LINES MATCHING A PATTERN */ {
CHAR LINE[MAXLINE];
WHILE (GETLINE(LINE, MAXLINE) > 0)
IF (INDEX(LINE, "THE") >= 0)
PRINTF("%S", LINE);
} GETLINE(S, LIM) /* GET LINE INTO S, RETURN LENGTH *
CHAR S[];
INT LIM;
{
INT C, I;
I = 0; WHILE(--LIM>0 && (C=GETCHAR()) != EOF && C != '\N')
S[I++] = C;
IF (C == '\N')
S[I++] = C;
S[I] = '\0';
RETURN(I);
}
INDEX(S,T) /* RETURN INDEX OF T IN S,-1 IF NONE */
CHAR S[], T[];
{
INT I, J, K;
FOR (I = 0; S[I] != '\0'; I++) {
FOR(J=I, K=0; T[K] !='\0' && S[J] == T[K]; J++; K++)
;
IF (T[K] == '\0')
RETURN(I);
}
RETURN(-1);