Керниган и Ритчи - Язык программирования Си (793773), страница 21
Текст из файла (страница 21)
Если в объявлении функции аргументы не указаны,как вdouble atof();то и в этом случае считается, что ничего об аргументах atof не известно, и все проверки на соответствие еепараметров будут выключены. Предполагается, что такая специальная интерпретация пустого спискапозволит новым компиляторам транслировать старые Си-программы. Но в новых программах пользоватьсяэтим — не очень хорошая идея. Если у функции есть аргументы, опишите их, если их нет, используйте словоvoid.Располагая соответствующим образом описанной функцией atof, мы можем написать функцию atoi,преобразующую строку символов в целое значение, следующим образом:/* atoi: преобразование строки s в int с помощью atof */int atoi (char s[]){double atof (char s[]);return (int) atof (s);}Обратите внимание на вид объявления и инструкции return.
Значение выражения вreturn выражение;перед тем, как оно будет возвращено в качестве результата, приводится к типу функции. Следовательно,поскольку функция atoi возвращает значение int, результат вычисления atof типа double в инструкцииreturn автоматически преобразуется в тип int.
При преобразовании возможна потеря информации, инекоторые компиляторы предупреждают об этом. Оператор приведения явно указывает на необходимостьпреобразования типа и подавляет любое предупреждающее сообщение.Упражнение 4.2. Дополните функцию atof таким образом, чтобы она справлялась с числами вида123.456e-6в которых после мантиссы может стоять е (или Е) с последующим порядком (быть может, со знаком).4.3.
Внешние переменныеПрограмма на Си обычно оперирует с множеством внешних объектов: переменных и функций.Прилагательное "внешний" (external) противоположно прилагательному "внутренний", которое относится каргументам и переменным, определяемым внутри функций. Внешние переменные определяются внефункций и потенциально доступны для многих функций. Сами функции всегда являются внешнимиобъектами, поскольку в Си запрещено определять функции внутри других функций. По умолчаниюодинаковые внешние имена, используемые в разных файлах, относятся к одному и тому же внешнемуобъекту (функции). (В стандарте это называется редактированием внешних связей (external linkage7).) В этомсмысле внешние переменные похожи на области COMMON в фортране и на переменные самого внешнегоблока в Паскале. Позже мы покажем, как внешние функции и переменные сделать видимыми только внутриодного исходного файла.Поскольку внешние переменные доступны всюду, их можно использовать в качестве связующих данныхмежду функциями как альтернативу связей через аргументы и возвращаемые значения.
Для любой функциивнешняя переменная доступна по ее имени, если это имя было должным образом объявлено.7Сейчас уже и в русский язык прочно вошло слово "линкование". — Примеч. ред.Если число переменных, совместно используемых функциями, велико, связи между последними черезвнешние переменные могут оказаться более удобными и эффективными, чем длинные списки аргументов.Но, как отмечалось в главе 1, к этому заявлению следует относиться критически, поскольку такая практикаухудшает структуру программы и приводит к слишком большому числу связей между функциями по данным.Внешние переменные полезны, так как они имеют большую область действия и время жизни.Автоматические переменные существуют только внутри функции, они возникают в момент входа в функцию иисчезают при выходе из нее.
Внешние переменные, напротив, существуют постоянно, так что их значениясохраняются и между обращениями к функциям. Таким образом, если двум функциям приходитсяпользоваться одними и теми же данными и ни одна из них не вызывает другую, то часто бывает удобнооформить эти общие данные в виде внешних переменных, а не передавать их в функцию и обратно черезаргументы.В связи с приведенными рассуждениями разберем пример.
Поставим себе задачу написать программукалькулятор, понимающую операторы +, -, * и /. Такой калькулятор легче будет написать, еслиориентироваться на польскую, а не инфиксную запись выражений. (Обратная польская запись применяется внекоторых карманных калькуляторах и в таких языках, как Forth и Postscript.)В обратной польской записи каждый оператор следует за своими операндами. Выражение в инфикснойзаписи, скажем(1 - 2) * (4 + 5)в польской записи представляется как1 2 - 4 5 + *Скобки не нужны, неоднозначности в вычислениях не бывает, поскольку известно, сколько операндовтребуется для каждого оператора.Реализовать нашу программу весьма просто. Каждый операнд посылается в стек; если встречается оператор,то из стека берется соответствующее число операндов (в случае бинарных операторов два) и выполняетсяоперация, после чего результат посылается в стек.
В нашем примере числа 1 и 2 посылаются в стек, затемзамещаются на их разность -1. Далее в стек посылаются числа 4 и 5, которые затем заменяются их суммой (9).Числа -1 и 9 заменяются в стеке их произведением (т. е. -9). Встретив символ новой строки, программаизвлекает значение из стека и печатает его.Таким образом, программа состоит из цикла, обрабатывающего на каждом своем шаге очереднойвстречаемый оператор или операнд:while (следующий элемент не конец-файла)if (число)послать его в стекelse if (оператор)взять из стека операндывыполнить операциюрезультат послать в стекelse if (новая-строка)взять с вершины стека число и напечататьelseошибкаОперации "послать в стек" и "взять из стека" сами по себе тривиальны, однако по мере добавления к ниммеханизмов обнаружения и нейтрализации ошибок становятся достаточно длинными.
Поэтому их лучшеоформить в виде отдельных функций, чем повторять соответствующий код по всей программе. И конечнонеобходимо иметь отдельную функцию для получения очередного оператора или операнда.Главный вопрос, который мы еще не рассмотрели, — это вопрос о том, где расположить стек и какимфункциям разрешить к нему прямой доступ. Стек можно расположить в функции main и передавать сам стеки текущую позицию в нем в качестве аргументов функциям push ("послать в стек") и pop ("взять из стека").Но функции main нет дела до переменных, относящихся к стеку, — ей нужны только операции по помещениючисел в стек и извлечению их оттуда.
Поэтому мы решили стек и связанную с ним информацию хранить вовнешних переменных, доступных для функций push и pop, но не доступных для main.Переход от эскиза к программе достаточно легок. Если теперь программу представить как текст,расположенный в одном исходном файле, она будет иметь следующий вид:#include /* могут быть в любом количестве */#define /* могут быть в любом количестве */объявления функций для mainmain () {...}внешние переменные для push и popvoid push (double f) {...}double pop (void) {...}int getop(char s[] ) {...}подпрограммы, вызываемые функцией getopПозже мы обсудим, как текст этой программы можно разбить на два или большее число файлов.Функция main — это цикл, содержащий большой переключатель switch, передающий управление на ту илииную ветвь в зависимости от типа оператора или операнда.
Здесь представлен более типичный случайприменения переключателя switch по сравнению с рассмотренным в параграфе 3.4.#include <stdio.h>#include <stdlib.h> /* для atof() */#define MAXOP 100 /* макс. размер операнда или оператора */#define NUMBER '0' /* признак числа */int getop (char []);void push (double);double pop (void);/* калькулятор с обратной польской записью */main (){int type;double op2;char s[MAXOP];while ((type = getop (s)) != EOF) {switch (type) {case NUMBER:push (atof (s));break;case '+':push (pop() + pop());break;case '*':push (pop() * pop());break;case '-':op2 = pop();push (pop() - op2);break;case '/' :op2 = pop();if (op2 != 0.0)push (pop() / op2);elseprintf("ошибка: деление на нуль\п");break;case '\n' :printf("\t%.8g\n", pop());break;default:printf("ошибка: неизвестная операция %s\n", s);break;}}return 0;}Так как операторы + и * коммутативны, порядок, в котором операнды берутся из стека, не важен, однако вслучае операторов - и /, левый и правый операнды должны различаться.
Так, вpush(pop() - рор()); /* НЕПРАВИЛЬНО */очередность обращения к pop не определена. Чтобы гарантировать правильную очередность, необходимопервое значение из стека присвоить временной переменной, как это и сделано в main.#define MAXVAL 100 /* максимальная глубина стека */int sp = 0; /* следующая свободная позиция в стеке */double val[ MAXVAL ]; /* стек *//* push: положить значение f в стек */void push(double f){if (sp < MAXVAL)val[sp++] = f;elseprintf( "ошибка: стек полон, %g не помещается\п", f);}/* pop: взять с вершины стека и выдать в качестве результата */double pop(void){if (sp > 0)return val[--sp];else {printf( "ошибка: стек пуст\п");return 0.0;}}Переменная считается внешней, если она определена вне функции.
Таким образом, стек и индекс стека,которые должны быть доступны и для push, и для pop, определяются вне этих функций. Но main неиспользует ни стек, ни позицию в стеке, и поэтому их представление может быть скрыто от main.Займемся реализацией getop — функции, получающей следующий оператор или операнд. Нам предстоитрешить довольно простую задачу.
Более точно: требуется пропустить пробелы и табуляции; если следующийсимвол — не цифра и не десятичная точка, то нужно выдать его; в противном случае надо накопить строкуцифр с десятичной точкой, если она есть, и выдать число NUMBER в качестве результата.#include <ctype.h>int getch(void);void ungetch(int);/* getop: получает следующий оператор или операнд */int getop(char s[]){int i, c;while ((s[0] = с = getch()) == ' ' || с == '\t' );s[1] = '\0';if (!isdigit(c) && с != '.')return с; /* не число */i = 0;if (isdigit(c)) /* накапливаем целую часть */while (isdigit(s[++i] = с = getch()));if (с == '.') /* накапливаем дробную часть */while (isdigit(s[++i] = с = getch()));s[i] = '\0';if (c != EOF)ungetch(c);return NUMBER;}Как работают функции getсh и ungetch? Во многих случаях программа не может "сообразить", прочла лиона все, что требуется, пока не прочтет лишнего.
Так, накопление числа производится до тех пор, пока невстретится символ, отличный от цифры. Но это означает, что программа прочла на один символ больше, чемнужно, и последний символ нельзя включать в число.Эту проблему можно было бы решить при наличии обратной чтению операции "положить-назад", с помощьюкоторой можно было бы вернуть ненужный символ. Тогда каждый раз, когда программа считает на одинсимвол больше, чем требуется, эта операция возвращала бы его вводу, и остальная часть программы моглабы вести себя так, будто этот символ вовсе и не читался. К счастью, описанный механизм обратной посылкисимвола легко моделируется с помощью пары согласованных друг с другом функций, из которых getchпоставляет очередной символ из ввода, a ungetch отправляет символ назад во входной поток, так что приследующем обращении к getch мы вновь его получим.Нетрудно догадаться, как они работают вместе. Функция ungetch запоминает посылаемый назад символ внекотором буфере, представляющем собой массив символов, доступный для обеих этих функций; getchчитает из буфера, если там что-то есть, или обращается к getchar, если буфер пустой.















