Функция

 

Функция - это самостоятельная единица программы, спроектированная для реализации конкретной задачи. Вызов функций приводит к выполнению некоторых действий. Например, при обращении к функции printf( ) осуществляется вывод данных на экран. В общем, функции могут выполнять действия и получать значения величин, используемых в программе.

 

Почему мы пользуемся функциями? Во-первых, они избавляют нас от повторного программирования. Если конкретную задачу в программе необходимо выполнить несколько раз, мы напишем соответствующую функцию только один раз, а затем будем вызывать ее всегда, когда требуется. Во-вторых, мы можем применять одну функцию, например putchar( ), в различных программах.

! Если некоторая задача выполняется только в одной программе, лучше оформить ее решение в виде функции, т.к. функции повышают уровень модульности программы и, следовательно, облегчают ее чтение, внесение изменений и коррекцию ошибок. Дополнительное преимущество указанного подхода заключается в том, что если мы создадим функции общего вида, то их можно будет использовать и в других программах.

 

 

Многие программисты думают о функции, как о "черном ящике". Они задают ее через поступающую информацию и полученные результаты. Все, что происходит внутри черного ящика, их не касается. Что нам требуется знать о функциях? Нужно знать, как их можно определять, как к ним обращаться и как устанавливать связи между функцией и программой, ее вызывающей.

 

Абстракция управления в языке Си обеспечивается с помощью функций. Все функции могут быть рекурсивными. В языке Си отсутствуют подпрограммы (процедуры), однако возврат функцией значения в вызывающую программу не обязателен, следовательно, функции могут быть разделены на две категории - функции, возвращающие значения, и функции, не возвращающие значения в вызывающую программу (подпрограммы) .

 

Определение функций, возвращающих значение, имеет следующий формат:

[static]

тип-результата имя-функции (формальные аргументы)

описание формальных параметров

{

тело функции

}

 

где имя функции - правильный идентификатор, а тело функции имеет вид

определения и описания

операторы

! Все, что взято в квадратные скобки, может и не быть static - мы рассмотрим в лекции 10. Указание типа-результата функции в языке Си не является обязательным. Если тип результата не указан, то предполагается, что результат имеет тип int. Поскольку указание типа функции приводит к большей ясности и легкости чтения программы, а также упрощает нахождение в ней ошибок, тип функции всегда должен быть указан явно.

 

 

В качестве результата функция не может возвращать массив (см. лекцию 13) или функцию, но может возвращать указатель на массив или функцию. Выполнение функции, возвращающей значения, обязано завершиться оператором return вида

return e;

 

который обеспечивает выдачу результата e. Функция, возвращающая значение, может содержать более одного оператора return.

 

Определения функции, не возвращающей значения, имеют следующий формат:

[static] void имя-функции(формальные аргументы)

описание формальных параметров

{

тело функции

}

 

Выполнение такой функции завершается, если выполнено ее тело или оператор return вида

return;

 

Функция, не возвращающая значения, может содержать более одного оператора return.

 

Класс памяти static (необязательный) ограничивает видимость функции и других внешних определений. Функция с классом памяти static невидима вне содержащего ее файла. Если в тексте программы есть обращение к функции, то необходимо описание функции, которое в тексте должно быть помещено раньше ее определения. Описания функции имеют следующую форму:

[static или extern] тип-результата имя-функции( );

[static или extern] void имя-функции( );

 

Если в описании не указан класс памяти, то по умолчанию, предполагается extern.

!

Для компиляторов, не способных обрабатывать тип void, программист может определить тип void как

#define void int

 

и использовать его для определения функций, не возвращающих значения. Рекомендуется следовать этому соглашению для улучшения ясности программы. Однако в таких случаях компилятор будет не в состоянии обнаружить некорректное использование этих функций для возврата значений, поскольку на самом деле рассматриваемые функции возвращают значения, и эти значения имеют тип int.

 

Аргументы функции

 

