Язык программирования Си. Издание 3-е, исправленное - Брайан Керниган
Шрифт:
Интервал:
Закладка:
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[i])
goto found;
/* нет одинаковых элементов */
…
found:
/* обнаружено совпадение: a[i] - b[i] */
Программу нахождения совпадающих элементов можно написать и без 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 следует прибегать крайне редко, если использовать эту инструкцию вообще.
Глава 4. Функции и структура программы
Функции разбивают большие вычислительные задачи на более мелкие и позволяют воспользоваться тем, что уже сделано другими разработчиками, а не начинать создание программы каждый раз "с нуля". В выбранных должным образом функциях "упрятаны" несущественные для других частей программы детали их функционирования, что делает программу в целом более ясной и облегчает внесение в нее изменений.
Язык проектировался так, чтобы функции были эффективными и простыми в использовании. Обычно программы на Си состоят из большого числа небольших функций, а не из немногих больших. Программу можно располагать в одном или нескольких исходных файлах. Эти файлы можно компилировать отдельно, а загружать вместе, в том числе и с ранее откомпилированными библиотечными функциями. Процесс загрузки здесь не рассматривается, поскольку он различен в разных системах.
Объявление и определение функции - это та область, где стандартом ANSI в язык внесены самые существенные изменения. Как мы видели в главе 1, в описании функции теперь разрешено задавать типы аргументов. Синтаксис определения функции также изменен, так что теперь объявления и определения функций соответствуют друг другу. Это позволяет компилятору обнаруживать намного больше ошибок, чем раньше. Кроме того. если типы аргументов соответствующим образом объявлены, то необходимые преобразования аргументов выполняются автоматически.
Стандарт вносит ясность в правила, определяющие области видимости имен; в частности, он требует, чтобы для каждого внешнего объекта было только одно определение. В нем обобщены средства инициализации: теперь можно инициализировать автоматические массивы и структуры. Улучшен также препроцессор Си. Он включает более широкий набор директив условной компиляции, предоставляет возможность из макроаргументов генерировать строки в кавычках, а кроме того. содержит более совершенный механизм управления процессом макрорасширения.
4.1 Основные сведения о функциях
Начнем с того, что сконструируем программу, печатающую те строки вводимого текста, в которых содержится некоторый "образец", заданный в виде строки символов. (Эта программа представляет собой частный случай функции grep системы UNIX.) Рассмотрим пример: в результате поиска образца "ould" в строках текста
Ah Love! could you and I with Fate conspire
To grasp this sorry Scheme of Things entire,
Would not we shatter it to bits - and then
Re-mould it nearer to The Heart's Desire!
мы получим
Ah Love! could you and I with Fate conspire
Would not we shatter it to bits - and then
Re-mould it nearer to the Heart's Desire!
Работа по поиску образца четко распадается на три этапа:
while (существует еще строка)
if (строка содержит образец)
напечатать ее
Хотя все три составляющие процесса поиска можно поместить в функцию main, все же лучше сохранить приведенную структуру и каждую ее часть реализовать в виде отдельной функции. Легче иметь дело с тремя небольшими частями, чем с одной большой, поскольку, если несущественные особенности реализации скрыты в функциях, вероятность их нежелательного воздействия друг на друга минимальна. Кроме того, оформленные в виде функций соответствующие части могут оказаться полезными и в других программах.
Конструкция "while (существует еще строка)" реализована в getline (см. главу 1), а фразу "напечатать ее" можно записать с помощью готовой функции printf. Таким образом, нам остается перевести на Си только то, что определяет, входит ли заданный образец в строку.
Чтобы решить эту задачу, мы напишем функцию strindex(s,t), которая указывает место (индекс) в строке s, где начинается строка t, или -1, если s не содержит t. Так как в Си нумерация элементов в массивах начинается с нуля, отрицательное число -1 подходит в качестве признака неудачного поиска. Если далее нам потребуется более сложное отождествление по образцу, мы просто заменим strindex на другую функцию, оставив при этом остальную часть программы без изменений. (Библиотечная функция strstr аналогична функции strindex и отличается от последней только тем, что возвращает не индекс, а указатель.)
После такого проектирования программы ее "деталировка" оказывается очевидной. Мы имеем представление о программе в целом и знаем, как взаимодействуют ее части. В нашей программе образец для поиска задается строкой-литералом, что снижает ее универсальность. В главе 5 мы еще вернемся к проблеме инициализации символьных массивов и покажем, как образец сделать параметром, устанавливаемым при запуске программы. Здесь приведена несколько измененная версия функции getline, и было бы поучительно сравнить ее с версией, рассмотренной в главе 1.
#include ‹stdio.h›
#define MAXLINE 1000 /* максимальный размер вводимой строки */
int getline(char line[], int max);
int strindex(char source[], char searchfor[]);
char pattern[] ="ould"; /* образец для поиска */
/* найти все строки, содержащие образец */
main()
{
char line[MAXLINE];
int found = 0;
while (getline(line, MAXLINE) › 0)
if (strindex(line, pattern) ›= 0) {
printf ("%s", line);
found++;
}
return found;
}
/* getline: читает строку в s, возвращает длину */
int getline(char s[], int lim)
{
int c, i;
i = 0;
while (--lim › 0 && (c=getchar()) != EOF && с != 'n') /* I.B.: misprint was here -lim instead of --lim */
s[i++] = c;
if (c == 'n')
s[i++] = c;
s[i] = ' ';
return i;
}
/* strindex: вычисляет место t в s или выдает -1, если t нет в s */
int strindex (char s[], char t[])
{
int i, j, k;
for (i = 0; s[i] != ' '; i++) {
for (j = i, k = 0; t[k] != ' ' && s[j] == t[k]; j++, k++)
;
if (k › 0 && t[k] == ' ')
return i;
}
return -1;
}
Определение любой функции имеет следующий вид:
тип-результата имя-функции (объявления аргументов)
{
объявления и инструкции
}
Отдельные части определения могут отсутствовать, как, например, в определении "минимальной" функции
dummy() {}
которая ничего не вычисляет и ничего не возвращает. Такая ничего не делающая функция в процессе разработки программы бывает полезна в качестве "хранителя места". Если тип результата опущен, то предполагается, что функция возвращает значение типа int.
Любая программа - это просто совокупность определений переменных и функций. Связи между функциями осуществляются через аргументы, возвращаемые значения и внешние переменные. В исходном файле функции могут располагаться в любом порядке; исходную программу можно разбивать на любое число файлов, но так, чтобы ни одна из функций не оказалась разрезанной.