Раздел 15 — Функции подробно

Где живут переменные, как функция возвращает несколько результатов и как передать одну функцию в другую

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

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

3 академических часа теории, практической работы в разделе нет: приёмы раздела применяются в практических работах №15–17 (раздел 17) и всех следующих. Раздел заканчивается тестом на портале.
01

Область видимости

Область видимости имени — часть текста программы, где к нему можно обратиться. В C их три вида:

Где объявленоГде видноПример
в блоке { … }, в том числе в теле функцииот объявления до конца блокалокальная переменная, переменная цикла for (int i …)
в списке параметровво всём теле функцииint n в int sum(int n)
вне всех функцийот объявления до конца файлаглобальная переменная, функция, typedef, enum

Имя во внутреннем блоке скрывает такое же имя внешнего: до конца блока x означает внутреннюю переменную, внешняя недоступна, но не меняется (examples/15/scope.c):

int x = 1;
{
    int x = 2;                  /* скрывает внешнюю x до конца блока */
    printf("inner x = %d\n", x);   /* inner x = 2 */
}
printf("outer x = %d\n", x);       /* outer x = 1 */

Компилятор сокрытие не запрещает и по умолчанию о нём молчит (предупреждение — ключ -Wshadow). Это частый источник ошибок «присвоил, а значение не изменилось»: присвоено внутренней переменной. Правило курса — не давать внутренним переменным имена внешних.

02

Время жизни: автоматические и static

Время жизни — когда переменная существует в памяти. Видимость — про текст программы, время жизни — про её выполнение.

/* Номер вызова: static-локальная создаётся один раз и хранит значение между вызовами */
int next_id(void)
{
    static int id = 100;            /* инициализация — один раз, до запуска программы */
    calls++;
    return id++;
}

/* Обычная локальная: новый кадр — новая переменная */
int fresh(void)
{
    int n = 100;
    calls++;
    return n++;
}
next_id 100, fresh 100
next_id 101, fresh 100
next_id 102, fresh 100
calls = 6

Статические и глобальные переменные лежат не в стеке, а в отдельной области памяти программы, размер которой не ограничен стеком. Поэтому массив на миллион элементов из раздела 12 объявляют static bool composite[1000001]; внутри функции или вне функций — в стеке (1 МБ в Windows) он не поместится.

Нельзя возвращать адрес автоматической переменной. Она исчезает вместе с кадром функции, и указатель смотрит в память, которую займёт следующий вызов. gcc предупреждает и подставляет вместо адреса NULL, чтобы ошибка проявилась сразу:
int *make_value(void)
{
    int value = 42;
    return &value;                  /* value исчезнет вместе с кадром функции */
}
warning: function returns address of local variable [-Wreturn-local-addr]; проверка в main показывает, что указатель равен NULL. Результат возвращают значением, через параметр-указатель (пункт 5) или в динамической памяти (раздел 17).
03

Глобальные переменные: почему их избегают

Глобальную переменную может изменить любая функция файла. Поэтому:

Данные передают параметрами, результат возвращают. Глобальными оставляют константы (const double PI = 3.14159265358979;, enum), большие таблицы, которые программа только читает, и счётчики для отладки, как calls в примере выше. Если значение нужно хранить между вызовами одной функции — static-локальная: её никто, кроме этой функции, не видит.

04

Передача параметров и const

В C параметры передаются только по значению: функция получает копию аргумента. Изменить переменную вызывающего функция может, только получив её адрес (раздел 10). Отсюда три вида параметров:

ПараметрЧто получает функцияКогда
int n, double xкопию значениявходные числа, символы, небольшие структуры
int *resultадрес переменной вызывающегофункция должна её изменить: результат, счётчик
const Student *s, const int a[]адрес, но менять по нему нельзябольшая структура или массив только для чтения

Массив передаётся адресом первого элемента, даже если параметр записан как int a[10]: внутри функции a — указатель, и число 10 компилятор игнорирует. Поэтому размер массива передают отдельным параметром, а sizeof параметра-массива — ошибка:

int count(int a[10])
{
    return (int)(sizeof a / sizeof a[0]);   /* 8 / 4 = 2, а не 10 */
}
warning: ‘sizeof’ on array function parameter ‘a’ will return size of ‘int *’ [-Wsizeof-array-argument]
in main 10, in function 2

const в параметре — обещание функции не менять данные. Компилятор держит обещание: присваивание a[i] = 0 в функции с const int a[] — ошибка сборки. А вызывающий видит по заголовку, что его массив останется целым. У указателя const бывает в двух местах:

ЗаписьНельзяМожно
const int *p*p = 5 — менять данныеp++ — двигать указатель
int *const pp++ — двигать указатель*p = 5
const int *const pни то, ни другоетолько читать

Правило чтения — справа налево от имени: const int *p — «p — указатель на int, который const». В параметрах почти всегда нужен первый вид: функция читает чужие данные через собственную копию указателя.

05

Несколько результатов

return возвращает одно значение. Когда результатов несколько, есть три способа (examples/15/results.c):

1. Параметры-указатели — функция пишет результаты по адресам, которые ей дали:

/* Минимум и максимум массива — через указатели; n >= 1 */
void min_max(const int a[], int n, int *min, int *max)
{
    *min = *max = a[0];
    for (int i = 1; i < n; i++) {
        if (a[i] < *min) {
            *min = a[i];
        }
        if (a[i] > *max) {
            *max = a[i];
        }
    }
}
…
int lo, hi;
min_max(a, 5, &lo, &hi);          /* min -3, max 12 */