Формальный аргумент - переменная в вызываемой программе, а фактический аргумент - конкретное значение, присвоенное этой переменной вызывающей программой. Фактический аргумент может быть константой, переменной или более сложным выражением. Независимо от типа фактического аргумента он вначале вычисляется, а затем его величина передается функции. Фактический аргумент - это конкретное значение, которое присваивается переменной, называемой формальным аргументом.

 

Если для связи с некоторой функцией требуется более одного аргумента, то наряду с именем функции можно задать список аргументов, разделенных запятыми. Например:

print_num(i,j)

int i,j;

{

printf("значение i=%d. Значение j=%d.", i,j);

}

 

Обращение в программе к данной функции будет таковым:

print_num(6,19);

 

Особое внимание нужно уделить правилам передачи аргументов при обращении к функциям. Синтаксис языка Си предусматривает только один способ передачи аргументов - передачу по значениям. Это означает, что формальные параметры (аргументы) функции локализованы в ней, то есть недоступны вне определения функции и никакие операции над формальными параметрами в теле функции не изменяют значения фактических параметров. Передача параметров по значению предусматривает следующие действия:

При компиляции функции выделяются участки памяти для формальных параметров. Формальные параметры оказываются внутренними объектами функции. При этом для параметров типа float формируются объекты типа double. Для параметров типа char, short int создаются объекты типа int. Если параметром является массив, то формируется указатель на начало этого массива и он служит представлением массива-параметра в теле функции (об указателе массивов описано в 12 лекции данного курса).

Вычисляются значения выражений, использованных в качестве фактических параметров при вызове функции.

Значения выражений- фактических параметров заносятся в участки памяти, выделенные для формальных параметров функции. При этом float преобразуется в double, a char, short int - в тип int.

В теле функции выполняется обработка с использованием значений внутренних объектов-параметров, и результат передается в точку вызова функции как возвращаемое ею значение.

Никакого влияния на фактические параметры функция не оказывает.

Возвращение значений

 

Напишем функцию, вычисляющую абсолютную величину числа. Абсолютная величина числа - это его значение, если отбросить знак. Например, абсолютная величина 125 - это 125, а абсолютная величина числа ( -125 ) - это тоже 125. Назовем эту функцию abs( ). Входом для этой функции будет любое число, для которого мы хотим найти абсолютную величину. Выходная величина возвращается при помощи ключевого слова языка Си - return. Поскольку функция abs( ) должна быть вызвана другой функцией, мы создадим простую функцию main( ), основной целью которой будет проверка, работает ли функция abs( ). Программа, спроектированная для того, чтобы проверить работу функции именно таким образом, называется драйвером. Драйвер подвергает функцию последовательным проверкам. Если результаты оказываются удовлетворительными, то ее можно поместить в программу, заслуживающую большего внимания. Термин драйвер обычно относится к программам, управляющим работой устройств:

/*драйвер*/

int main( )

{

int a=100, b=0, c=-122;

int d,e,f;

d=abs(a); e=abs(b); f=abs(c);

printf("%d, %d, %d ",d,e,f);

}

int abs(int x) /* функция, вычисляющая величину числа */

{

int y;

y = (x < 0) ? -x : x;

/*возвращает значение y вызывающей программы*/

return(y);

}

 

Результат работы программы выглядит так:

100 0 122

 

Ключевое слово return указывает на то, что значение выражения, заключенного в круглые скобки, будет присвоено функции, содержащей это ключевое слово (оператор). Поэтому, когда функция abs( ) впервые вызывается драйвером, значением abs(a) будет число 100, которое затем присваивается переменной d.

 

Переменная y является внутренним объектом функции abs( ), но значение y передается в вызывающую программу с помощью оператора return. Действие, оказываемое оператором

d=abs(a);

 

по другому можно выразить так:

abs(a);

d=y;

 

Такой записью мы воспользоваться не можем! Вызывающая программа даже не подозревает о том, что переменная y существует.

 

