Раздел 7 — Функции: основы

Как дать имя алгоритму, вызывать его сколько угодно раз и собирать программу из небольших проверяемых частей

Прогресс курса Раздел 7 из 28

Что вы освоите в этом разделе

2 академических часа теории, практической работы в разделе нет — функции применяются во всех работах дальше, начиная с №7. Раздел заканчивается тестом на портале. Подробно о функциях — область видимости, передача через указатель, указатель на функцию — раздел 15.
01

Зачем нужны функции

В практических работах раздела 6 одни и те же десять строк — «запросить число, выбросить остаток строки, проверить EOF и диапазон» — повторялись в каждой программе, а в одной программе по несколько раз. Повторённый код плох тремя вещами: его дольше читать, ошибку надо исправлять во всех копиях, а копии со временем расходятся.

Функция — именованный фрагмент программы с параметрами и результатом. Её пишут один раз, а вызывают сколько угодно. В блок-схеме вызов функции — символ «предопределённый процесс» (раздел 2, пункт 04). Вы уже вызывали функции: printf, scanf, sqrt. Теперь — свои.

02

Определение и вызов

тип_результата имя(тип параметр1, тип параметр2, …)
{
    тело
    return значение;
}
/* НОД функцией: определение до main, вызов из main */
#include <stdio.h>

int gcd(int a, int b)
{
    while (b != 0) {
        int r = a % b;
        a = b;
        b = r;
    }
    return a;
}

int main(void)
{
    printf("%d\n", gcd(48, 18));            /* 6 */
    int x = 84, y = 36;
    int g = gcd(x, y);                       /* 12 */
    printf("%d/%d = %d/%d\n", x, y, x / g, y / g);   /* 84/36 = 7/3 */
    printf("%d\n", gcd(gcd(12, 18), 8));     /* НОД трёх чисел: 2 */
    return 0;
}
/* Процедура: печать линии из n символов c */
void print_line(int n, char c)
{
    for (int i = 0; i < n; i++) {
        putchar(c);              /* вывести один символ */
    }
    putchar('\n');
}
                                    /* print_line(10, '-');  ->  ---------- */

В Pascal функцию без результата называют процедурой. В C это та же функция, только с типом void.

03

Прототип

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

#include <stdio.h>

int gcd(int a, int b);          /* прототип: объявление без тела */

int main(void)
{
    printf("%d\n", gcd(48, 18));
    return 0;
}

int gcd(int a, int b)           /* определение — ниже */
{
    …
}

Так main стоит в начале файла, и программу удобно читать сверху: сначала что делается, потом как. Заголовочный файл <stdio.h> — это и есть набор прототипов printf, scanf и других (раздел 1, пункт 06).

ОшибкаСообщение gcc
вызов до объявленияerror: implicit declaration of function 'gcd' (с gcc 14 — ошибка, раньше — предупреждение)
аргументов меньше, чем параметровerror: too few arguments to function 'gcd'
аргументов большеerror: too many arguments to function 'gcd'
прототип и определение не совпадаютerror: conflicting types for 'gcd'
прототип есть, определения нетошибка компоновщика: undefined reference to 'gcd'
04

Параметры передаются по значению

При вызове значение каждого аргумента копируется в параметр. Параметр — отдельная переменная функции; изменение параметра не меняет аргумент. В gcd параметры a и b меняются в цикле, а x и y в main остаются 84 и 36 — поэтому их можно вывести после вызова.

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

/* Обмен не работает: функция меняет свои копии */
#include <stdio.h>

void swap(int a, int b)
{
    int t = a;
    a = b;
    b = t;
    printf("inside:  a = %d, b = %d\n", a, b);
}

int main(void)
{
    int x = 1, y = 2;
    swap(x, y);
    printf("outside: x = %d, y = %d\n", x, y);
    return 0;
}
inside:  a = 2, b = 1
outside: x = 1, y = 2

Чтобы функция могла изменить переменную вызывающего, ей передают не значение, а адрес переменной — так работает scanf(…, &n). Это указатели, раздел 10; там swap будет исправлен. До тех пор функция возвращает один результат через return.

05

return и функция-условие

return может стоять в нескольких местах: функция заканчивается на первом выполненном. Так удобно писать функции-проверки, которые возвращают bool:

#include <stdbool.h>

bool is_prime(int n)
{
    if (n < 2) {
        return false;
    }
    for (int d = 2; d <= n / d; d++) {
        if (n % d == 0) {
            return false;          /* нашли делитель — дальше искать незачем */
        }
    }
    return true;
}

bool is_leap(int year)
{
    return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}

Вызов читается как фраза: if (is_prime(n)), days = is_leap(year) ? 29 : 28;. Цикл поиска делителя из раздела 6 стал короче: return из середины цикла заменяет и break, и проверку «почему цикл закончился».

Не на всех путях есть return. Если функция с типом int может дойти до закрывающей скобки без return, вызывающий получит мусор. gcc предупреждает: control reaches end of non-void function [-Wreturn-type]. Типичный случай — if (…) return 1; else if (…) return 0; без последнего else.
06