2. Признак успеха — результатом, значение — через указатель. Так устроены scanf и fread: результат говорит, получилось ли, а данные лежат там, куда указали. Значение по указателю меняется только при успехе:

/* Строка -> int: результат — признак успеха, число — через указатель */
bool parse_int(const char *text, int *value)
{
    char *end = NULL;
    errno = 0;
    long v = strtol(text, &end, 10);
    if (end == text || *end != '\0' || errno == ERANGE || v < INT_MIN || v > INT_MAX) {
        return false;
    }
    *value = (int)v;
    return true;
}
"42" -> 42
"-17" -> -17
"12abc" -> error
"" -> error
"99999999999" -> error

strtol при выходе за диапазон long возвращает предельное значение и ставит errno в ERANGE; поэтому errno обнуляют перед вызовом. long в Windows — 32 бита, в Linux — 64, проверка v > INT_MAX нужна для Linux.

3. Структура — когда результаты связаны по смыслу и их удобно передавать дальше вместе. Структура возвращается целиком, копированием:

typedef struct {
    int count;
    double mean;
} Stats;

Stats stats(const int a[], int n);     /* Stats s = stats(a, 5); — count 5, mean 4.20 */
06

Указатель на функцию

Код функции тоже лежит в памяти, и у него есть адрес. Имя функции без скобок — этот адрес. Его можно сохранить в переменную, положить в массив и передать в другую функцию, а потом вызвать функцию через указатель. Тип указателя записывает, какие параметры функция принимает и что возвращает; его удобно назвать через typedef:

typedef double (*Operation)(double, double);   /* указатель на функцию (double, double) -> double */

double add(double a, double b) { return a + b; }   /* так же sub и mul */

/* Применить f к каждому элементу: a[i] = f(a[i], arg) */
void apply(double a[], int n, Operation f, double arg)
{
    for (int i = 0; i < n; i++) {
        a[i] = f(a[i], arg);
    }
}

double a[] = {1, 2, 3};
apply(a, 3, mul, 10);           /* имя функции без скобок — её адрес */
apply(a, 3, add, 0.5);           /* 10.5 20.5 30.5 */
07

qsort и bsearch

Главное применение указателя на функцию — библиотечная сортировка qsort из stdlib.h. Она сортирует массив любого типа: ей передают адрес массива, число элементов, размер элемента и функцию сравнения, которая знает, как сравнить два элемента:

void qsort(void *base, size_t n, size_t size, int (*compare)(const void *, const void *));

Функция сравнения получает адреса двух элементов как const void * — «указатель неизвестно на что» — и возвращает отрицательное число, если первый должен стоять раньше, ноль, если равны, и положительное, если позже. Внутри указатели приводят к настоящему типу:

/* Сравнение int без вычитания: a - b переполняется на больших числах */
int compare_int(const void *a, const void *b)
{
    int x = *(const int *)a;
    int y = *(const int *)b;
    return (x > y) - (x < y);
}

/* По убыванию балла, при равенстве — по имени */
int by_score_desc(const void *a, const void *b)
{
    const Student *s = a;
    const Student *t = b;
    if (s->score != t->score) {
        return s->score < t->score ? 1 : -1;
    }
    return strcmp(s->name, t->name);
}

qsort(numbers, n, sizeof numbers[0], compare_int);
qsort(group, m, sizeof group[0], by_score_desc);
-2000000000 -7 0 13 42 2000000000
Kuznetsova 5.00
Antonov 4.75
Ivanov 4.75
Sidorov 4.50
Petrova 3.80
Сравнение вычитанием return x - y; ломается на больших числах. 2 000 000 000 − (−2 000 000 000) не помещается в int: переполнение — неопределённое поведение (раздел 3), на практике получается отрицательное число, и qsort «узнаёт», что 2·10⁹ меньше −2·10⁹. Массив {2000000000, -2000000000, 0} после такой сортировки остаётся 2000000000 -2000000000 0. Выражение (x > y) - (x < y) даёт −1, 0 или 1 без переполнения.

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

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

  • Видимость — где имя доступно в тексте; время жизни — когда переменная существует при выполнении
  • Внутреннее имя скрывает внешнее; одинаковых имён во вложенных блоках избегают
  • static-локальная видна в своей функции, живёт всю программу и инициализируется один раз
  • Глобальные переменные — только для констант и данных «только для чтения»; данные передают параметрами
  • Адрес локальной переменной из функции не возвращают
  • Параметры — копии; изменить переменную вызывающего можно только через указатель; массив приходит указателем, размер — отдельным параметром
  • const в параметре-указателе обещает не менять данные: const int *p
  • Несколько результатов — параметры-указатели, «успех + значение через указатель» или структура
  • Имя функции без скобок — её адрес; тип указателя на функцию называют через typedef
  • qsort/bsearch — с функцией сравнения (const void *, const void *), без вычитания; qsort неустойчива
Связи раздела. Опирается на раздел 7 — функции, кадр стека; раздел 8 — сортировки и двоичный поиск; раздел 10 — указатели как параметры; раздел 13 — структуры и ->. Нужен для: раздела 16 — рекурсия и сортировки за O(n log n), сравнение с qsort; раздела 17 — функции над динамическим массивом и списком (ПЗ №15–17); раздела 18 — static и extern для функций и переменных в разных файлах; раздела 23 — функции сравнения и лямбды в std::sort.
Тест по разделу 15 — на учебном портале. Практической работы в разделе нет; тест открывает раздел 16. Курс — на portal.nevabit.ru, учётную запись выдаёт преподаватель.
Раздел 14: Двоичные файлы Все разделы Раздел 16: Рекурсия