На этом шаге мы приведем общие сведения о функциях.
В процессе программной реализации алгоритмов часто возникает необходимость выполнения повторяющихся действий на разных группах данных. Например, требуется вычислять синусы заданных величин или начислять заработную плату работникам. Ясно, что неразумно всякий раз, когда надо вычислить синус какого-то аргумента или начислить зарплату какому-то работнику, создавать заново соответствующую программу именно под конкретные данные. Напрашивается вывод, что этот процесс надо как-то параметризовать, т. е. создать параметрическую программу, которая могла бы, например, вычислять синус от любого аргумента, получая извне его конкретное значение, или создать программу, которая бы начисляла зарплату любому работнику, получая данные конкретного работника. Такие программы, созданные с использованием формальных значений своих параметров, при передаче им конкретных значений параметров возвращают пользователю результаты расчетов. Их называют функциями по аналогии с математическими функциями.
Если в математике определена некая функция у=f(х1,x2,..., xN), то на конкретном наборе данных {х11, x21,..., xN1} эта функция возвратит вычисленное ею значение у1=f(х11, x21,..., xN1). В данном случае можем сказать, что аргументы х1,x2,..., xN - это формальные параметры функции f(), а х11, x21,..., xN1 - их конкретные значения (фактические параметры). Функция в языке С объявляется почти аналогичным образом: задается тип возвращаемого ею значения (из рассмотренного материала мы знаем, что переменные могут иметь типы int, float, long и т.д., а тип значения, возвращаемого функцией, может быть таким, как и тип переменных). После задания типа возвращаемого значения задается имя функции (как и для математической функции). Затем в круглых скобках указываются ее аргументы - формальные параметры, каждый из которых должен быть описан так, как если бы он описывался в программе самостоятельно вне функции. Это только первая часть объявления функции в языке С.
Далее формируется тело функции - программный код, реализующий тот алгоритм, который и положено выполнять определяемой функции. Например, объявим условную функцию расчета зарплаты одного работника:
float salary(int TabNom, int Mes) { //здесь должен быть программный код расчета зарплаты return (значение вычисленной зарплаты); }
Тип возвращаемого значения - float, т. к. сумма зарплаты (в общем случае) число не целое. Имя функции - salary. У функции два формальных параметра и оба целого типа: табельный номер работника и номер месяца, за который должен производиться расчет. Особым признаком функции является наличие оператора return, который возвращает результат расчетов. После того как такая функция разработана, пользоваться ею можно так же, как мы пользуемся математической функцией. Для данного примера мы смогли бы записать:
float у; y=salary(1001,12);
float y=salary(1001,12);
int tn=1001; int ms=12; float y=salary(tn,ms);
Во всех случаях при обращении к функции мы в ее заголовочную часть подставляли вместо формальных параметров их конкретные значения (фактические параметры). Каков внутренний механизм параметризации программы и превращения ее в функцию?
Мы описываем в заголовке формальные параметры и затем в теле функции используем их при создании программного кода так, как будто известны их значения. Это возможно благодаря тому, что компилятор, когда начнет компилировать функцию, соотнесет с каждым ее параметром определенный адрес некоторого места в так называемой стековой памяти, созданной специально для обеспечения вызова подпрограмм из других программ. А функция - это ведь тоже своего рода подпрограмма, да еще и возвращающая некоторое значение, а не только получающая какой-то результат расчетов. Размер такой адресованной области для каждого параметра определяется типом описанного формального параметра. Выделяется место и для будущего возвращаемого результата.
В теле функции будет построен программный код, работающий, когда речь идет о формальных параметрах, с адресами не обычной, а стековой памяти. Когда мы, обращаясь к функции, передаем ей фактические значения параметров, то эти значения пересылаются по тем адресам стека, которые были определены для формальных параметров (т. е. кладутся на "полочки" в стеке, отведенные для формальных параметров). Но программный код тела функции как раз и работает с этими "полочками" (ячейками), содержащими параметры. Поскольку тело строится так, что оно работает с "полочками", то остается только класть на них разные данные и получать соответствующие результаты. Вот это и осуществляется, когда мы каждый раз передаем функции конкретные значения ее параметров.
Отсюда можно сделать выводы: поскольку передаваемые функции значения пересылаются в стековую память (т. е. там формируется их копия), то сама функция, работая со стеком и ни с чем другим, не может изменять значения переменных, которые подставляются в ее заголовочную часть вместо формальных параметров. В примере мы писали float y=saiary(tn,ms), подставляя вместо формальных параметров TabNom и Mes значения переменных tn и ms. И мы утверждаем, что значения tn и ms не изменятся. Если же передавать функции не значения переменных, а их адреса, то переменные, адреса которых переданы в качестве фактических параметров, смогут изменяться в теле функции. Ведь по адресу можно записать все, что угодно, где бы он ни находился (в стеке или в обычной памяти). Функции в основной программе должны описываться до начала самой программы: либо их текст располагается до main(), либо он подключается к main() директивой #inciude (которая тоже стоит раньше main() в тексте), если функция расположена где-то в другом файле.
Перейдем теперь от столь пространного введения к созданию некоторых функций и проверке их работы в основной программе.
На следующем шаге мы рассмотрим функцию ввода строки с клавиатуры.