улучшает читабельность программного кода. Однако тело функции оказывается достаточно компактным, нарушение этого принципа не приводит к заметным негативным последствиям.
Пример 2. Табулирование функции y = x2 в n равноотстоящих точках. void table(double xn, double xk, int n)
{
int i; double x, dx;
printf("%5s%10s%10s\n", "Number", "X", "Y"); x = xn;
dx = (xk - xn) / (n - 1); for(i = 1; i <= n; i++)
{
printf("%6d%10.3f%10.3f\n", i, x, x * x); x += dx;
}
}
36.6.Функции, возвращающие и невозвращающие
значение
Внекоторых языках, например в языке Паскаль, различают две вида подпрограмм:
∙Подпрограммы – процедуры.
∙Подпрограммы – функции.
Вязыке Си имеется все лишь один вид подпрограмм – функция. Однако такая функция может играть роль, как подпрограммы – процедуры, так и подпрограммы – функции языка Паскаль. Дело в том, что функция языка Си может возвращать значение, а может его не возвращать. Функция языка Си, не имеющая возвращаемого значения, играет роль процедуры, а возвращающая значение – роль функции языка Паскаль. Указанием на отсутствие у функции языка Си возвращаемого значения является использование в ее заголовке в качестве спецификатора типа, возвращаемого значения, зарезервированного слова void. Примером такой функции является библиотечная функция clrscr(), прототип которой имеет следующий вид:
void clrscr(void);
Примером функции, возвращающей значение, является библиотечная функция sqrt(), прототип которой имеет следующий вид:
double sqrt(double);
Заметим, что на ранних этапах использования языка Си широко применялся принцип неявного int. Существо этого принципа сводится к следующему. В тех случаях, когда отсутствует указание на тип объекта или функции, то по умолчанию предполагается наличие спецификатора типа int. Использование этого принципа позволяло повысить компактность записи объявлений и определений. Например, заголовок функции main() можно
было записать в следующем виде:
/* Устаревший стиль */ main(void)
81
Стандарт С99 не поддерживает принцип неявного int. Поэтому в заголовке функции main() следует в явном виде указать тип возвращаемого
значения:
/* Современный стиль */
int main(void)
Значение, возвращаемое функцией, формируется с помощью инструкции
return, которая должна иметь следующий формат: return [выражение];
Порядок выполнения этой инструкции состоит в следующем:
∙Вычисляется значение элемента выражение.
∙Завершаются вычисления в функции.
∙Создается временный объект, тип которого определяется типом, указанным в спецификации типа заголовка функции.
∙Во временный объект переписывается вычисленное значение элемента выражение. При этом могут выполняться предусмотренные языком автоматические преобразования типа.
∙Управление передается в точку вызова.
36.7.Вызов функции. Передача параметров по
значению
В языке Си средством обращения к функции является вызов функции.
Вызов функции имеет следующий формат:
имя([список_фактически х_параметров ])
Вызов функции содержит три структурных элемента. Два из них являются обязательными, а один нет. Вызов функции начинается с обязательного элемента имя. Этот элемент определяет имя функции, к которой выполняется вызов. Вторым обязательным элементом вызова функции является оператор функция, в качестве которого в языке Си используется пара круглых скобок. Наличие этого оператора делает расположенный перед ними идентификатор именем функции. Последний элемент вызова список_фактически_параметров относится к категории необязательных элементов. Фактические параметры наряду с формальными параметрами, которые содержатся в заголовке определения функции, служат для организации обмена данными между функцией и вызвавшим ее программным кодом (между функцией и ее клиентом). Фактические и формальные параметры должны быть согласованы по количеству, порядку следования и типам. Фактические параметры имеют синтаксис выражений. Приведем примеры вызовов для функции max2(), определение которой было написано выше. Пусть объявлены три переменных типа double a, b и с. Тогда можно написать следующие вызовы рассматриваемой функции:
max2(3.0, 4.0) max2(a, b)
max(a * a + 5, b + 3) max2(max2(a, b), c)
82
Таким образом, при работе с функцией max2() ее аргументами могут быть константы, переменные, арифметически выражения и даже вызовы самой функции max2().
Теперь перейдем к рассмотрению вопроса о том, где располагать вызов функции в клиентском коде. Возможны два варианта использования вызова функции в ее клиентском коде. В первом варианте вызов функции используется либо в качестве операнда выражения, либо в качестве фактического параметра функции. Такая возможность открывается в том случае, когда функция имеет возвращаемое значение. Примером такой функции является рассматриваемая функция max2(). В том случае, кода возвращаемым типом является тип void (это равносильно тому, что функция не имеет возвращаемого значения) вызов функции можно использовать только в составе инструкции – выражения. Примером такой функции может служить библиотечная функция clrscr():
int main(void)
{
clrscr(); /* … */
}
Опишем порядок выполнения вызова функции.
1.Вычисления в точке вызова временно прерываются. Запоминается точка возврата и состояния некоторых регистров процессора.
2.Управление передается вызываемой функции. В стеке выделяется память для формальных параметрах и локальных переменных.
3.Вычисляются значения фактических параметров, и формальные параметры функции инициализируются вычисленными значениями, соответствующих им фактических параметров. Операцией инициализации ограничивается взаимосвязь между формальными и фактическими параметрами.
4.Начинает выполняться тело функции. Выполнение функции завешается в момент времени, когда встречается инструкция return. Если функция не имеет возвращаемого значения, то в ее теле может отсутствовать инструкция return. В этом случае вычисления заканчиваются в том момент, когда управление достигает закрывающей скобки тела функции.
5.Память, выделенная функции в стеке, освобождается. Восстанавливается исходное состояние процессора. Управление передается в точку вызова функции.
Отметим одну особенность передачи параметров по значению. Эта особенность состоит в том, что для возврата результата, полученного в функции, необходимо в качестве формального параметра указатели (см. пункт 1.36.8). Дело в том, что при использовании передачи по значению через параметры данные передаются только в функцию. Применение указателей в качестве параметров функции позволяет организовать дополнительный канал связи между вызывающей и вызываемой функциями.
83
Обратимся к примеру. Воспользуемся приведенным выше определением функции max2(). Напишем для этой функции тестовую программу.
#include<stdio.h>
#include<conio.h>
double max2(double a, double b)
{
double max; if(a > b)
max = a; else
max = b; return max;
}
int main(void)
{
double a, b, с; printf(“a=”); scanf(“%lf”, &a); printf(“b=”); scanf(“%lf”, &b); printf(“с=”); scanf(“%lf”, &с);
printf(“max(a, b)=%10.3f\n”, max2(a, b)); printf(“max(a * a + 5, b + 3)=%10.3f\n”,
max(a * a + 5, b + 3));
printf(“max(3.0, 5.0)=%10.3f\n”, max2(3.0, 4.0)); printf(“max(a, b, c)=%10.3f\n”, max2(max2(a, b), c)); return 0;
}
36.8.Внешние переменные и процедурная
абстракция
Существуют две категории переменных в программе, написанной на языке Си:
∙локальные,
∙внешние (глобальные).
Локальными переменными называются переменные, объявленные в теле функции. Внешние переменные – это переменные, объявленные вне функций. Память для внешних переменных выделяется в статической области основной памяти. Все переменные, память для которых выделяется в статической области принято называть статическими. Время статических переменных совпадает со временем выполнения программы. Локальные переменные – это переменные, относящиеся к реализации функции. Память для таких переменных по умолчанию выделяется в стеке. Принято переменные, память для которых выделяется в стеке, называть автоматическими. Локальную переменную можно разместить в статической
84
области памяти. Для этого в ее определении следует использовать зарезервированное слово static. Примером переменной, которую следует объявлять внутри функции, является счетчик цикла.1
Укажем на одно полезное нововведение стандарта C99, которое касается объявления локальных переменных. Дело заключается в том, что этот стандарт разрешает объявлять переменные внутри инструкции for. Причем
область видимости этой переменной ограничивается телом цикла. Например:
/* Стандарт С89 */ int s =0, i;
for(i = 0; i < 10; i++) s += i * i;
printf(“i=%d”, i);
Понятие прототипа вначале появилось в языке C++. Затем оно было заимствовано языком Си, что нашло отражение в стандарте С89. Прототипом функции может служить ее заголовок, после которого поставлен символ точка с запятой. В прототипе необязательно указывать имена формальных параметров. Прототип для разработанной выше функции max2() может быть записан в одном из следующих видов:
double max2(double a, double b);
double max2(double, double);
Предпочтительнее указывать в прототипах имена параметров, наделяя их мнемоническим смыслом. Это повышает читабельность программы. Прототипы необходимы для корректной компиляции вызова функции. Следует отметить, что стандарт с89 не был последовательным в отношении использования прототипов. Он в отличие от языка С++ допускал компиляцию вызова функции при отсутствии прототипа. В этом случае компилятор создавал так называемое неявное объявление функции, которое
имеет следующий формат: int имя();
Пустые круглые скобки являются для компилятора указанием на то, что ему не предоставляется информация о формальных параметрах объявляемой функции.
В стандарте С99 неявные объявления больше не поддерживает.
/* Стандарт С99 */ int s =0;
for(int i = 0; i < 10; i++) s += i * i;
/* printf(“i=%d”, i); на переменную i ссылаться нельзя она здесь невидна. */
1 Этот материал следует согласовать с материалом, изложенным в перой части по переменным
85