Б. Керриган, Д. Ритчи Язык программирования C. Б. Керниган, Д. зык программирования и . Издание 3е, исправленное Перевод с английского под редакцией Вс. С. Штаркмана СанктПетербург 2003
Скачать 31.48 Mb.
|
Основные сведения о функциях ___ 95 Чтобы решить эту задачу, мы напишем функцию кото- рая указывает место (индекс) в строке s, где начинается строка t, или если s не содержит Так как в Си нумерация элементов в массивах начи- нается с нуля, отрицательное число -1 подходит в качестве признака не- удачного поиска. Если далее нам потребуется более сложное отождеств- ление по образцу, мы просто заменим st ri ndex на другую функцию, оста- вив при этом остальную часть программы без изменений. функция st rst г аналогична функции st rindex и отличается от последней только что возвращает не индекс, а указатель.) После такого проектирования ее "деталировка" оказывает- ся очевидной. Мы имеем представление о программе в целом и знаем, как взаимодействуют ее части. В нашей программе образец для поиска зада- ется строкой-литералом, что снижает ее универсальность. В главе 5 мы еще вернемся к проблеме инициализации символьных массивов и пока- жем, как образец сделать параметром, устанавливаемым при запуске про- граммы. Здесь приведена несколько измененная версия функции getline, и было бы поучительно сравнить ее с версией, рассмотренной в главе 1. 1000 /* максимальный размер вводимой строки int getline(char line[], int max); int char char pattern[] = /* образец для поиска */ /* найти все строки, содержащие образец */ { char int found = 0; while MAXLINE) > 0) if (strindex(line, pattern) >= 0) { printf line); found++; } return found; /* getline: читает строку в s, возвращает длину */ int getline(char int { int 96 _ Глава 4. Функции и структура программы i = 0; while > 0 && != EOF && с != s[i++] = с; if (с == s [i++] = c; s[i] = return /* вычисляет место t в s или выдает -1, если t нет в s */ int strindex (char s[], char { int i, j, k; for (i = 0; s[i] != i++) { for (j = i, k = 0; t[k] != && s[j] == j++, k++) if (k > 0 && t[k] == return i; } return Определение любой функции имеет следующий вид: имя-функции аргументов) { объявления и инструкции } Отдельные части определения могут отсутствовать, например, в опре- делении "минимальной" функции {} которая ничего не вычисляет и ничего не возвращает. Такая ничего не де- лающая функция в процессе разработки программы бывает полезна в ка- честве "хранителя места". Если тип результата опущен, то предполага- ется, что функция возвращает значение типа int. Любая программа - это просто совокупность определений переменных и функций. Связи между функциями осуществляются через аргументы, возвращаемые значения и внешние переменные. В исходном файле функ- ции могут располагаться в любом порядке; исходную программу можно разбивать на любое число файлов, но так, чтобы ни одна из функций не оказалась разрезанной. Основные сведения о функциях 97 Инструкция return реализует механизм возврата результата от вызы- ваемой функции к вызывающей. За словом может следовать лю- бое выражение: return Если потребуется, выражение будет приведено к возвращаемому типу функции. Часто выражение заключают в скобки, но они не обязательны. Вызывающая функция вправе проигнорировать возвращаемое значе- ние. Более того, выражение в retu rn может отсутствовать, и тогда вообще никакое значение не будет возвращено в вызывающую функцию. Управ- ление возвращается в вызывающую функцию без результирующего зна- чения также и в том случае, когда вычисления достигли "конца" (т. е. по- следней закрывающей фигурной скобки функции). Не запрещена (но долж- на вызывать настороженность) ситуация, когда в одной и той же функ- ции одни rn имеют при себе выражения, а другие - не имеют. Во всех случаях, когда функция "забыла" передать результат в retu rn, она обяза- тельно выдаст "мусор". Функция main в программе поиска по образцу возвращает в качестве результата количество найденных строк. Это число доступно той среде, из которой данная программа была вызвана. Механизмы компиляции и загрузки расположенных в нескольких исходных файлах, в разных системах могут различаться. В системе UNIX, например, эти работы выполняет упомянутая в главе 1 команда Предположим, что три функции нашего последнего примера расположены в трех разных файлах: с, с. Тогда команда скомпилирует указанные файлы, поместив результат компиляции в фай- лы модулей о, get о и о, и затем загрузит их в исполняемый файл out. Если обнаружилась ошибка, например в файле с, то его можно скомпилировать снова и результат загру- зить ранее полученными объектными файлами, выполнив следующую команду: Команда использует стандартные расширения файлов с" о", что- бы отличать исходные файлы от объектных. Упражнение 4.1. Напишите функцию s t r i n d e x ( s , t), которая выдает позицию самого правого вхождения t в s или -1, если вхождения не об- наружено. 1116 98 _ Глава 4. Функции и структура программы 4.2. Функции, возвращающие нецелые значения В предыдущих примерах функции либо вообще не возвращали резуль- тирующих значений (void), либо возвращали значения типа int. А как быть, когда результат функции должен иметь другой тип? Многие вы- числительные функции, как, например, sq rt, sin и cos, возвращают значе- ния типа double; другие специальные функции могут выдавать значения еще каких-то типов. Чтобы проиллюстрировать, каким образом функция может возвратить нецелое значение, напишем функцию atof которая переводит строку s в соответствующее число с плавающей точкой двой- ной точности. Функция atof представляет собой расширение функции atoi, две версии которой были рассмотрены в главах 2 и 3. Она имеет де- ло со знаком (которого может и не быть), с десятичной точкой, а также с целой и дробной частями, одна из которых может отсутствовать. Наша версия не является высококачественной программой преобразования вво- димых чисел; такая программа потребовала бы заметно больше памяти. Функция atof входит в стандартную библиотеку программ; ее описание содержится в заголовочном файле Прежде всего отметим, что объявлять тип возвращаемого значения должна сама так как этот тип не есть int. Указатель типа задается перед именем функции. /* преобразование строки s double */ double atof (char { double power; int i, sign; (i 0; isspace i++) ; игнорирование левых символов-разделителей */ sign = (s[i] == ? -1 : 1; if == for (val = 0.0; isdigit i val = 10.0 * val + (s[i] - if for (power = 1.0; i++ val = 10.0 * val + (s[i] - 4.2. Функции, возвращающие нецелые значения 99 power *= , f - return sign * / power; Кроме того, важно, чтобы вызывающая программа знала, что atof возвращает нецелое значение. Один из способов обеспечить это - явно описать atof в вызывающей программе. Подобное описание демонстри- руется ниже в программе простенького калькулятора (достаточного для проверки баланса чековой книжки), который каждую вводимую строку воспринимает как число, прибавляет его к текущей сумме и печатает ее новое значение. MAXLINE 100 /* примитивный калькулятор */ double sum, atof char int getline (char line[], int max); sum = 0; while MAXLINE) > 0) sum += return 0; В объявлении - double sum, atof говорится, что sum - переменная типа double, a atof - функция, которая принимает один аргумент типа ] и возвращает результат типа double. Объявление и определение функции atof должны соответствовать друг другу. Если в одном исходном файле сама функция atof и обращение к ней в main имеют разные типы, то это несоответствие будет зафиксиро- вано компилятором как ошибка. Но если функция atof была скомпили- рована отдельно (что более вероятно), то несоответствие типов не будет обнаружено, и atof возвратит значение типа double, которое функция main воспримет как int, что приведет к бессмысленному результату. Это последнее утверждение, вероятно, вызовет у вас удивление, по- скольку ранее говорилось о необходимости соответствия объявлений Глава 4. Функции и структура программы и определений. Причина несоответствия, возможно, будет следствием того, что вообще отсутствует прототип функции, и функция неявно объяв- ляется при первом своем появлении в выражении, как, например, в sum += atof(line) Если в выражении встретилось имя, нигде ранее не объявленное, за кото- рым следует открывающая скобка, то такое имя по контексту считается именем функции, возвращающей результат типа при этом относи- тельно ее аргументов ничего не предполагается. Если в объявлении функ- ции аргументы не указаны, как в double то и в этом случае что ничего об аргументах atof не известно, и все проверки на соответствие ее параметров будут выключены. Предпо- лагается, что такая специальная интерпретация пустого списка позволит новым компиляторам транслировать старые Си-программы. Но в новых программах пользоваться этим - не очень хорошая идея. Если у функции есть аргументы, опишите их, если их нет, используйте слово void. Располагая соответствующим образом описанной функцией мы можем написать функцию atoi, преобразующую строку символов в целое значение, следующим образом: /* преобразование строки s в int с помощью atof */ int atoi { double atof (char return (int) atof (s); } Обратите внимание на вид объявления и инструкции return. Значение выражения в выражение; перед тем, как оно будет возвращено в качестве результата, приводится к типу функции. Следовательно, поскольку функция atoi возвращает зна- чение int, результат вычисления atof типа double в инструкции r e t u r n автоматически преобразуется в тип int. При преобразовании возможна потеря информации, и некоторые компиляторы предупреждают об этом. Оператор приведения явно указывает на необходимость преобразования типа и подавляет любое предупреждающее сообщение. Упражнение 4.2. Дополните функцию atof таким чтобы она справлялась с числами вида 4.3. Внешние переменные 101 в которых после мантиссы может стоять е (или Е) с последующим поряд- ком (быть может, со 4.3. Внешние переменные Программа на Си обычно оперирует с множеством внешних объектов: переменных и функций. Прилагательное "внешний" противо- положно прилагательному "внутренний", которое относится к аргумен- там и переменным, определяемым внутри функций. Внешние перемен- ные определяются вне функций и потенциально доступны для многих функций. Сами функции всегда являются внешними объектами, посколь- ку в Си запрещено определять функции внутри других функций. По умол- чанию одинаковые внешние имена, используемые в разных от- носятся к одному и тому же внешнему объекту (функции). (В стандарте это редактированием внешних связей (external В этом смысле внешние переменные похожи на области COMMON в фортране и на переменные самого внешнего блока в Паскале. Позже мы покажем, как внешние функции и переменные сделать видимыми только внутри одно- го исходного файла. Поскольку внешние переменные доступны всюду, их можно использо- вать в качестве связующих данных между функциями как альтернативу связей через аргументы и возвращаемые значения. Для любой функции внешняя переменная доступна по ее имени, если это имя было должным образом объявлено. Если число переменных, совместно используемых функциями, вели- ко, связи между последними через внешние переменные могут оказаться более удобными и эффективными, чем длинные списки аргументов. как отмечалось в главе 1, к этому заявлению следует относиться крити- чески, поскольку такая практика ухудшает структуру программы и при- водит к слишком большому числу связей между функциями по данным. Внешние переменные полезны, так как они имеют большую область действия и время жизни. Автоматические переменные существуют толь- ко внутри функции, они возникают в момент входа в функцию и исчеза- ют при выходе из нее. Внешние переменные, напротив, существуют по- стоянно, так что их значения сохраняются и между обращениями к функ- циям. Таким образом, если двум функциям приходится пользоваться од- ними и теми же данными и ни одна из них не вызывает другую, то часто ' ужо и русский слово — Примеч. ред. 1.02 Глава 4. Функции и структура программы бывает удобно оформить эти общие данные в виде внешних переменных, а не передавать их в функцию и обратно через аргументы. В связи с приведенными рассуждениями разберем пример. Поставим себе задачу написать программу-калькулятор, понимающую операторы +, -, * и /. Такой калькулятор легче будет написать, если ориентироваться на польскую, а не инфиксную запись выражений. (Обратная польская запись применяется в некоторых карманных калькуляторах и в таких языках, как Forth и Postscript.) В обратной польской записи каждый оператор следует за своими опе- рандами. Выражение в инфиксной записи, скажем (1 - 2) * (4 + 5) в польской записи представляется как Скобки не нужны, неоднозначности в вычислениях не поскольку известно, сколько операндов требуется для каждого оператора. Реализовать нашу программу весьма просто. Каждый операнд посыла- ется в стек; если встречается оператор, то из стека берется соответству- ющее число операндов (в случае бинарных операторов два) и выполняет- ся операция, после чего результат посылается в стек. В нашем примере числа 1 и 2 посылаются в стек, затем замещаются на их разность Далее в стек посылаются числа 4 и 5, которые затем заменяются их суммой (9). Числа -1 и 9 заменяются в стеке их произведением (т. е. -9). Встретив сим- вол новой строки, программа извлекает значение из стека и печатает его. Таким образом, программа состоит из цикла, обрабатывающего на каж- дом своем шаге очередной встречаемый оператор или операнд: while элемент не if (число) его в стек else if (оператор) из стека операнды операцию результат в стек else if взять с вершины стека число и else ошибка Операции "послать в стек" и "взять из стека" сами по себе тривиальны, однако по мере добавления к ним механизмов обнаружения и нейтра- лизации ошибок становятся достаточно длинными. Поэтому их лучше 4.3. Внешние переменные _ юз оформить в виде отдельных функций, чем повторять соответствующий код по всей программе. И конечно необходимо иметь отдельную функ- цию для получения очередного оператора или операнда. Главный вопрос, который мы еще не рассмотрели, - это вопрос о том, где расположить стек и каким функциям разрешить к нему прямой доступ. Стек можно расположить в функции main и передавать сам стек и текущую позицию в нем в качестве аргументов функциям push ("по- слать в стек") и pop ("взять из стека"). Но функции main нет дела до пере- менных, относящихся к стеку, - ей нужны только операции по помеще- нию чисел в стек и извлечению их оттуда. Поэтому мы решили стек и свя- занную с ним информацию хранить во внешних переменных, доступных для функций push и pop, но не доступных для Переход от эскиза к программе достаточно легок. Если теперь му представить как текст, расположенный в одном исходном файле, она будет иметь следующий вид: /* могут быть в любом количестве */ /* могут быть любом количестве */ объявления функций для main внешние переменные для push и pop void push (double f) double pop (void) int getop(char подпрограммы, вызываемые функцией getop Позже мы как текст этой программы можно разбить на два или большее число файлов. Функция main - это цикл, содержащий большой переключатель switch, передающий управление на ту или иную ветвь в зависимости от типа опе- ратора или операнда. Здесь представлен более типичный случай примене- ния переключателя switch по сравнению с рассмотренным в параграфе 3.4. MAXOP 100 /* размер операнда */ NUMBER /* признак числа */ 104 Глава 4. Функции и структура программы int getop (char []); void push (double); double pop (void); /* калькулятор с обратной польской записью */ main () { int type; double op2; char while = getop (s)) != EOF) { switch (type) { case NUMBER: push (atof break; case push (pop() + break; case push () * pop ()); break; case op2 = pop (); push (pop () - op2); break; case : op2 = pop (); if (op2 != 0.0) push (pop () / op2); else деление на break; case : ()); break; неизвестная операция break; return 0; } 4.3. Внешние переменные _ 105 Так как операторы + и * коммутативны, порядок, в котором операнды бе- рутся из стека, не важен, однако в случае операторов - и /, левый и пра- вый операнды должны различаться. Так, в - /* НЕПРАВИЛЬНО */ очередность обращения к pop не определена. Чтобы гарантировать пра- вильную очередность, необходимо первое значение из стека присвоить временной переменной, как это и сделано в main. MAXVAL 100 /* максимальная глубина стека */ int sp = 0; /* следующая свободная позиция стеке */ double /* стек */ /* push: положить значение f стек */ void f) { if (sp < MAXVAL) = f; else printf( "ошибка: стек полон, %g не f); /* pop: взять с вершины стека и выдать в качестве результата */ double pop(void) { if (sp > 0) return else { стек return 0.0; Переменная считается внешней, если она определена вне функции. Та- ким образом, стек и индекс стека, которые должны быть доступны и для push, и для pop, определяются вне этих функций. Но main не использует ни стек, ни позицию в стеке, и поэтому их представление может быть скры- то от main. Займемся реализацией getop - функции, получающей следующий опе- ратор или операнд. Нам предстоит решить довольно простую задачу. Более точно: требуется пропустить пробелы и табуляции; если следующий сим- вол - не цифра и не десятичная точка, то нужно выдать его; в противном _ Глава 4. Функции и структура программы случае надо накопить строку цифр с десятичной точкой, если она и выдать число NUMBER в качестве результата. int void /* getop: получает следующий оператор или операнд */ int int i, c; while = с = с == s[1] if && с return с; /* не число */ i = 0; if /* накапливаем целую часть */ while (isdigit(s[++i] = с = if (с /* накапливаем дробную часть */ while = с = = if (c != EOF) return NUMBER; Как работают функции get с h и ungetch? Во многих случаях программа не может "сообразить", прочла ли она все, что требуется, пока не прочтет лишнего. Так, накопление числа производится до тех пор, пока не встре- тится символ, отличный от цифры. Но это означает, что программа про- чла на один символ больше, чем нужно, и последний символ нельзя чать в число. Эту проблему можно было бы решить при наличии обратной чтению операции с помощью которой можно было бы вернуть ненужный символ. Тогда каждый раз, когда программа считает на один символ больше, чем требуется, эта операция возвращала бы его вводу, и остальная часть программы могла бы вести себя так, будто этот символ |