Оператор return оказывает и другое действие. Он завершает выполнение функции и передает управление следующему оператору в вызывающей функции. Это происходит даже в том случае, если оператор return является не последним оператором тела функции:

/* Функция, вычисляющая абсолютную величину числа, вторая версия */

int abs(int x)

{

if(x < 0)

return(-x);

else

return(x);

}

 

Эта версия программы проще. Для пользователя обе версии неразличимы, т. к. у них имеется один и тот же вход, и они обеспечивают один и тот же выход. Только внутренние структуры обеих функций различны.

/* третья версия функции abs( ) */

int abs(int x)

{

if(x < 0) return(-x);

else return(x);

printf("Работа завершена! ");

}

 

Наличие оператора return препятствует тому, чтобы оператор печати когда-нибудь выполнился в программе.

 

Можно пользоваться оператором

return;

 

Его применение приводит к тому, что функция, в которой он содержится, завершает свое выполнение и управление возвращается в вызывающую функцию. Поскольку у данного оператора отсутствует выражение в скобках, никакое значение не передается функции.

 

Все функции в программе, написанной на языке Си, равноправны. Каждая из них может вызывать любую другую функцию и, в свою очередь, каждая может быть вызвана любой другой функцией. Это делает функции языка Си несколько отличными от процедур Паскаля, поскольку процедуры в Паскале могут быть вложены в другие процедуры, причем процедуры, описанные в разных процедурах, являются недоступными для процедур, описанных в других независимых процедурах.

 

У процедуры main есть специфика. Она заключается в том, что после сборки программы, состоящей из нескольких функций, ее выполнение начинается с первого оператора функции main( ).

 

Каждая функция может вызвать саму себя. Действие, состоящее в том, что функция вызывает сама себя, называется рекурсией.

 

Рекурсивной называют функцию, которая прямо или косвенно сама вызывает себя. Именно возможность прямого или косвенного вызова позволяет различать прямую или косвенную рекурсию. При каждом обращении к рекурсивной функции создается новый набор объектов автоматической памяти, локализованных в теле функции. Функция называется косвенно рекурсивной в том случае, если она содержит обращение к другой функции, содержащей прямой или косвенный вызов определяемой (первой) функции. В этом случае по тексту определения функции ее рекурсивность (косвенная) может быть не видна. Если в теле функции явно используется вызов этой же функции , то имеет место прямая рекурсия.

! Так как при каждом обращении к рекурсивной функции создается новый набор объектов автоматической памяти, локализованных в теле функции, то при использовании рекурсивных алгоритмов с глубокой вложенностью рекурсии может быстро произойти переполнение стека реализации рекурсий, поэтому надежнее использовать итерационные алгоритмы. Например, пусть нужно реализовать "салфетку Серпинского" (геометрический фрактал). Как она образуется? Рисуется треугольник и в нем средние линии. В образованных при углах исходного треугольника новых треугольниках опять рисуются средние линии и так далее до заданного порядка вложенности рекурсии. Полученная "салфетка Серпинского" допускает другое, не рекурсивное, построение с помощью моделирования методом Монте-Карло.

 

Локальные переменные

 

Переменные, известные только одной функции, а именно той, которая их содержит, называются локальными переменными. Теперь мы подчеркиваем, что локальные переменные являются действительно локальными. Даже в том случае, когда мы используем одно и то же имя для переменных в двух различных функциях, компилятор считает их разными переменными.

Нахождение адресов

 

В результате выполнения операции & определяется адрес ячейки памяти, которая соответствует переменной. Если age - имя переменной, то &age - ее адрес. Можно представить себе адрес как ячейку памяти, но можно рассматривать его и как метку, которая используется компилятором для идентификации переменной. Предположим, мы имеем оператор

age=105;

 

Пусть также адрес ячейки, где размещается переменная age - 15125. В результате выполнения оператора

printf("%d %d ", age, &age);

 

получим 105 15125.

Указатели, первое знакомство

 

