Функции в языке Си
Давайте повторим и систематизируем всё, что мы знаем о функциях.
Как устроены функции в языке Си
Все функции в C, включая те, которые пишет пользователь, состоят из двух частей: заголовка функции и тела функции.
Заголовок любой функции включает в себя три обязательных компонента:
- тип возвращаемого значения;
- имя функции;
- список параметров, записанный в круглых скобках
()

Рис.1 Общая структура функций на примере функции main
Давайте разберём заголовки уже знакомых нам функций rand, pow и srand.
Листинг 1. Заголовки функций rand, pow и srand
int rand(void) // заголовок функции rand
double pow(double base, double exp) // заголовок функции pow
void srand(int seed) // заголовок функции srandТип возвращаемого значения
Первым в заголовке функции указывается тип возвращаемого значения.
В Листинге 1 мы видим, что функция rand возвращает значение типа int, функция pow возвращает значение типа double. Тут всё довольно просто.
Чтобы показать, что функция не возвращает никакого значения, на этом месте пишут ключевое слово void. Для примера посмотрите на заголовок функции srand.
Имя функции
Имя функции записывается после типа возвращаемого значения.
Мы используем имя функции, чтобы обратиться к функции (вызвать функцию). Каждая функция должна иметь своё уникальное имя. В имени функции можно использовать: английские буквы, цифры и символ _. Причём имя не должно начинаться с цифры.
Рекомендация:
- называйте ваши функции так, чтобы по имени функции было понятно, что конкретно делает функция.
Список параметров
В круглых скобках после имени функции необходимо перечислить формальные параметры. Так мы сообщаем компилятору, сколько значений и какого типа ожидает функция от того кода, который её вызывает.
Важно! Параметры перечисляются через запятую. Для каждого параметра необходимо указать: тип данных и имя.
double pow(double base, double exp) — правильно.
double pow(double base, exp) — ОШИБКА! Нужно явно прописать тип данных для параметра exp.
А что же делать, если нашей функции не нужны параметры? В таких случаях, как мы знаем с самого первого урока, вместо списка параметров в круглых скобках нужно записать ключевое слово void.
Листинг 2.
int rand(void) // у функции rand нет параметров
double pow(double base, double exp) // функция pow принимает два значения типа double
void srand(int seed) // функция srand принимает одно значение типа intТело функции
Тело функции — всё, что записано внутри фигурных скобок {} сразу после заголовка функции. Здесь мы описываем все те действия, которые должна выполнить функция.
Как создать пользовательскую функцию в Си?
Золотое правило программирования гласит: прежде чем использовать что-нибудь в программе, надо это определить.
А что значит "определить" функцию?
Определение функции (function definitions) — это полное описание всей структуры функции: заголовка и тела функции.
Таким образом, определить функцию означает подробно описать все структурные части функции. Кстати, иногда вместо определить функцию говорят описать реализацию функции.
Листинг 3. Общий синтаксис определения функции.
тип_возвращаемого_значения имя_функции(список_параметров)
{
// тело функции
}
Где допустимо определять функции в языке Си?
Самое очевидное место, где следует определять функции, понятно, перед функцией main, сразу после директив #include.
Листинг 4. Программа с функцией max2
#include <stdio.h>
// определение функции max2
int max2(int a, int b)
{
int max = b;
if (a > b) {
max = a;
}
return max;
}
// конец определения функции max2
int main(void)
{
int x = 0, y = 0, m = 0;
scanf("%d %d", &x, &y);
m = max2(x, y); // вызов функции max2
printf("max(%d,%d) = %d\n", x, y, m);
return 0;
}Убедитесь, что эта программа работает.

