C. Функции
Время чтения: 6 минут
Функция (function) – это механизм, позволяющий вынести часть логики программы в отдельный блок, который можно многократно исполнять из разных мест прогаммы.
Вот пример использования функции в программке, которая складывает, возводит в квадрат и выводит на экран целые числа:
#include <stdio.h>
int add(int a, int b)
{
int result = a + b;
return result;
}
int square(int value)
{
int result = value * value;
return result;
}
void show_result(int result)
{
printf("Result: %d\n", result);
}
int main()
{
int result = 0;
result = add(2, 2);
show_result(result); // Result: 4
result = square(4);
show_result(result); // Result: 16
result = add(result, square(2));
show_result(result); // Result: 20
}
Это называется определением функции (в следующем разделе разберём подробно):
int add(int a, int b)
{
int result = a + b;
return result;
}
Это называется вызовом функции:
add(2, 2)
square(2)
Давай посмотрим как это делать и из чего состоит функция.
Определение функции
В прошлом разделе я использовал такую функцию:
int add(int a, int b)
{
int result = a + b;
return result;
}
На первой строчке описана сигнатура функции. Сигнатура, это полное описание всех свойств функции, которое состоит из следующих частей.
Название функции
- В примере это “add”.
- Имя, которое мы будем писать чтобы вызвать эту функцию.
- Требования к имени функции такие же, как к имени переменной.
Параметры (аргументы) функции
- В примере это два int параметра “a” и “b”.
- Параметры, которые мы будем передавать в функцию для того, чтобы она что-то с нимим сделала.
- Параметры являются локальными переменными, которым присваисваиваются значения, переданные в функицю.
- Требования к имени параметра такие же, как к имени переменной.
- Тип параметра может быть любым.
Возвращаемое значение
- В этом примере это “int” в начале строчки.
- Значение, которое функция возвращает в то место, откуда её вызвали.
- Тип возвращаемого значения может быть любым.
- В примере функция возвращает int значение в место вызова.
- Очень часто переменные просто исполняются, и не возвращают никаких значний – в этом случае надо использовать тип возвращаемого значения void.
- Чтобы вернуть значение, надо написать “return ЗНАЧЕНИЕ;” (в случае void просто “return;”) – эта операция завершает выполнение функции и программа продолжает выполняться в месте вызова функции.
Определение (прототип) и реализация функции
Так, с функцией разобрались – давай теперь разберёмся как её описать в программе.
Сначала два важных понятия:
- Определение (definition) функции – сигнатура функции (определение также называют прототипом)
- Реализация (implementation) функции – сигнатура функции + текст функции
Описать функцию можно двумя способами:
Сначала определение, потом реализация
#include <stdio.h>
// Определение (definition)
void test();
int main()
{
// Вызов функции test
test();
}
// Реализация (implementation)
void test()
{
printf("I'm function implementation!\n");
}
Смотри, есть одно важное правило:
Вызов функции должен располагаться в коде после её определения.
То есть такой код не скомпилируется:
// НЕ СКОМПИЛИРУЕТСЯ
#include <stdio.h>
int main()
{
test();
}
void test()
{
printf("I'm function implementation!\n");
}
// НЕ СКОМПИЛИРУЕТСЯ
Ага, понятно – поэтому там определение сначала написано. А что со вторым вариантом?
Только реализация
#include <stdio.h>
void test()
{
printf("I'm function implementation!\n");
}
int main()
{
test();
}
Здесь реализация функции выступает сразу и определением – и мы без проблем можем вызвать её в main().
Задание на закрепление
Создай функцию, которая возвращает int число 42 и выведи её значение в функции main().
Опиши функцию двумя способами: “сначала определение, потом реализация” и “только реализация”.
Возможные трудности
У начинающих мастеров владения функциями возникают проблемы такого характера:
- Как мне веруть больше одного значения?
- Почему я передаю указатель, а туда не записывается адрес на выделенную память?
- Как объединить две похожих функции?
Давай разберём эти проблемы.
Как мне веруть больше одного значения?
Как ты успел заметить, в функции можно указать только одно возвращаемое значение. Однако, это не проблема для мастера владения функциями:
#include <stdio.h>
// В параметрах - указатели на переменные
void get_multiple_results(int value, int *a, int *b, int *c)
{
// Через операцию разыменования достаю и записываю значения в a, b, c
*a = *a * 2 + value;
*b = *b / 2 + value;
*c = *c % 2 + value;
}
int main()
{
int a = 1, b = 2, c = 4;
// Передаю адреса переменных a, b, c
get_multiple_results(1, &a, &b, &c);
printf("%d %d %d\n", a, b, c); // 3 2 1
}
Вот и пригодились указатели.
Почему я передаю указатель, а туда не записывается адрес на выделенную память?
Вот пример кода с ошибкой:
#include <stdio.h>
void fill_arr(int *arr, int n)
{
arr = (int*)calloc(n, sizeof(int));
if(arr == NULL)
return;
for(int i = 0; i < n; i++)
scanf("%d", &arr[i]);
}
int main()
{
int *A = NULL;
fill_arr(A, 5);
A[0]; // Ошибка! A == NULL
}
Здесь проблема в том, что параметр arr это локальная переменная функции fill_arr, которая просто скопировала себе адрес переменной A.
Из-за того что arr это вообще другая переменная, в переменную A адрес на выделенную память так и не доходит. Что можно сделать?
Есть два пути:
- Вспомнить что надо передавать адрес переменной, чтобы туда что-то записать:
#include <stdio.h>
void fill_arr(int **arr, int n)
{
*arr = (int*)calloc(n, sizeof(int));
if(*arr == NULL)
return;
for(int i = 0; i < n; i++)
scanf("%d", &(*arr)[i]);
}
int main()
{
int *A = NULL;
fill_arr(&A, 5);
// Не забываем предохраняться от NULL
if(A == NULL)
return 0;
A[0]; // Всё ок!
}
- Вспомнить что существует возвращаемое значение
#include <stdio.h>
int* fill_arr(int n)
{
int *arr = (int*)calloc(n, sizeof(int));
if(arr == NULL)
return arr;
for(int i = 0; i < n; i++)
scanf("%d", &arr[i]);
return arr;
}
int main()
{
int *A = fill_arr(5);
// Не забываем предохраняться от NULL
if(A == NULL)
return 0;
A[0]; // Всё ок!
}
Если что, то в случае, когда ты просто хочешь поработать с существующим массивом внутри функции, ничего такого делать не надо – параметр функции просто скопирует адрес твоего указателя, и ты будешь работать с ним как обычно.
Как объединить две похожих функции?
Например, у тебя есть две функции – одна ищет в массиве максимальное положительное число, а другая минимальное положительное число:
#include <stdio.h>
int find_max(int *arr)
{
int max_i = -1;
for(int i = 0; i < n; i++)
if(arr[i] > 0 && (max_i == -1 || arr[i] > arr[max_i]))
max_i = i;
return max_i;
}
int find_min(int *arr)
{
int min_i = -1;
for(int i = 0; i < n; i++)
if(arr[i] > 0 && (min_i == -1 || arr[i] < arr[min_i]))
min_i = i;
return min_i;
}
int main()
{
int A[] = {-1, 0, 2, 5, 11};
int max_i = find_max(A), min_i = find_min(A);
printf("%d %d\n", max_i, min_i); // 4 2
}
Можно объединить функцию поиска в одну, добавив в функцию параметр, который будет определять максимальное или минимальное число надо найти:
#include <stdio.h>
int find(int *arr, char is_max)
{
int res_i = -1;
for(int i = 0; i < n; i++)
// Вынес, чтобы сократить запись большой проверки
if(arr[i] >= 0)
continue;
if(res_i == -1 || (is_max ? arr[i] > arr[res_i] : arr[i] < arr[res_i]))
res_i = i;
return res_i;
}
int main()
{
int A[] = {-1, 0, 2, 5, 11};
int max_i = find(A, 1), min_i = find(A, 0);
printf("%d %d\n", max_i, min_i); // 4 2
}
Было две, стала одна – благодаря этому область поиска возможных ошибок сократилась вдвое и модифицировать код стало легче (например, если я захочу переделать алгоритм под поиск в матрице).
Процесс, в результате которого код становится удобнее читать и поддерживать, называется рефакторингом (refactoring).
Также, если у тебя какая-либо функция разрастается настолько, что не помещается даже на полтора экрана – лучше задуматься о вынесении каких-то кусков этой функции в отдельные функции. При этом не важно, что эта функция будет использоваться однократно – зато читать такой код будет намного легче (это важнее).
Заключение
Итого, ты узнал что такое:
- Функция
- Сигнатура функции
- Имя
- Параметры (аргументы)
- Возвращаемое значение
- Определение (прототип) функции
- Реализация функции
- Возвращение нескольких параметров
- Передача массивов
- Объединение функций
- Рефакторинг