Указатель - некоторое символическое представление адреса. В описанном примере &age означает указатель на переменную age. Фактический адрес - это число, а символическое представление адреса &age является константой типа указатель. Адрес ячейки, отводимой переменной age, в процессе выполнения программы не меняется. В языке Си имеются и переменные типа указатель. Точно так же как значением переменной типа char является символ, а значением переменной типа int - целое число, значением переменной типа указатель служит адрес некоторой величины. Если мы дадим указателю имя ptr, то сможем написать, например, такой оператор:

ptr = &age;

/* присваивает адрес age переменной ptr */

 

Мы говорим в этом случае, что ptr указывает на age. Различие между двумя формами записи, ptr и &age, заключается в том, что ptr - это переменная, в то время как &age - константа. В случае необходимости мы можем сделать так, чтобы переменная ptr указывала на какой-нибудь другой объект:

ptr = &name;

/*ptr указывает на name, а не на age*/

 

Теперь значением переменной ptr является адрес переменной name.

Операция косвенной адресации *

 

Предположим, мы знаем, что в переменной ptr содержится ссылка на переменную name. Тогда для доступа к значению этой переменной можно воспользоваться операцией косвенной адресации *:

val = *ptr;

/* определение значения, на которое указывает ptr */

 

Последние два оператора, взятые вместе, эквивалентны следующему:

val = name;

Описание указателей

 

Примеры описания указателей:

int *pi; /*указатель на переменную типа целого*/

char *pc; /*указатель на символьную переменную*/

float *pf, *pg;

/* указатели на переменные с плавающей точкой*/

 

Спецификация типа задает тип переменной, на которую ссылается указатель, а символ звездочка ( * ) определяет саму переменную как указатель. Описание вида int *pi говорит , что pi - это указатель и что *pi - величина типа int.

 

При вызове функции информация о переменной может передаваться функции в двух видах. Если мы используем форму обращения

function1(x);

 

происходит передача значения переменной x. Если же мы используем форму обращения

function2(&x);

 

происходит передача адреса переменной x. Первая форма обращения требует, чтобы определение функции включило в себя формальный аргумент того же типа, что и x:

function1(num)

int num;

 

Вторая форма обращения требует, чтобы определение функции включало в себя формальный аргумент, являющийся указателем на объект соответствующего типа:

function2(&ptr)

int *ptr;

! Пользуйтесь первой формой, если входное значение необходимо функции для некоторых вычислений или действий, и второй формой, если функция должна будет изменять значения переменных в вызывающей программе. Программисты, работающие на языке Паскаль, могут заметить, что первая форма вызова аналогична обращению с параметром-значением, а вторая-с параметром-переменной.

 

Подведем итоги по указателям

 

Когда за знаком & следует имя переменной, результатом операции является адрес указанной переменной.

 

Когда за знаком * следует указатель на переменную, результатом операции является величина, помещенная в ячейку с указанным адресом.

 

Пример:

age = 105;

ptr =&age;/*указатель на age*/

val= *ptr;

 

Результатом выполнения этого фрагмента является присваивание значения 105 переменной val.

 

Типичное определение функции имеет следующий вид:

имя (список аргументов)

описание аргументов

тело функции

 

Наличие списка аргументов и описаний не обязательны. Переменные, отличные от аргументов, описываются внутри тела, которое заключается в фигурные скобки.

 

Аргументы используются для передачи значений из вызывающей программы в функцию. Использование ключевого слова return позволяет передавать в вызывающую программу одно значение из вызываемой функции. Обычно выполнение функции не оказывает никакого влияния на значения переменных вызывающей программы. Чтобы иметь возможность непосредственно изменять значения переменных вызывающей программы, необходимо использовать указатели в качестве аргументов. Это может оказаться необходимым в случае, если в вызывающую программу требуется передать более чем одно значение.

 

Функции должны иметь тот же тип, что и значения, которые они возвращают в качестве результатов. По умолчанию предполагается, что функции имеют тип int. Если функция имеет другой тип, он должен быть указан и в вызывающей программе, и в самом определении функции.

