Пример 4-6. Как и можно было ожидать, цикл do-while используется значительно реже, чем while и for, составляя примерно пять процентов от всех циклов. Тем не менее, иногда он оказывается полезным, как, например, в следующей функции itoa, которая преобразует число в символьную строку (обратная функции atoi). Эта задача оказывается несколько более сложной, чем может показаться сначала. Дело в том, что простые методы выделения цифр генерируют их в неправильном порядке. Мы предпочли получить строку в обратном порядке, а затем обратить ее.
itoa(int n, char s[]) // Преобразование n в строку s
{
int i, sign;
if ((sign = n) < 0) // Сохраняем знак
n = -n; // Делаем n > 0
i = 0;
do // Генерируем цифры
{ // в обратном порядке
s[i++] = n % 10 + '0'; // Следующая цифра
}
while ((n /=10) > 0); // Исключить её
if (sign < 0)
s[i++] = '-'
s[i] = '\0';
reverse(s);
}
Цикл do-while здесь необходим, или по крайней мере удобен, поскольку, каково бы ни было значение n, массив s должен содержать хотя бы один символ. Мы заключили в фигурные скобки один оператор, составляющий тело do-while, хотя это и не обязательно, для того, чтобы торопливый читатель не принял часть while за начало оператора цикла while.
Упражнение 4-3. При представлении чисел в двоичном дополнительном коде наш вариант itoa не справляется с наибольшим отрицательным числом, т.е. cо значением n, определяемым из соотношения:
,
где m – размер слова. Объясните почему. Измените программу так, чтобы она правильно печатала это значение на любой машине.
Упражнение 4-4. Напишите аналогичную функцию itob(n,s), которая преобразует целое без знака n в его двоичное символьное представление в s. Запрограммируйте функцию itoh, которая преобразует целое в шестнадцатеричное представление.
Упражнение 4-5. Напишите вариант iтоа, который имеет три, а не два аргумента. Третий аргумент – минимальная ширина поля; преобразованное число должно, если это необходимо, дополняться слева пробелами, так чтобы оно имело достаточную ширину.
Иногда бывает удобным иметь возможность управлять выходом из цикла иначе, чем проверкой условия в начале или в конце. Оператор break позволяет выйти из операторов for, while и do до окончания цикла точно так же, как и из переключателя. Оператор break приводит к немедленному выходу из самого внутреннего охватывающего его цикла (или переключателя).
Пример 4-7. Следующая программа удаляет хвостовые пробелы и табуляции из конца каждой строки файла ввода. Она использует оператор break для выхода из цикла, когда найден крайний правый отличный от пробела и табуляции символ.
#define maxline 1000
main() // Удалить пробелы, табуляции и новые строки
{
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 родственен оператору BRеак, но используется реже; он приводит к началу следующей итерации охватывающего цикла (for, while, do ). В циклах while и do это означает непосредственный переход к выполнению проверочной части; в цикле for управление передается на шаг реинициализации.
Оператор continue применяется только в циклах, но не в переключателях. Оператор continue внутри цикла, включенного внутрь переключателя, вызывает только выполнение следующей итерации цикла, но не выход из переключателя.
В качестве примера приведем фрагмент, который обрабатывает только положительные элементы массива a; отрицательные значения пропускаются.
for (i = 0; i < n; i++)
{
if (a[i] < 0) // Пропуск отрицательного элемента
continue;
... // Обработка положительного элемента
}
Оператор continue часто используется, когда последующая часть цикла оказывается слишком сложной, так что рассмотрение условия, обратного проверяемому, приводит к слишком глубокому уровню вложенности программы.
Упражнение 4-6. Напишите программу копирования ввода на вывод, с тем исключением, что из каждой группы последовательных одинаковых строк выводится только одна. (Это простой вариант утилиты Uniq системы Unix).
В языке «C» предусмотрен и оператор goto, которым бесконечно злоупотребляют, и метки для ветвления. С формальной точки зрения оператор GOTO никогда не является необходимым, и на практике почти всегда можно обойтись без него. Мы не использовали goto в этой книге.
Тем не менее, мы укажем несколько ситуаций, где оператор goto может найти свое место. Наиболее характерным является его использование тогда, когда нужно прервать выполнение в некоторой глубоко вложенной структуре, например, выйти сразу из двух циклов. Здесь нельзя непосредственно использовать оператор break, так как он прерывает только самый внутренний цикл. Поэтому:
for ( ... )
for ( ... )
{
...
if (disaster)
goto error;
}
...
error:
... //Ликвидировать беспорядок
Если программа обработки ошибок нетривиальна и ошибки могут возникать в нескольких местах, то такая организация оказывается удобной. Метка имеет такую же форму, что и имя переменной, и за ней всегда следует двоеточие. Метка может быть приписана к любому оператору той же функции, в которой находится оператор goto.
В качестве другого примера рассмотрим задачу нахождения первого отрицательного элемента в двумерном массиве. (Многомерные массивы рассматриваются в главе 6). Вот одна из возможностей:
for (i = 0; i < n; i++)
for (j = 0; j < m; j++)
if (v[i][j] < 0)
goto found;
... // Не найден
found:
... // Найден в позиции 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)
... // Найден в позиции i-1, j-1
else
... // Не обнаружен
Хотя мы не являемся в этом вопросе догматиками, нам все же кажется, что если и использовать оператор goto, то нужно это делать весьма умеренно и осторожно, либо вообще обходиться без goto (как это делает большинство грамотных программистов!).
5. ФУНКЦИИ И СТРУКТУРА ПРОГРАММ
Функции разбивают большие вычислительные задачи на маленькие подзадачи и позволяют использовать в работе то, что уже сделано другими, а не начинать каждый раз с пустого места. Соответствующие функции часто могут скрывать в себе детали проводимых в разных частях программы операций, знать которые нет необходимости, проясняя тем самым всю программу, как целое, и облегчая мучения при внесении изменений.
Язык «C» разрабатывался со стремлением сделать функции эффективными и удобными для использования; «C»-программы обычно состоят из большого числа маленьких функций, а не из нескольких больших. Программа может размещаться в одном или нескольких исходных файлах любым удобным образом; исходные файлы могут компилироваться отдельно и загружаться вместе наряду со скомпилированными ранее функциями из библиотек. Мы здесь не будем вдаваться в детали этого процесса, поскольку они зависят от используемой системы.
Большинство программистов хорошо знакомы с «библиотечными» функциями для ввода и вывода (getchar, putchar и др.) и для численных расчетов (sin, cos, sqrt и др.). В этой главе мы сообщим больше о написании новых функций.
Для начала давайте разработаем и составим программу печати каждой строки ввода, которая содержит определенную комбинацию символов (это – специальный случай утилиты 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, функция, которую мы запрограммировали в главе 2, а «вывод этой строки» – это функция printf, которую уже кто-то подготовил для нас. Это значит, что нам осталось только написать процедуру для определения, содержит ли строка данную комбинацию символов или нет. Мы можем решить эту проблему, позаимствовав разработку из PL/1: функция index(s,t) возвращает позицию, или индекс, строки s, где начинается строка t, и -1, если s не содержит t . В качестве начальной позиции мы используем 0, а не 1, потому что в языке «C» массивы начинаются с позиции нуль. Когда нам в дальнейшем понадобится проверять на совпадение более сложные конструкции, нам придется заменить только функцию index; остальная часть программы останется той же самой.