ет значение типа 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;
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] <= '9'; I++)
VAL = 10 * VAL + S[I] - '0';
IF (S[I] == '.')
I++;
FOR (POWER = 1; S[I] >= '0' && S[I] <= '9'; I++) {
VAL = 10 * VAL + S[I] - '0';
POWER *= 10;
}
RETURN(SIGN * VAL / POWER);
}
Вторым, но столь же важным, является то, что вызывающая
функция должна объявить о том, что ATOF возвращает значение,
отличное от INT типа. Такое объявление демонстрируется на
примере следующего примитивного настольного калькулятора
/едва пригодного для подведения баланса в чековой книжке/,
который считывает по одному числу на строку, причем это чис-
ло может иметь знак, и складывает все числа, печатая сумму
после каждого ввода.
#DEFINE MAXLINE 100
MAIN() /* RUDIMENTARY DESK CALKULATOR */
{
DOUBLE SUM, ATOF();
CHAR LINE[MAXLINE];
SUM = 0;
WHILE (GETLINE(LINE, MAXLINE) > 0)
PRINTF("\T%.2F\N",SUM+=ATOF(LINE));
Оисание
DOUBLE SUM, ATOF();
говорит, что SUM является переменной типа DOUBLE , и что
ATOF является функцией, возвращающей значение типа DOUBLE .
Эта мнемоника означает, что значениями как SUM, так и
ATOF(...) являются плавающие числа двойной точности.
Если функция 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 мы уже обсуждали тот факт , что аргументы фун-
кций передаются по значению, т.е. вызванная функция получает
свою временную копию каждого аргумента, а не его адрес. это
означает, что вызванная функция не может воздействовать на
исходный аргумент в вызывающей функции. Внутри функции каж-
дый аргумент по существу является локальной переменной, ко-
торая инициализируется тем значением, с которым к этой функ-
ции обратились.
Если в качестве аргумента функции выступает имя массива,
то передается адрес начала этого массива; сами элементы не
копируются. Функция может изменять элементы массива, исполь-
зуя индексацию и адрес начала. Таким образом, массив переда-
ется по ссылке. В главе 5 мы обсудим, как использование ука-
зателей позволяет функциям воздействовать на отличные от
массивов переменные в вызывающих функциях.
Между прочим, несуществует полностью удовлетворительного
способа написания переносимой функции с переменным числом
аргументов. Дело в том, что нет переносимого способа, с по-
мощью которого вызванная функция могла бы определить, сколь-
ко аргументов было фактически передано ей в данном обраще-
нии. Таким образом, вы, например, не можете написать дейст-
вительно переносимую функцию, которая будет вычислять макси-
мум от произвольного числа аргументов, как делают встроенные
функции MAX в фортране и PL/1.
Обычно со случаем переменного числа аргументов безопасно
иметь дело, если вызванная функция не использует аргументов,
которые ей на самом деле не были переданы, и если типы сог-
ласуются. Самая распространенная в языке "C" функция с пере-
менным числом - PRINTF . Она получает из первого аргумента
информацию, позволяющую определить количество остальных ар-
гументов и их типы. Функция PRINTF работает совершенно неп-
равильно, если вызывающая функция передает ей недостаточное
количество аргументов, или если их типы не согласуются с ти-
пами, указанными в первом аргументе. Эта функция не является
переносимой и должна модифицироваться при использовании в
различных условиях.
Если же типы аргументов известны, то конец списка аргу-
ментов можно отметить, используя какое-то соглашение; напри-
мер, считая, что некоторое специальное значение аргумента
(часто нуль) является признаком конца аргументов.
4.4. Внешние переменные
Программа на языке "C" состоит из набора внешних объек-
тов, которые являются либо переменными, либо функциями. Тер-
мин "внешний" используется главным образом в противопостав-
ление термину "внутренний", которым описываются аргументы и
автоматические переменные, определенные внурти функций.
Внешние переменные определены вне какой-либо функции и, та-
ким образом, потенциально доступны для многих функций. Сами
функции всегда являются внешними, потому что правила языка
"C" не разрешают определять одни функции внутри других. По
умолчанию внешние переменные являются также и "глобальными",
так что все ссылки на такую переменную, использующие одно и
то же имя (даже из функций, скомпилированных независимо),
будут ссылками на одно и то же. В этом смысле внешние пере-
менные аналогичны переменным COмMON в фортране и EXTERNAL в
PL/1. Позднее мы покажем, как определить внешние переменные
и функции таким образом, чтобы они были доступны не глобаль-
но, а только в пределах одного исходного файла.
В силу своей глобальной доступности внешние переменные
предоставляют другую, отличную от аргументов и возвращаемых
значений, возможность для обмена данными между функциями.
Если имя внешней переменной каким-либо образом описано, то
любая функция имеет доступ к этой переменной, ссылаясь к ней
по этому имени.
В случаях, когда связь между функциями осуществляется с
помощью большого числа переменных, внешние переменные оказы-
ваются более удобными и эффективными, чем использование
длинных списков аргументов. Как, однако, отмечалось в главе
1, это соображение следует использовать с определенной осто-
рожностью, так как оно может плохо отразиться на структуре
программ и приводить к программам с большим числом связей по
данным между функциями.
Вторая причина использования внешних переменных связана
с инициализацией. В частности, внешние массивы могут быть
инициализированы а автоматические нет. Мы рассмотрим вопрос
об инициализации в конце этой главы.
Третья причина использования внешних переменных обуслов-
лена их областью действия и временем существования. Автома-
тические переменные являются внутренними по отношению к фун-
кциям; они возникают при входе в функцию и исчезают при вы-
ходе из нее. Внешние переменные, напротив, существуют посто-
янно. Они не появляютя и не исчезают, так что могут сохра-
нять свои значения в период от одного обращения к функции до
другого. В силу этого, если две функции используют некоторые
общие данные, причем ни одна из них не обращается к другой ,
то часто наиболее удобным оказывается хранить эти общие дан-
ные в виде внешних переменных, а не передавать их в функцию
и обратно с помощью аргументов.
Давайте продолжим обсуждение этого вопроса на большом
примере. Задача будет состоять в написании другой программы
для калькулятора, лучшей,чем предыдущая. Здесь допускаются
операции +,-,*,/ и знак = (для выдачи ответа).вместо инфикс-
ного представления калькулятор будет использовать обратную
польскую нотацию,поскольку ее несколько легче реализовать.в
обратной польской нотации знак следует за операндами; инфик-
сное выражение типа
(1-2)*(4+5)=
записывается в виде
12-45+*=
круглые скобки при этом не нужны
Реализация оказывается весьма простой.каждый операнд по-
мещается в стек; когда поступает знак операции,нужное число
операндов (два для бинарных операций) вынимается,к ним при-
меняется операция и результат направляется обратно в
стек.так в приведенном выше примере 1 и 2 помещаются в стек
и затем заменяются их разностью, -1.после этого 4 и 5 вво-
дятся в стек и затем заменяются своей суммой,9.далее числа
-1 и 9 заменяются в стеке на их произведение,равное -9.опе-
рация = печатает верхний элемент стека, не удаляя его (так
что промежуточные вычисления могут быть проверены).
Сами операции помещения чисел в стек и их извлечения
очень просты,но, в связи с включением в настоящую программу
обнаружения ошибок и восстановления,они оказываются доста-
точно длинными. Поэтому лучше оформить их в виде отдельных
функций,чем повторять соответствующий текст повсюду в прог-
рамме. Кроме того, нужна отдельная функция для выборки из
ввода следующей операции или операнда. Таким образом, струк-
тура программы имеет вид:
WHILE( поступает операция или операнд, а не конец
IF ( число )
поместить его в стек
еLSE IF ( операция )
вынуть операнды из стека
выполнить операцию
поместить результат в стек
ELSE
ошибка
Основной вопрос, который еще не был обсужден, заключает-
ся в том,где поместить стек, т. Е. Какие процедуры смогут
обращаться к нему непосредственно. Одна из таких возможнос-
тей состоит в помещении стека в MAIN и передачи самого стека
и текущей позиции в стеке функциям, работающим со стеком. Но
функции MAIN нет необходимости иметь дело с переменными, уп-
равляющими стеком; ей естественно рассуждать в терминах по-
мещения чисел в стек и извлечения их оттуда. В силу этого мы
решили сделать стек и связанную с ним информацию внешними
переменными , доступными функциям PUSH (помещение в стек) и