Функции с переменным количеством аргументов

 

В языке Си допустимы функции, количество аргументов у которых при компиляции функции не фиксировано. Количество и тип аргументов становится известным только в момент вызова функции, когда явно задан список фактических аргументов (параметров). При определении и описании таких функций, имеющих списки параметров неопределенной длины. Спецификация формальных параметров заканчивается запятой и многоточием:

тип имя(спецификация-явных-параметров,...);

 

Здесь тип - тип возвращаемого функцией значения; имя - имя функции.

 

Спецификация явных параметров - список спецификации параметров, количество и типы которых фиксированы и известны в момент компиляции. Эти параметры обязательны. Каждая функция с переменным количеством параметров должна иметь хотя бы один обязательный параметр. После списка явных (обязательных) параметров ставится запятая, а затем - многоточие. Компилятор знает, что дальнейший контроль соответствия количества и типов параметров при обработке вызова функции проводить не нужно. Чтобы функция с переменным количеством параметров могла воспринимать параметры различных типов, необходимо в качестве исходных данных каким-то образом передавать ей информацию о типах параметров.

 

Пример:

#include <stdio.h>

/* Функция суммирует значения своих параметров */

long summa(int m,...) /*m - число параметров*/

{

int *p=&m; /*настроили указатель на параметр m*/

long t=0;

for(;m>=0;m--) t+=*++p;

return t;

}

void main()

