Указатели на функцию. Функция как параметр (callback)

Практический курс по C/C++: https://stepik.org/course/193691

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

<тип> (*<имя указателя>) (<типы параметров>);

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

int sq_rect(int width, int height)
{
         return width * height;
}

Имя функции sq_rect здесь есть не что иное, как указатель на функцию. Мы в этом можем легко убедиться, если выведем на экран значение этого указателя:

int main(void) 
{
         printf("sq_rect = %p\n", sq_rect);
         return 0;
}

После запуска увидим строку:

sq_rect = 00401439

То есть, функция в данном случае располагается по адресу 0x00401439. Но раз sq_rect – это указатель, значит, можно объявить еще один указатель на эту функцию и присвоить ему адрес sq_rect. Согласно приведенному синтаксису, такой указатель можно объявить следующим образом:

int (*ptr_func) (int, int);

Здесь первый int – это возвращаемый тип функции sq_rect, далее в круглых скобках прописываем имя указателя (придумываем сами) и, затем, в круглых скобках через запятую типы параметров функции. Получаем указатель с именем ptr_func на любую функцию, которая возвращает целое значение и принимает два целочисленных параметра.

Давайте проиницализируем этот указатель на функцию sq_rect:

ptr_func = sq_rect;

и вызовем функцию через ptr_func, передав два аргумента:

int res = ptr_func(2, 3);
printf("ptr_func(2, 3) = %d\n", res);

Увидим результат ее работы:

ptr_func(2, 3) = 6

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

int per_rect(int width, int height)
{
         return 2 * (width + height);
}

Тогда ptr_rect можно инициализировать на нее:

ptr_func = per_rect;

и также вызвать с двумя аргументами:

int res = ptr_func(2, 3);

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

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

void print_hi(void)
{
         puts("Hi!");
}

Тогда при попытке инициализации указателя ptr_func такой функцией:

ptr_func = print_hi;

получим предупреждение о несовпадении типов. Другие версии компиляторов могут здесь выдавать ошибку и останавливать процесс трансляции программы. Очень важно, чтобы тип указателя на функцию и сама функция совпадали. Иначе, это может привести к серьезным ошибкам в процессе выполнения программы. Поэтому делать такие присваивания недопустимо.

Для функции print_hi указатель следует объявить следующим образом:

void (*ptr_hi) (void);
ptr_hi = print_hi;
ptr_hi();

Параметры как указатели на функции

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

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

#include <stdio.h>
 
#define SIZE        10
 
void filter(int dst[], size_t size_dst, 
                            const int src[], size_t size_src, 
                            int (*is_correct)(int))
{
         for(int i = 0; i < size_dst; ++i)
                   dst[i] = 0;
 
         for(int i = 0, j = 0; i < size_src; ++i)
                   if(is_correct(src[i]))
                            dst[j++] = src[i];
}
 
int is_even(int x)
{
         return x % 2 == 0;
}
 
int main(void) 
{
         int digits[] = {-3, 4, 10, 11, -5, 3};
         int result[SIZE];
 
         filter(result, SIZE, digits, sizeof(digits) / sizeof(*digits), is_even);
         
         for(int i = 0; i < SIZE; ++i)
                   printf("%d ", result[i]);
 
         return 0;
}

Вначале объявлена функция filter(), которая принимает два массива с их размерами и ссылку на функцию. Затем, массив dst, в который заносится результат, обнуляется и в следующем цикле выполняется копирование только тех значений из второго массива src, для которых функция is_correct() возвращает не нулевое значение. То есть, указатель is_correct определяет критерий отбора значений.

После функции filter() объявлена еще одна функция is_even(), которая будет использоваться в качестве критерия отбора: для четных значений она возвращает единицу, а для нечетных – ноль.

В функции main() объявляются два массива и, затем, вызывается функция filter(), в которую передаются указатели на эти массивы, их длины и указатель на функцию is_even(). То есть, параметр is_correct будет ссылаться на функцию is_even().

После запуска программы увидим следующий результат:

4 10 0 0 0 0 0 0 0 0

Как видите, в массив result попали только четные значения из массива digits.

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

int is_positive(int x)
{
         return x > 0;
}

И указываем ее при вызове функции filter():

filter(result, SIZE, digits, sizeof(digits) / sizeof(*digits), is_positive);

Запускаем программу, получаем результат:

4 10 11 3 0 0 0 0 0 0

Видите, какие красивые конструкции можно формировать, используя указатели на функции?

Массивы из указателей на функции

Можно пойти еще дальше и определить массив из указателей на функции. Общий синтаксис здесь следующий:

<тип> (*<имя массива>[<размер>]) (<параметры>)

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

int is_odd(int x)
{
         return x % 2 != 0;
}

И сформируем массив из указателей на эти критерильные функции:

int (*criterials[]) (int) = {is_even, is_odd, is_positive};

Теперь в filter() можно передавать одну из этих функций, просто указывая нужный индекс массива criterials:

filter(result, SIZE, digits, sizeof(digits) / sizeof(*digits), criterials[1]);

Это может быть полезно, когда у пользователя запрашивается критерий отбора, например, в виде меню:

1. Четные значения.
2. Нечетные значения.
3. Положительные значения.

И выбранный номер select_id просто преобразуется в индекс indx = select_id - 1 массива criterials.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме