ЯЗЫК С
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 (строка содержит нужную комбинацию)
вывод этой строки
- 74 -
Конечно, возможно запрограммировать все действия в виде
одной основной процедуры, но лучше использовать естественную
структуру задачи и представить каждую часть в виде отдельной
функции. С тремя маленькими кусками легче иметь дело, чем с
одним большим, потому что отдельные не относящиеся к сущест-
ву дела детали можно включить в функции и уменьшить возмож-
ность нежелательных взаимодействий. Кроме того, эти куски
могут оказаться полезными сами по себе.
"Пока имеется еще строка" - это 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);
}
- 75 -
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);
}
Каждая функция имеет вид имя (список аргументов, если они
имеются) описания аргументов, если они имеются
{
описания и операторы , если они имеются
}
Как и указывается, некоторые части могут отсутство-
вать; минимальной функцией является
DUMMY () { }
которая не совершает никаких действий.
/Такая ничего не делающая функция иногда оказывается
удобной для сохранения места для дальнейшего развития прог-
раммы/. если функция возвращает что-либо отличное от целого
значения, то перед ее именем может стоять указатель типа;
этот вопрос обсуждается в следующем разделе.
- 76 -
Программой является просто набор определений отдельных
функций. Связь между функциями осуществляется через аргумен-
ты и возвращаемые функциями значения /в этом случае/; ее
можно также осуществлять через внешние переменные. Функции
могут располагаться в исходном файле в любом порядке, а сама
исходная программа может размещаться на нескольких файлах,
но так, чтобы ни одна функция не расщеплялась.
Оператор RETURN служит механизмом для возвращения зна-
чения из вызванной функции в функцию, которая к ней обрати-
лась. За RETURN может следовать любое выражение:
RETURN (выражение)
Вызывающая функция может игнорировать возвращаемое
значение, если она этого пожелает. Более того, после RETURN
может не быть вообще никакого выражения; в этом случае в вы-
зывающую программу не передается никакого значения. Управле-
ние также возвращется в вызывающую программу без передачи
какого-либо значения и в том случае, когда при выполнении мы
"проваливаемся" на конец функции, достигая закрывающейся
правой фигурной скобки. EСли функция возвращает значение из
одного места и не возвращает никакого значения из другого
места, это не является незаконным, но может быть признаком
каких-то неприятностей. В любом случае "значением" функции,
которая не возвращает значения, несомненно будет мусор. От-
ладочная программа LINT проверяет такие ошибки.
Механика компиляции и загрузки "C"-программ, располо-
женных в нескольких исходных файлах, меняется от системы к
системе. В системе "UNIX", например, эту работу выполняет
команда 'CC', упомянутая в главе 1. Предположим, что три
функции находятся в трех различных файлах с именами MAIN.с,
GETLINE.C и INDEX.с . Тогда команда
CC MAIN.C GETLINE.C INDEX.C
компилирует эти три файла, помещает полученный настраиваемый
объектный код в файлы MAIN.O, GETLINE.O и INDEX.O и загружа-
ет их всех в выполняемый файл, называемый A.OUT .
Если имеется какая-то ошибка, скажем в MAIN.C, то этот
файл можно перекомпилировать отдельно и загрузить вместе с
предыдущими объектными файлами по команде
CC MAIN.C GETLIN.O INDEX.O
Команда 'CC' использует соглашение о наименовании с ".с"
и ".о" для того, чтобы отличить исходные файлы от объектных.
Упражнение 4-1.
----------------
Составьте программу для функции RINDEX(S,T), которая
возвращает позицию самого правого вхождения т в S и -1, если
S не содержит T.
- 77 -
4.2. Функции, возвращающие нецелые значения.
До сих пор ни одна из наших программ не содержала како-
го-либо описания типа функции. Дело в том, что по умолчанию
функция неявно описывается своим появлением в выражении или
операторе, как, например, в
WHILE (GETLINE(LINE, MAXLINE) > 0)
Если некоторое имя, которое не было описано ранее, появ-
ляется в выражении и за ним следует левая круглая скобка, то
оно по контексту считается именем некоторой функции. Кроме
того, по умолчанию предполагается, что эта функция возвраща-
ет значение типа INT. Так как в выражениях CHAR преобразует-
ся в INT, то нет необходимости описывать функции, возвращаю-
щие CHAR. Эти предположения покрывают большинство случаев,
включая все приведенные до сих пор примеры.
Но что происходит, если функция должна возвратить значе-
ние какого-то другого типа ? Многие численные функции, такие
как SQRT, SIN и COS возвращают DOUBLE; другие специальные
функции возвращают значения других типов. Чтобы показать,
как поступать в этом случае, давайте напишем и используем
функцию ATоF(S), которая преобразует строку S в эквивалент-
ное ей плавающее число двойной точности. Функция ATоF явля-
ется расширением атоI, варианты которой мы написали в главах
2 и 3; она обрабатывает необязательно знак и десятичную точ-
ку, а также целую и дробную часть, каждая из которых может
как присутствовать, так и отсутствовать./эта процедура пре-
образования ввода не очень высокого качества; иначе она бы
заняла больше места, чем нам хотелось бы/.
Во-первых, сама ATоF должна описывать тип возвращаемого
ею значения, поскольку он отличен от INT. Так как в выраже-
ниях тип FLOAT преобразуется в DOUBLE, то нет никакого смыс-
ла в том, чтобы ATOF возвращала FLOAT; мы можем с равным ус-
пехом воспользоваться дополнительной точностью, так что мы
полагаем, что возвращаемое значение типа DOUBLE. Имя типа
должно стоять перед именем функции, как показывается ниже:
DOUBLE ATOF(S) /* CONVERT STRING S TO DOUBLE */
CHAR S[];
{
DOUBLE VAL, POWER;
INT I, SIGN;
- 78 -
FOR(I=0; S[I]==' ' \!\! S[I]=='\N' \!\! S[I]=='\T'; I++)
; /* SKIP WHITE SPACE */
SIGN = 1;
IF (S[I] == '+' \!\! S[I] == '-') /* SIGN */
SIGN = (S[I++] == '+') ? 1 : -1;
FOR (VAL = 0; S[I] >= '0' && S[I] = '0' && S[I] 0)
PRINTF("\T%.2F\N",SUM+=ATOF(LINE));
Оисание
DOUBLE SUM, ATOF();
говорит, что SUM является переменной типа DOUBLE , и что
ATOF является функцией, возвращающей значение типа DOUBLE .
Эта мнемоника означает, что значениями как SUM, так и
ATOF(...) являются плавающие числа двойной точности.
- 79 -
Если функция ATOF не будет описана явно в обоих местах,
то в "C" предполагается, что она возвращает целое значение,
и вы получите бессмысленный ответ. Если сама ATOF и обраще-
ние к ней в MAIN имеют несовместимые типы и находятся в од-
ном и том же файле, то это будет обнаружено компилятором. Но
если ATOF была скомпилирована отдельно /что более вероятно/,
то это несоответствие не будет зафиксировано, так что ATOF
будет возвращать значения типа DOUBLE, с которым MAIN будет
обращаться, как с INT , что приведет к бессмысленным резуль-
татам. /Программа LINT вылавливает эту ошибку/.
Имея ATOF, мы, в принципе, могли бы с ее помощью напи-
сать ATOI (преобразование строки в INT):
ATOI(S) /* CONVERT STRING S TO INTEGER */
CHAR S[];
{
DOUBLE ATOF();
RETURN(ATOF(S));
}
Обратите внимание на структуру описаний и оператор RETURN.
Значение выражения в
RETURN (выражение)
всегда преобразуется к типу функции перед выполнением самого
возвращения. Поэтому при появлении в операторе RETURN значе-
ние функции атоF, имеющее тип DOUBLE, автоматически преобра-
зуется в INT, поскольку функция ATOI возвращает INT. (Как
обсуждалось в главе 2, преобразование значения с плавающей
точкой к типу INT осуществляется посредством отбрасывания
дробной части).
Упражнение 4-2.
----------------
Расширьте ATOF таким образом, чтобы она могла работать с
числами вида
123.45е-6
где за числом с плавающей точкой может следовать 'E' и пока-
затель экспоненты, возможно со знаком.
4.3. Еще об аргументах функций.
В главе 1 мы уже обсуждали тот факт , что аргументы фун-
кций передаются по значению, т.е. вызванная функция получает
свою временную копию каждого аргумента, а не его адрес. это
означает, что вызванная функция не может воздействовать на
исходный аргумент в вызывающей функции. Внутри функции каж-
дый аргумент по существу является локальной переменной, ко-
торая инициализируется тем значением, с которым к этой функ-
ции обратились.
- 80 -
Если в качестве аргумента функции выступает имя массива,
то передается адрес начала этого массива; сами элементы не
копируются. Функция может изменять элементы массива, исполь-
зуя индексацию и адрес начала. Таким образом, массив переда-
ется по ссылке. В главе 5 мы обсудим, как использование ука-
зателей позволяет функциям воздействовать на отличные от
массивов переменные в вызывающих функциях.
Между прочим, несуществует полностью удовлетворительного
способа написания переносимой функции с переменным числом
аргументов. Дело в том, что нет переносимого способа, с по-
мощью которого вызванная функция могла бы определить, сколь-
ко аргументов было фактически передано ей в данном обраще-
нии. Таким образом, вы, например, не можете написать дейст-
вительно переносимую функцию, которая будет вычислять макси-
мум от произвольного числа аргументов, как делают встроенные
функции MAX в фортране и PL/1.
Обычно со случаем переменного числа аргументов безопасно
иметь дело, если вызванная функция не использует аргументов,
которые ей на самом деле не были переданы, и если типы сог-
ласуются. Самая распространенная в языке "C" функция с пере-
менным числом - PRINTF . Она получает из первого аргумента
информацию, позволяющую определить количество остальных ар-
гументов и их типы. Функция PRINTF работает совершенно неп-
равильно, если вызывающая функция передает ей недостаточное
количество аргументов, или если их типы не согласуются с ти-
пами, указанными в первом аргументе. Эта функция не является
переносимой и должна модифицироваться при использовании в
различных условиях.
Если же типы аргументов известны, то конец списка аргу-
ментов можно отметить, используя какое-то соглашение; напри-
мер, считая, что некоторое специальное значение аргумента
(часто нуль) является признаком конца аргументов.
4.4. Внешние переменные.
Программа на языке "C" состоит из набора внешних объек-
тов, которые являются либо переменными, либо функциями. Тер-
мин "внешний" используется главным образом в противопостав-
ление термину "внутренний", которым описываются аргументы и
автоматические переменные, определенные внурти функций.
Внешние переменные определены вне какой-либо функции и, та-
ким образом, потенциально доступны для многих функций. Сами
функции всегда являются внешними, потому что правила языка
"C" не разрешают определять одни функции внутри других. По
умолчанию внешние переменные являются также и "глобальными",
так что все ссылки на такую переменную, использующие одно и
то же имя (даже из функций, скомпилированных независимо),
будут ссылками на одно и то же. В этом смысле внешние пере-
менные аналогичны переменным COмMON в фортране и EXTERNAL в
PL/1. Позднее мы покажем, как определить внешние переменные
и функции таким образом, чтобы они были доступны не глобаль-
но, а только в пределах одного исходного файла.
- 81 -
В силу своей глобальной доступности внешние переменные
предоставляют другую, отличную от аргументов и возвращаемых
значений, возможность для обмена данными между функциями.
Если имя внешней переменной каким-либо образом описано, то
любая функция имеет доступ к этой переменной, ссылаясь к ней
по этому имени.
В случаях, когда связь между функциями осуществляется с
помощью большого числа переменных, внешние переменные оказы-
ваются более удобными и эффективными, чем использование
длинных списков аргументов. Как, однако, отмечалось в главе
1, это соображение следует использовать с определенной осто-
рожностью, так как оно может плохо отразиться на структуре
программ и приводить к программам с большим числом связей по
данным между функциями.
Вторая причина использования внешних переменных связана
с инициализацией. В частности, внешние массивы могут быть
инициализированы а автоматические нет. Мы рассмотрим вопрос
об инициализации в конце этой главы.
Третья причина использования внешних переменных обуслов-
лена их областью действия и временем существования. Автома-
тические переменные являются внутренними по отношению к фун-
кциям; они возникают при входе в функцию и исчезают при вы-
ходе из нее. Внешние переменные, напротив, существуют посто-
янно. Они не появляютя и не исчезают, так что могут сохра-
нять свои значения в период от одного обращения к функции до
другого. В силу этого, если две функции используют некоторые
общие данные, причем ни одна из них не обращается к другой ,
то часто наиболее удобным оказывается хранить эти общие дан-
ные в виде внешних переменных, а не передавать их в функцию
и обратно с помощью аргументов.
Давайте продолжим обсуждение этого вопроса на большом
примере. Задача будет состоять в написании другой программы
для калькулятора, лучшей,чем предыдущая. Здесь допускаются
операции +,-,*,/ и знак = (для выдачи ответа).вместо инфикс-
ного представления калькулятор будет использовать обратную
польскую нотацию,поскольку ее несколько легче реализовать.в
обратной польской нотации знак следует за операндами; инфик-
сное выражение типа
(1-2)*(4+5)=
записывается в виде
12-45+*=
круглые скобки при этом не нужны
- 82 -
Реализация оказывается весьма простой.каждый операнд по-
мещается в стек; когда поступает знак операции,нужное число
операндов (два для бинарных операций) вынимается,к ним при-
меняется операция и результат направляется обратно в
стек.так в приведенном выше примере 1 и 2 помещаются в стек
и затем заменяются их разностью, -1.после этого 4 и 5 вво-
дятся в стек и затем заменяются своей суммой,9.далее числа
-1 и 9 заменяются в стеке на их произведение,равное -9.опе-
рация = печатает верхний элемент стека, не удаляя его (так
что промежуточные вычисления могут быть проверены).
Сами операции помещения чисел в стек и их извлечения
очень просты,но, в связи с включением в настоящую программу
обнаружения ошибок и восстановления,они оказываются доста-
точно длинными. Поэтому лучше оформить их в виде отдельных
функций,чем повторять соответствующий текст повсюду в прог-
рамме. Кроме того, нужна отдельная функция для выборки из
ввода следующей операции или операнда. Таким образом, струк-
тура программы имеет вид:
WHILE( поступает операция или операнд, а не конец
IF ( число )
поместить его в стек
еLSE IF ( операция )
вынуть операнды из стека
выполнить операцию
поместить результат в стек
ELSE
ошибка
Основной вопрос, который еще не был обсужден, заключает-
ся в том,где поместить стек, т. Е. Какие процедуры смогут
обращаться к нему непосредственно. Одна из таких возможнос-
тей состоит в помещении стека в MAIN и передачи самого стека
и текущей позиции в стеке функциям, работающим со стеком. Но
функции MAIN нет необходимости иметь дело с переменными, уп-
равляющими стеком; ей естественно рассуждать в терминах по-
мещения чисел в стек и извлечения их оттуда. В силу этого мы
решили сделать стек и связанную с ним информацию внешними
переменными , доступными функциям PUSH (помещение в стек) и
POP (извлечение из стека), но не MAIN.
Перевод этой схемы в программу достаточно прост. Ведущая
программа является по существу большим переключателем по ти-
пу операции или операнду; это, по-видимому, более характер-
ное применеие переключателя, чем то, которое было продемонс-
трировано в главе 3.
#DEFINE MAXOP 20 /* MAX SIZE OF OPERAND, OPERАTOR *
#DEFINE NUMBER '0' /* SIGNAL THAT NUMBER FOUND */
#DEFINE TOOBIG '9' /* SIGNAL THAT STRING IS TOO BIG *
- 83 -
MAIN() /* REVERSE POLISH DESK CALCULATOR */
/(
INT TUPE;
CHAR S[MAXOP];
DOUBLE OP2,ATOF(),POP(),PUSH();
WHILE ((TUPE=GETOP(S,MAXOP)) !=EOF);
SWITCH(TUPE) /(
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);
ELSE
PRINTF("ZERO DIVISOR POPPED\N");
BREAK;
CASE '=':
PRINTF("\T%F\N",PUSH(POP()));
BREAK;
CASE 'C':
CLEAR();
BREAK;
CASE TOOBIG:
PRINTF("%.20S ... IS TOO LONG\N",S)
BREAK;
/)
/)
#DEFINE MAXVAL 100 /* MAXIMUM DEPTH OF VAL STACK */
- 84 -
INT SP = 0; /* STACK POINTER */
DOUBLE VAL[MAXVAL]; /*VALUE STACK */
DOUBLE PUSH(F) /* PUSH F ONTO VALUE STACK */
DOUBLE F;
/(
IF (SP 0)
RETURN(VAL[--SP]);
ELSE /(
PRINTF("ERROR: STACK EMPTY\N");
CLEAR();
RETURN(0);
/)
/)
CLEAR() /* CLEAR STACK */
/(
SP=0;
/)
Команда C очищает стек с помощью функции CLEAR, которая
также используется в случае ошибки функциями PUSH и POP. к
функции GETOP мы очень скоро вернемся.
Как уже говорилось в главе 1, переменная является внеш-
ней, если она определена вне тела какой бы то ни было функ-
ции. Поэтому стек и указатель стека, которые должны исполь-
зоваться функциями PUSH, POP и CLEAR, определены вне этих
трех функций. Но сама функция MAIN не ссылается ни к стеку,
ни к указателю стека - их участие тщательно замаскировано. В
силу этого часть программы, соответствующая операции = , ис-
пользует конструкцию
PUSH(POP());
для того, чтобы проанализировать верхний элемент стека, не
изменяя его.
Отметим также, что так как операции + и * коммутативны,
порядок, в котором объединяются извлеченные операнды, несу-
щественен, но в случае операций - и / необходимо различать
левый и правый операнды.
- 85 -
Упражнение 4-3.
---------------
Приведенная основная схема допускает непосредственное
расширение возможностей калькулятора. Включите операцию де-
ления по модулю /%/ и унарный минус. Включите команду "сте-
реть", которая удаляет верхний элемент стека. Введите коман-
ды для работы с переменными. /Это просто, если имена пере-
менных будут состоять из одной буквы из имеющихся двадцати
шести букв/.
4.5. Правила, определяющие область действия.
Функции и внешние переменные, входящие в состав
"C"-программы, не обязаны компилироваться одновременно;
программа на исходном языке может располагаться в нескольких
файлах, и ранее скомпилированные процедуры могут загружаться
из библиотек. Два вопроса представляют интерес:
Как следует составлять описания, чтобы переменные пра-
вильно воспринимались во время компиляции ?
Как следует составлять описания, чтобы обеспечить пра-
вильную связь частей программы при загрузке ?
4.5.1. Область действия.
Областью действия имени является та часть программы, в
которой это имя определено. Для автоматической переменной,
описанной в начале функции, областью действия является та
функция, в которой описано имя этой переменной, а переменные
из разных функций, имеющие одинаковое имя, считаются не от-
носящимися друг к другу. Это же справедливо и для аргументов
функций.
Область действия внешней переменной простирается от точ-
ки, в которой она объявлена в исходном файле, до конца этого
файла. Например, если VAL, SP, PUSH, POP и CLEAR определены
в одном файле в порядке, указанном выше, а именно:
INT SP = 0;
DOUBLE VAL[MAXVAL];
DOUBLE PUSH(F) {...}
DOUBLE POP() {...}
CLEAR() {...}
то переменные VAL и SP можно использовать в PUSH, POP и
CLEAR прямо по имени; никакие дополнительные описания не
нужны.
С другой стороны, если нужно сослаться на внешнюю пере-
менную до ее определения, или если такая переменная опреде-
лена в файле, отличном от того, в котором она используется,
то необходимо описание EXTERN.
- 86 -
Важно различать описание внешней переменной и ее опреде-
ление. описание указывает свойства переменной /ее тип, раз-
мер и т.д./; определение же вызывает еще и отведение памяти.
Если вне какой бы то ни было функции появляются строчки
INT SP;
DOUBLE VAL[MAXVAL];
то они определяют внешние переменные SP и VAL, вызывают от-
ведение памяти для них и служат в качестве описания для ос-
тальной части этого исходного файла. В то же время строчки
EXTERN INT SP;
EXTERN DOUBLE VAL[];
описывают в остальной части этого исходного файла переменную
SP как INT, а VAL как массив типа DOUBLE /размер которого
указан в другом месте/, но не создают переменных и не отво-
дят им места в памяти.
Во всех файлах, составляющих исходную программу, должно
содержаться только одно определение внешней переменной; дру-
гие файлы могут содержать описания EXTERN для доступа к ней.
/Описание EXTERN может иметься и в том файле, где находится
определение/. Любая инициализация внешней переменной прово-
дится только в определении. В определении должны указываться
размеры массивов, а в описании EXTERN этого можно не делать.
Хотя подобная организация приведенной выше программы и
маловероятна, но VAL и SP могли бы быть определены и инициа-
лизированы в одном файле, а функция PUSH, POP и CLEAR опре-
делены в другом. В этом случае для связи были бы необходимы
следующие определения и описания:
в файле 1:
----------
INT SP = 0; /* STACK POINTER */
DOUBLE VAL[MAXVAL]; /* VALUE STACK */
в файле 2:
----------
EXTERN INT SP;
EXTERN DOUBLE VAL[];
DOUBLE PUSH(F) {...}
DOUBLE POP() {...}
CLEAR() {...}
так как описания EXTERN 'в файле 1' находятся выше и вне
трех указанных функций, они относятся ко всем ним; одного
набора описаний достаточно для всего 'файла 2'.
- 87 -
Для программ большого размера обсуждаемая позже в этой
главе возможность включения файлов, #INCLUDE, позволяет
иметь во всей программе только одну копию описаний EXTERN и
вставлять ее в каждый исходный файл во время его компиляции.
Обратимся теперь к функции GETOP, выбирающей из файла
ввода следующую операцию или операнд. Основная задача прос-
та: пропустить пробелы, знаки табуляции и новые строки. Если
следующий символ отличен от цифры и десятичной точки, то
возвратить его. В противном случае собрать строку цифр /она
может включать десятичную точку/ и возвратить NUMBER как
сигнал о том, что выбрано число.
Процедура существенно усложняется, если стремиться пра-
вильно обрабатывать ситуацию, когда вводимое число оказыва-
ется слишком длинным. Функция GETOP считывает цифры подряд
/возможно с десятичной точкой/ и запоминает их, пока после-
довательность не прерывается. Если при этом не происходит
переполнения, то функция возвращает NUMBER и строку цифр.
Если же число оказывается слишком длинным, то GETOP отбрасы-
вает остальную часть строки из файла ввода, так что пользо-
ватель может просто перепечатать эту строку с места ошибки;
функция возвращает TOOBIG как сигнал о переполнении.
GETOP(S, LIM) /* GET NEXT OPRERATOR OR OPERAND */
CHAR S[];
INT LIM;
{
INT I, C;
WHILE((C=GETCH())==' '\!\! C=='\T' \!\! C=='\N')
;
IF (C != '.' && (C '9'))
RETURN(C);
S[0] = C;
FOR(I=1; (C=GETCHAR()) >='0' && C ='0' && C 0) ? BUF[--BUFP] : GETCHAR());
}
UNGETCH(C) /* PUSH CHARACTER BACK ON INPUT */
INT C;
{
IF (BUFP > BUFSIZE)
PRINTF("UNGETCH: TOO MANY CHARACTERS\N");
ELSE
BUF [BUFP++] = C;
}
Мы использовали для хранения возвращаемых символов массив, а
не отдельный символ, потому что такая общность может приго-
диться в дальнейшем.
- 89 -
Упражнение 4-4.
----------------
Напишите функцию UNGETS(S) , которая будет возвращать во
ввод целую строку. Должна ли UNGETS иметь дело с BUF и BUFP
или она может просто использовать UNGETCH ?
Упражнение 4-5.
----------------
Предположите, что может возвращаться только один символ. Из-
мените GETCH и UNGETCH соответствующим образом.
Упражнение 4-6.
----------------
Наши функции GETCH и UNGETCH не обеспечивают обработку возв-
ращенного символа EOF переносимым образом. Решите, каким
свойством должны обладать эти функции, если возвращается
EOF, и реализуйте ваши выводы.
4.6. Статические переменные.
Статические переменные представляют собой третий класс
памяти, в дополнении к автоматическим переменным и EXTERN, с
которыми мы уже встречались.
Статические переменные могут быть либо внутренними, либо
внешними. Внутренние статические переменные точно так же,
как и автоматические, являются локальными для некоторой фун-
кции, но, в отличие от автоматических, они остаются сущест-
вовать, а не появляются и исчезают вместе с обращением к
этой функции. это означает, что внутренние статические пере-
менные обеспечивают постоянное, недоступное извне хранение
внутри функции. Символьные строки, появляющиеся внутри функ-
ции, как, например, аргументы PRINTF , являются внутренними
статическими.
Внешние статические переменные определены в остальной
части того исходного файла, в котором они описаны, но не в
каком-либо другом файле. Таким образом, они дают способ
скрывать имена, подобные BUF и BUFP в комбинации
GETCH-UNGETCH, которые в силу их совместного использования
должны быть внешними, но все же не доступными для пользова-
телей GETCH и UNGETCH , чтобы исключалась возможность конф-
ликта. Если эти две функции и две переменные объеденить в
одном файле следующим образом
STATIC CHAR BUF[BUFSIZE]; /* BUFFER FOR UNGETCH */
STATIC INT BUFP=0; /*NEXT FREE POSITION IN BUF */
GETCH() {...}
UNGETCH() {...}
то никакая другая функция не будет в состоянии обратиться к
BUF и BUFP; фактически, они не будут вступать в конфликт с
такими же именами из других файлов той же самой программы.
Статическая память, как внутренняя, так и внешняя, спе-
цифицируется словом STATIC , стоящим перед обычным описани-
ем. Переменная является внешней, если она описана вне какой
бы то ни было функции, и внутренней, если она описана внутри
некоторой функции.
- 90 -
Нормально функции являются внешними объектами; их имена
известны глобально. возможно, однако, объявить функцию как
STATIC ; тогда ее имя становится неизвестным вне файла, в
котором оно описано.
В языке "C" "STATIC" отражает не только постоянство, но
и степень того, что можно назвать "приватностью". Внутренние
статические объекты определены только внутри одной функции;
внешние статические объекты /переменные или функции/ опреде-
лены только внутри того исходного файла, где они появляются,
и их имена не вступают в конфликт с такими же именами пере-
менных и функций из других файлов.
Внешние статические переменные и функции предоставляют
способ организовывать данные и работающие с ними внутренние
процедуры таким образом, что другие процедуры и данные не
могут прийти с ними в конфликт даже по недоразумению. Напри-
мер, функции GETCH и UNGETCH образуют "модуль" для ввода и
возвращения символов; BUF и BUFP должны быть статическими,
чтобы они не были доступны извне. Точно так же функции PUSH,
POP и CLEAR формируют модуль обработки стека; VAR и SP тоже
должны быть внешними статическими.
4.7. Регистровые переменные.
Четвертый и последний класс памяти называется регистро-
вым. Описание REGISTER указывает компилятору, что данная пе-
ременная будет часто использоваться. Когда это возможно, пе-
ременные, описанные как REGISTER, располагаются в машинных
регистрах, что может привести к меньшим по размеру и более
быстрым программам. Описание REGISTER выглядит как
REGISTER INT X;
REGISTER CHAR C;
и т.д.; часть INT может быть опущена. Описание REGISTER мож-
но использовать только для автоматических переменных и фор-
мальных параметров функций. В этом последнем случае описания
выглядят следующим образом:
F(C,N)
REGISTER INT C,N;
{
REGISTER INT I;
...
}
- 91 -
На практике возникают некоторые ограничения на регистро-
вые переменные, отражающие реальные возможности имеющихся
аппаратных средств. В регистры можно поместить только нес-
колько переменных в каждой функции, причем только определен-
ных типов. В случае превышения возможного числа или исполь-
зования неразрешенных типов слово REGISTER игнорируется.
Кроме того невозможно извлечь адрес регистровой переменной
(этот вопрос обсуждается в главе 5). Эти специфические огра-
ничения варьируются от машины к машине. Так, например, на
PDP-11 эффективными являются только первые три описания
REGISTER в функции, а в качестве типов допускаются INT, CHAR
или указатель.
4.8. Блочная структура.
Язык "C" не является языком с блочной структурой в смыс-
ле PL/1 или алгола; в нем нельзя описывать одни функции
внутри других.
Переменные же, с другой стороны, могут определяться по
методу блочного структурирования. Описания переменных (вклю-
чая инициализацию) могут следовать за левой фигурной скоб-
кой,открывающей любой оператор, а не только за той, с кото-
рой начинается тело функции. Переменные, описанные таким об-
разом, вытесняют любые переменные из внешних блоков, имеющие
такие же имена, и остаются определенными до соответствующей
правой фигурной скобки. Например в
IF (N > 0) {
INT I; /* DECLARE A NEW I */
FOR (I = 0; I 0); /* DISCARD IT */
WHILE (--I >= 0)
PUTCHAR(S[I]);
}
Альтернативой этому способу является рекурсивное реше-
ние, когда при каждом вызове функция PRINTD сначала снова
обращается к себе, чтобы скопировать лидирующие цифры, а за-
тем печатает последнюю цифру.
PRINTD(N) /* PRINT N IN DECIMAL (RECURSIVE)*/
INT N;
(
INT I;
IF (N 0) THEN
BEGIN
A = 1;
B = 2
END
Имеется также возможность определения макроса с аргумен-
тами, так что заменяющий текст будет зависеть от вида обра-
щения к макросу. Определим, например, макрос с именем MAX
следующим образом:
#DEFINE MAX(A, B) ((A) > (B) ? (A) : (B))
когда строка
X = MAX(P+Q, R+S);
будет заменена строкой
X = ((P+Q) > (R+S) ? (P+Q) : (R+S));
Такая возможность обеспечивает "функцию максимума", которая
расширяется в последовательный код, а не в обращение к функ-
ции. При правильном обращении с аргументами такой макрос бу-
дет работать с любыми типами данных; здесь нет необходимости
в различных видах MAX для данных разных типов, как это было
бы с функциями.
- 97 -
Конечно, если вы тщательно рассмотрите приведенное выше
расширение MAX, вы заметите определенные недостатки. Выраже-
ния вычисляются дважды; это плохо, если они влекут за собой
побочные эффекты, вызванные, например, обращениями к функци-
ям или использованием операций увеличения. Нужно позаботить-
ся о правильном использовании круглых скобок, чтобы гаранти-
ровать сохранение требуемого порядка вычислений. (Рассмотри-
те макрос
#DEFINE SQUARE(X) X * X
при обращении к ней, как SQUARE(Z+1)). Здесь возникают даже
некоторые чисто лексические проблемы: между именем макро и
левой круглой скобкой, открывающей список ее аргументов, не
должно быть никаких пробелов.
Тем не менее аппарат макросов является весьма ценным.
Один практический пример дает описываемая в главе 7 стандар-
тная библиотека ввода-вывода, в которой GETCHAR и PUTCHAR
определены как макросы (очевидно PUTCHAR должна иметь аргу-
мент), что позволяет избежать затрат на обращение к функции
при обработке каждого символа.
Другие возможности макропроцессора описаны в приложении
А.
Упражнение 4-9.
---------------
Определите макрос SWAP(X, Y), который обменивает значе-
ниями два своих аргумента типа INT. (В этом случае поможет
блочная структура).
[ Назад ]
[ Далее ]