Рис.2 Результат работы программы Листинг 4
Теперь представим, что у нас в программе, например, 10 пользовательских функций. Когда мы запишем их определения, то код самой функции main опустится в самый конец исходника. Вроде самая главная функция, а находится в таком непочётном месте. Как-то несолидно получается.
Поэтому для функций сделано небольшое исключение из общего правила, описанного выше.
Разрешается до функции main записать прототип функции (иногда говорят объявить функцию (function declaration)), а реализацию функции написать в другом месте.
Прототип функции — это заголовок функции с ; в конце. Например, для функции max2 прототип будет выглядеть так: int max2(int a, int b);.
Когда компилятор встречает в тексте программы вызов функции, то он должен:
- проверить, что функция с таким именем действительно существует;
- сверить количество передаваемых параметров и их типы;
- проверить тип возвращаемого значения при необходимости.
Для этих целей ему достаточно будет и прототипа. Если же компилятор видит вызов функции, но ранее по тексту программы он не встречал ни прототипа, ни определения функции, то он выдаст ошибку.
Давайте перепишем Листинг 4 с использованием прототипа функции max2.
Листинг 5. Использование прототипа функции max2
#include <stdio.h>
// объявление (прототип) функции max2
int max2(int a, int b);
int main(void)
{
int x = 0, y = 0, m = 0;
scanf("%d %d", &x, &y);
m = max2(x, y); // вызов функции max2
printf("max(%d,%d) = %d\n", x, y, m);
return 0;
}
// определение (реализация) функции max2
int max2(int a, int b)
{
int max = b;
if (a > b) {
max = a;
}
return max;
}Обратите внимание, что понятия "определить функцию" и "объявить функцию" не эквивалентны. Определить — полностью описать, объявить — описать только прототип. Т.е. объявляя функцию мы как бы говорим компилятору: "не волнуйся, функция с таким именем существует, вот её прототип, а реализация будет написана потом, мы об этом позаботимся".
Итого, для программ, состоящих из одного файла, есть два основных места, где можно определять пользовательские функции:
- перед функцией
main; - после функции
main, но тогда перед функциейmainнадо добавить прототип.
Давайте отразим это на нашей карте со структурой программы из первого урока.