{

printf(" summa(2,6,4)=%d",summa(2,6,4));

printf(" summa(6,1,2,3,4,5,6)=%d", summa(6,1,2,3,4,5,6));

 

Стандартные библиотечные функции

 

Библиотека языка Си содержит множество функций и макроопределений. Библиотеки меняются от системы к системе, но есть ядро функций (стандартная библиотека ).

 

Эти функции используются для:

манипулирования данными, их преобразования и шифрования;

определения пользователями функций с переменным числом аргументов;

динамического управления памятью ;

представления показаний системных часов в стандартных форматах даты и времени;

получения системной информации.

 

Главными преимуществами стандартных библиотечных функций Си являются мобильность и низкие затраты на сопровождение пользовательских приложений. Библиотечные функции не подвержены частым изменениям, поэтому программы, в которых они используются, легки в сопровождении. Некоторые из этих библиотечных функций соответствуют стандарту ANSI С - стандарту Си Американского национального института, благодаря чему они приемлемы для всех систем, соответствующих этому стандарту. Чтобы сократить затраты и время на разработку приложений, рекомендуется использовать библиотечные функции Си всякий раз, когда это оказывается возможным.

 

Стандартные библиотечные функции Си объявляются в наборе файлов-заголовков, которые в UNIX-системах обычно расположены в каталоге /usr/include. Опишем библиотечные функции ANSI C, определенные в файлах-заголовков, перечисленных ниже:

<stdio.h>

<stdlib.h>

<string.h>

<memory.h>

<malloc.h>

<time.h>

<assert.h>

<stdarg.h>

<getopt.h>

<setjmp.h>

 

Кроме указанных, в большинстве UNIX-систем есть файлы заголовков, которые не определены в ANSI C:

<pwd.h>

<grp.h>

<crypt.h>.

 

В этих файлах заголовков объявляются функции, которые помогают получить доступ к информации о учетных записях пользователей и групп в UNIX-системах. В указанных системах они определены в библиотеке libc.a. Эти функции полезны для разработки приложений.

 

В файле заголовков <stdio.h> объявляется тип данных FILE, который используется в Си-программах для обозначения потоковых файлов, или просто потоков, т.е. файлов, обмен с которыми осуществляется с помощью функций потокового ввода-вывода. Имеется также набор макрокоманд и функций, предназначенных для манипулирования потоковыми файлами. Ниже приведены некоторые из этих макрокоманд и функций, которые уже должны быть знакомы из предыдущих лекций.

Потоковая функция или макрокоманда Назначение

fopen Открывает поток для чтения и (или) записи

fclose Закрывает поток

fread Читает блок данных из потока

fgets Читает строку текста из потока

fscanf Читает форматированные данные из потока

fwrite Записывает блок данных в поток

fputs Записывает строку текста в поток

fprintf Записывает форматированные данные в поток

fseek Перемещает указатель чтения или записи в потоке

ftell Возвращает текущую позицию в потоке, начиная с которой будет выполнена следующая операция чтения или записи. Возвращаемое значение - это количество байтов смещения относительно начала потока

freopen Повторно использует указатель потока для ссылки на новый файл

fdopen Открывает потоковый файл с указанным дескриптором

feof Макрокоманда, которая возвращает ненулевое значение, если в данном потоке обнаружен символ конца файла, в противном случае - нулевое значение

ferror Макрокоманда, которая возвращает ненулевое значение, если в данном потоке была обнаружена ошибка или символ конца файла, в противном случае - нулевое значение

clearer Макрокоманда, которая сбрасывает флаг наличия ошибок в данном потоке

fileno Макрокоманда, которая возвращает дескриптор данного потокового файла

 

 

В заголовке <stdlib.h> объявляется набор функций, служащих для преобразования данных, генерации случайных чисел, получения и установки переменных среды shell, управления выполнением программ и выполнения команд shell. Обычно эти функции объявляются в заголовке <stdio.h>, но так как они не включают в себя манипулирование потоками, стандарт ANSI C группирует их в отдельный заголовок.

 

В заголовке <string.h> объявляется набор функций, предназначенных для манипулирования символьными строками.

 

В заголовке <memory.h> объявляется набор функций, предназначенных для манипулирования байтовым потоком. Эти функции похожи на строковые, но в отличие от них имеют более широкое назначение и могут использоваться для манипулирования несимвольными строковыми объектами. В частности, данные функции можно применять для инициализации, сравнения и копирования объектов типа struct.

 

В заголовке <time.h> объявляется набор функций, предназначенных для вызова системных параметров времени. Они могут применяться для определения местного времени и даты, времени и даты в универсальном формате (UTC), а также статистических данных об использовании процессами времени центрального процессора.

 

В заголовке <assert.h> объявляется макрокоманда, используемая для проверки некоторых условий выполнения процесса, которые в нормальной ситуации всегда должны быть истинны. Если все же во время выполнения процесса условие не выполняется, то макрокоманда выводит сообщение об ошибке в стандартный поток ошибок с указанием той строки исходного файла, в которой нарушается проверяемое условие. После этого макрокоманда прерывает процесс.

 

В заголовке <setjmp.h> объявляется набор функций, которые позволяют процессу вызывать оператор перехода goto из одной функции в другую. Вызов Си-оператора goto позволяет процессу передать управление выполнением от одного оператора к другому лишь в рамках этой же функции. Функции, определенные в заголовке <setjmp.h> устраняют данное ограничение. Эти функции необходимо использовать лишь тогда, когда без них действительно нельзя обойтись. Например, если ошибка обнаружена в рекурсивной функции, то есть смысл сообщить об ошибке, а затем выполнить оператор перехода goto в основную функцию, т.е. как бы начать процесс сначала.

 

В заголовке <pwd.h> определяется набор функций, предназначенных для получения учетной информации о пользователях.

 

В заголовке <grp.h> определяется набор функций, предназначенных для получения учетной информации о группах, содержащейся в UNIX- файле /etc/group.

 

В заголовке <crypt.h> объявляется набор функций, предназначенных для шифрования и дешифрования данных. Это очень важные функции, обеспечивающие безопасность системы. Например, файлы пользовательских паролей и системных данных, которым необходима высокая степень защиты, должны быть зашифрованы так, чтобы ни один человек, не имеющий специального разрешения, не мог узнать, что они из себя представляют. Более того, чтобы читать и изменять эти объекты, уполномоченные лица должны знать секретные ключи дешифровки.