Локальные переменные и стек вызовов

Переменные, объявленные внутри функции, и её параметры — локальные: они видны только в этой функции и существуют, пока функция выполняется. У main и у gcd могут быть переменные с одинаковыми именами — это разные переменные.

Где они хранятся: при каждом вызове в памяти выделяется кадр — место под параметры и локальные переменные функции и адрес, куда вернуться. Кадры складываются в стек вызовов: вызов кладёт кадр сверху, return его снимает. Для printf("%d", gcd(48, 18)):

  при вызове gcd        внутри gcd                после return
  ┌──────────────┐      ┌──────────────────────┐   ┌──────────────┐
  │              │      │ gcd: a, b, r         │   │              │
  │ main: x, y   │  →   │ main: x, y           │ → │ main: x, y   │
  └──────────────┘      └──────────────────────┘   └──────────────┘

Поэтому значения локальных переменных между вызовами не сохраняются: следующий вызов получает новый кадр. Отладчик (раздел 1, пункт 08) показывает стек командой backtrace (bt): какие функции вызваны и из каких строк. Подробно о времени жизни, static и глобальных переменных — раздел 15.

07

Функция чтения числа

Повтор запроса до верного ввода из раздела 6 — первая функция, которая пригодится во всех следующих программах:

/* Прочитать целое из [min; max]; при неверном вводе — повторить запрос.
   При конце ввода — сообщение и завершение программы с кодом 1. */
int read_int(const char *prompt, int min, int max)
{
    while (1) {
        printf("%s", prompt);
        int value = 0;
        int read = scanf("%d", &value);
        if (read == EOF) {
            printf("\nError: no input\n");
            exit(1);                  /* из <stdlib.h>: завершить программу */
        }
        int ch;
        while ((ch = getchar()) != '\n' && ch != EOF) {
        }
        if (read == 1 && value >= min && value <= max) {
            return value;
        }
        printf("Enter an integer %d..%d\n", min, max);
    }
}
08

Декомпозиция: программа из функций

Задача: вывести простые числа отрезка [a; b] и их количество. Разбиваем на части, каждая — функция с понятным именем:

/* Простые числа отрезка — программа из функций */
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

int read_int(const char *prompt, int min, int max);
bool is_prime(int n);
int print_primes(int a, int b);

int main(void)
{
    int a = read_int("a: ", 1, 100000);
    int b = read_int("b: ", a, 100000);      /* граница зависит от a */
    int count = print_primes(a, b);
    printf("total %d\n", count);
    return 0;
}

/* Вывести простые из [a; b] в строку, вернуть их количество */
int print_primes(int a, int b)
{
    int count = 0;
    for (int i = a; i <= b; i++) {
        if (is_prime(i)) {
            printf("%d ", i);
            count++;
        }
    }
    printf("\n");
    return count;
}

/* is_prime и read_int — как в пунктах 05 и 07 */
a: 1
b: 20
2 3 5 7 11 13 17 19
total 8

main теперь читается как план решения в три строки. Каждую функцию можно проверить отдельно: is_prime — на 1, 2, 9, 97; read_int — на буквах и границах.

Правило курсаПочему
Одна функция — одна задача, и её можно назвать одним глаголом: read_int, is_prime, print_primesесли в названии нужно «и» — это две функции
Функция, которая вычисляет, не выводит; выводит отдельная функция или mainвычисление можно использовать там, где выводить не нужно, и проверить без экрана
Функция помещается на экран — до 30–40 строкдлинную функцию нельзя проверить целиком глазами
Данные функция получает через параметры, результат отдаёт через return; глобальных переменных нетпо заголовку видно всё, от чего зависит функция
Прототипы — перед main, определения — послеmain читается первым, как оглавление

Ключевые выводы раздела

Запомните главное

  • Функция — именованный алгоритм с параметрами и результатом; пишется один раз, вызывается много
  • Заголовок: тип результата, имя, параметры; void — нет результата или нет параметров
  • Функция объявляется до вызова: определением выше или прототипом
  • Аргументы копируются в параметры; функция не меняет переменные вызывающего
  • return завершает функцию; в функции с результатом он есть на каждом пути
  • Локальные переменные живут, пока выполняется функция; кадры вызовов — в стеке
  • Одна функция — одна задача; вычисление отдельно от вывода
Связи раздела. Опирается на раздел 2 — предопределённый процесс в блок-схеме, декомпозиция; разделы 5–6 — тело функций из ветвлений и циклов, повтор ввода. Нужен для: раздела 8 — поиск и сортировки функциями; раздела 10 — указатели как параметры, исправленный swap; раздела 15 — область видимости, static, указатель на функцию; раздела 16 — рекурсия; раздела 18 — функции в отдельных файлах.
Тест по разделу 7 — на учебном портале. Курс на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 6: Циклы Все разделы Раздел 8: Массивы, поиск и сортировка