Рис.3 Общая структура программы на языке Си.
Важно!
Стандарт языка Си запрещает определять одну функцию в теле другой функции.
Следующая программа скорее всего не скомпилируется, проверьте это самостоятельно:
Листинг 6.
#include <stdio.h>
int main(void)
{
// определение функции max2 внутри main
int max2(int a, int b)
{
int max = b;
if (a > b) {
max = a;
}
return max;
}
int x = 0, y = 0, m = 0;
scanf("%d %d", &x, &y);
m = max2(x, y); // вызов функции max2
printf("max(%d,%d) = %d\n", x, y, m);
return 0;
}Разобравшись с тем, как объявлять и определять пользовательские функции в языке Си, давайте обсудим, как устроен вызов функций.
Как происходит вызов функции
После того, как мы определили функцию, мы можем вызывать её сколько угодно раз в любом месте программы. Кстати, иногда вместо "вызвать функцию" говорят "обратиться к функции".
Чтобы вызвать функцию нужно написать её имя, а затем в круглых скобках () указать аргументы, которые мы хотим передать функции.
Например: sqrt(25), max2(x, y).
Если функции для работы не нужны никакие параметры (т.е. в заголовке функции вместо списка параметров указано void), то для вызова такой функции нужно написать только её имя и круглые скобки, например: rand().
Частая ошибка: Новички иногда пишут что-то вроде rand(void). Это ошибка. void используется только в определении функции, для вызова он не нужен. Просто оставьте скобки пустыми.
Аргументы функции
Значения, которые передаются в функцию при вызове, называют аргументами (или фактическими параметрами) функции.
Напоминалка:
- Параметры (формальные параметры) функции — то, что функция ожидает получить. Мы описываем их в заголовке функции, когда пишем её определение.
- Аргументы (фактические параметры) функции — конкретные значения, которые реально передаются в функцию при её вызове.
Вспоминаем аналогию с анкетой из первого урока: пустые графы «Имя» и «Фамилия» — это формальные параметры. Вписанные в анкету конкретные значения «Гилл» и «Байтс» — это аргументы или фактические параметры.
В качестве аргументов можно использовать:
- конкретные значения:
max2(3, 5); - переменные:
max2(x, y); - выражения:
max2(x + 5, y * 2); - вызовы других функции:
max2(rand(), 10); - всё, перечисленное выше, вперемешку:
max2(x + rand(), 3 * y).
Возврат значения из функции. Инструкция return
Раньше мы использовали инструкцию return для того, чтобы завершить работу функции main и вернуть значение 0 в операционную систему.
Эта же самая инструкция используется для завершения работы любой функции. При этом, конечно, мы можем возвращать любое значение, а не только нуль.
Листинг 7. Общий синтаксис инструкции return
return возвращаемое_значение;Важно! Если в определении функции указан тип возвращаемого значения, то функции обязательно должна возвращать какое-либо значение с помощью инструкции return. Если в какой-то ветке выполнения функции return не сработает по тем или иным причинам, то мы получим неопредлённое поведение (англ. undefined behavior, UB)
Ранее мы упоминали, что существуют функции, которые не должны возвращать никакого значения (у них вместо типа возвращаемого значения записано слово void). Иногда их называют void-функциями или процедурами. Как же быть с ними?
Самый распространённый вариант — просто не использовать инструкцию return в теле void-функции. В таком случае работа функции завершится после того, как будут исполнены все инструкции, записанные в теле функции.
Если по каким-то причинам выполнение void-функции требуется завершить досрочно (например, как в подходе с early returns), то в этом случае просто пишут return; без какого-либо значения.
Разберём подробно, как происходит вызов функции на примере программы из Листинга 5.
Допустим, что после запуска программы мы ввели 5 11. Функция scanf запишет значения 5 и 11 в переменные x и y соответственно.
Далее идёт строка, содержащая обращение к функции max2:
m = max2(x, y);
Переменной m надо присвоить то, что находится справа от знака =. Там у нас указано записан вызов функции max2. Проследим по шагам, что будет происходить в программе.
Шаг 1. Подготовка аргументов функции.
Переменные заменяются на их текущие значения и проводятся вычисления выражений, если это необходимо. В нашем случае мы просто заменяем переменные их текущими значениями, т.е. max2(x, y) превращается в max2(5, 11). Когда аргументы готовы, управление передаётся функции max2.
Шаг 2. Подготовка локальных переменных в соответствии со списком параметров.
Функция max2 создаёт две временные локальные переменные a и b типа int. Их область видимости и время жизни ограничены функцией max2. В переменные a и b копируются значения, вычисленные на первом шаге.
Важно! Обратите внимание, что в функцию передаются не сами аргументы, а только копии их значений. Это стандартный механизм языка Си, который называется передача по значению (pass by value) или вызов функции с передачей значений.
Шаг 3. Выполняется тело функции.
После того, как всё готово, выполняется тело функции max2.
Сначала создаётся целочисленная переменная с именем max и ей присваивается значение, записанное в переменной b, т.е. теперь max = 11. Мы как бы предполагаем, что в переменной b записано большее из чисел.
Затем мы проверяем условие a > b. Если оно истинно, значит мы ошиблись в нашем предположении и значение в переменной max надо заменить значением из переменной a.
У нас a = 5, а b = 11, значит a > b ложно. Поэтому дополнительных действий не требуется. Следовательно, когда мы доходим до инструкции return, в переменной max хранится значение 11.
Шаг 4. Возврат из функции и уничтожение локальных переменных.
Инструкция return max; возвращает в вызывающую программу (функцию main) значение, записанное в переменной max. В нашем случае это число 11. После чего работа функции max2 завершается, а локальные переменные a b и max удаляются.
Важно! Обратите внимание, что все переменные, объявленные внутри функции являются локальными (включая и переменные из параметров). Они доступны только внутри этой функции. Говоря на профессиональном жаргоне: область видимости этих переменных ограничена телом функции, а время жизни — временем работы функции.
Шаг 5. Вызов функции max2(5, 11) заменяется значением, которое вернула функция max2.
Таким образом, после того, как функция max2 отработала выражение m = max2(x, y); превращается в m = 11;.
Важно! Программист должен самостоятельно следить за тем, чтобы:
- тип аргумента соответствовал типу формального параметра.
- возвращаемое значение соответствовало тому типу, который указан в заголовке функции.
Компилятор, конечно, тоже за этим следит. В некоторых простых случаях, если он заметит расхождение, то можеть выдать предупреждение, а иногда может выполнить неявное приведение типов. При этом, как обычно, возможна потеря точности. Если же приведение типа выполнить невозможно, то вы получите ошибку компиляции.
Завершим рассказ о вызове функций кратким описанием того, как происходит вызов функции:
- Подготовка аргументов функции. Должны остаться только конкретные значения. Передача управления функции.
- Функция создаёт локальные переменные в соответствии со списком параметров и копирует туда значения из аргументов.
- Выполняется тело функции.
- Возврат значения, передача управления вызывающей программе и удаление локальных переменных.
- Замена вызова функции значением, которое она вернула.
Для void-функций пункт 5 не выполняется, а 4 пункт состоит в удалении локальных переменных и передаче управления вызывающей программе.

Рис.4 Схема вызова функции и возврата значения
Рассмотрим несколько вариантов реализаций функции max2, которые проиллюстрируют дополнительные возможности функций.
Листинг 8.
int max2(int a, int b)
{
// две точки выхода из функции
if (a > b) {
return a;
} else {
return b;
}
}В этом варианте, мы убрали переменную max и сразу возвращаем большее из значений.
Листинг 9.
int max2(int a, int b)
{
return (a > b) ? a : b;
}Простые вычисления можно делать прямо в инструкции return. Но на первых порах этим лучше не увлекаться.
В этой реализации мы проводим вычисления с помощью тернарного оператора ? : прямо внутри инструкции return.
Посмотрим, как пользовательские функции могут вызывать друг друга. Давайте усовершенствуем нашу программу и добавим в неё функцию, которая вычисляет максимум из трёх чисел.
Листинг 10.
#include <stdio.h>
// прототипы функций
int max2(int a, int b);
int max3(int a, int b, int c);
int main(void)
{
int a = 0, b = 0, c = 0, m = 0;
scanf("%d %d %d", &a, &b, &c);
m = max3(a, b, c); // вызов функции max3
printf("max(%d,%d,%d) = %d\n", a, b, c, m);
return 0;
}
// реализация функции max2
int max2(int a, int b)
{
return (a > b) ? a : b;
}
// реализация функции max3
int max3(int a, int b, int c)
{
int max_ab = max2(a, b);
return max2(max_ab, c);
/* Или даже в одну строку:
return max2(max2(a, b), c);
return max2(a, max2(b, c));
*/
}В этом примере надо обратить внимание на два момента.
Во-первых, посмотрите как ловко мы переиспользовали уже готовую функцию max2, чтобы написать функцию max3.
Во-вторых, обратите внимание, что в функциях max2 и max3 использованы одинаковые имена локальных переменных a и b. Кроме того, в функции main также используются переменные с именами a, b и c. При этом программа работает корректно.
Это происходит из-за того, что переменные, объявленные в функции (в том числе и в функции main), имеют локальную область видимости. Например, функция main ничего не знает о переменных, которые есть в функции max2 или в функции max3. У каждой из функций есть своя собственная "коробчка" с именем a и эти коробчки между собой никак не связаны и друг от друга не зависят. Конфликта имён не происходит.
Практика
Проверьте самостоятельно, какие ошибки выдаёт компилятор, если:
- убрать из Листинга 5 прототип функции
max2; - убрать из Листинга 5 определение функции
max2; - добавить в Листинг 5 ещё один прототип функции
max2;
- убрать из Листинга 5 прототип функции
Неопределённое поведение — это ситуация, когда стандарт языка не описывает, что должно произойти в таком случае. Программа, содержащая неопределённое поведение может "случайно" работать правильно, может работать неправильно, может вообще упасть и вести себя различно при разных запусках и на разных компиляторах. При этом компилятор не обязан даже выдавать ошибку — иногда он лишь предупреждает, иногда молчит. Замените реализацию функции
max2в Листинге 5 на ту, что предложена ниже. В ней допущена сознательная ошибка: не обрабатываются ситуации, когда значениеaменьшеb. Поэтому, если передать в эту функцию, например,3 7, то мы получим неопределённое поведение, т.к. функция должна вернуть значение типаint, а в этом случае она ничего не возвращает. Проверьте, как обработает эту ситуацию ваша IDE. О результатах напишите в комментариях к уроку.int max2(int a, int b) { // две точки выхода из функции if (a > b) { return a; } else { printf("UB!!!\n"); // "случайно" забудем return в этой ветке // return b; } }Как и сказано в уроке, стандарт языка Си не позволяет объявлять функции внутри других функций. Но при этом разработчики компиляторов иногда расширяют возможности стандартов, поэтому у некоторых код из Листинга 6 нормально скомпилируется и отработает. Попробуйте скомпилировать и запустить Листинг 6 у себя в системе. Получилось ли у вас? Напишите об этом в комментариях к уроку с указанием вашей IDE и используемого компилятора.
Исследовательские задачи для хакеров
- попробуйте самостоятельно найти объявления и определения функции
absиfabsв соответствующих заголовочных файлах. Получилось ли?
Дополнительные материалы
1. Как уже упоминалось в 5 уроке, в некоторых языках программирования подпрограммы делятся на функции и процедуры прямо на уровне синтаксиса.
Например, в Pascal есть специальные ключевые слова procedure и function, а в языке Fortran: SUBROUTINE и FUNCTION. Также ключевые слова PROCEDURE и FUNCTION используются в языке MySQL.
Исходили из следующих представлений о разграничении сфер ответственности.
Функция — это нечто похожее на функцию из математики, некая подпрограмма, которую вызывают для того, чтобы вычислить тем или иным образом какое-то значение и вернуть его вызывающей программе. Например, наша функция max2 является самой что ни на есть настоящей функцией в этом смысле этого слова. Мы её вызываем, чтобы получить от неё результат — максимальное из двух переданных чисел.
А процедура — это такая подпрограмма, которую вызывают для того, чтобы она выполнила какое-нибудь действие, но от которой не требуется никакого результата вычислений. Примером может служить функция srand, которая вызывается для того, чтобы инициализировать счётчик псевдослучайных чисел, а не для того, чтобы от неё получить какое-то значение.
Теперь давайте рассмотрим функцию printf.
По смыслу, она больше похожа на процедуру, т.к. её обычно вызывают для того, чтобы вывести что-то на экран консоли. Но, как нам уже известно, в языке Си функция printf не просто выводит строку на экран, а она ещё и вычисляет значение — количество символов, которые она вывела на экран (в стандартный поток вывода).
Поэтому, формально говоря, printf — это функция, которая вычисляет, сколько символов она передала в стандартный поток вывода. Но это плохая функция, она не просто вычисляет это значение, она ещё производит побочный эффект, т.е. делает что-то ещё, помимо вычисления своего значения, и это изменяет среду в которой работает наша программа (выводит данные на экран). Более того, обычно мы игнорируем значение, которое вычисляет функция printf. Получается, что мы вызываем функцию не ради вычисления её значения, а, как говорят, ради побочного эффекта, которым сопровождается это вычисление.
Похожая ситуация и с функцией scanf. Конечно, её возвращаемое значение используется гораздо чаще, но это не отменяет того факта, что вызывают её не для вычисления этого значения, а для того, чтобы записать данные в переменные.
Выходит, что функции printf и scanf как будто немного "неправильные", т.к. в них смешиваются понятия "процедура" и "функция".
Есть мнение, что в качестве первого языка программирования лучше использовать языки вроде Pascal, где есть строгое разделение подпрограмм на функции и процедуры, т.к. это позволяет сформировать ясное представление о процедурах и функциях и сформировать у начинающего программиста правильное отношение к побочным эффектам. А языки вроде Си, где подобного разделения нет, напротив, не позволяют сформировать правильное представление об этих концепциях, и вынуждают писать код, состоящий сплошь из побочных эффектов, чем наносят неизгладимый вред всем, кто начинает изучение программирования с этих языков.
Я, конечно, не призываю вас сейчас же бросить изучения языка Си. =) Я вас предупреждаю о подводных камнях и призываю избегать побочных эффектов при написании собственных функций, которые нужны лишь для вычисления какого-либо значения.
2. Обратите внимание, что ключевое слово void имеет разное значение в зависимости от того, где оно употребляется.
Слово void, записанное в списке параметров означает, что параметры у функции отсутствуют, и это же слово в начале заголовка функции означает, что функция ничего не возвращает. Совсем скоро мы столкнёмся с тем, что значение символа * тоже зависит от контекста. Пока что мы использовали его только как оператор умножения.
3. В прототипе функции можно не указывать имена формальных параметров, достаточно указать только их типы:
int max2(int, int); // для прототипа этого достаточноНо в определении функции указывать имена параметров обязательно!