Menu

Указатели на функции в C: синтаксис, колбэки и qsort

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

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

Именно эта возможность стоит за сортировкой с произвольным порядком, обработчиками событий, интерфейсами плагинов и конечными автоматами. Это же и тот кусок синтаксиса C, который чаще всего заставляет читателя остановиться и прищуриться, поэтому с него и начнём.

Синтаксис объявления

Возьмём обычную функцию:

int add(int a, int b);

Чтобы объявить указатель, способный хранить её адрес, замените имя на (*имя):

int (*op)(int, int);

Читайте изнутри наружу: op — это указатель на функцию, принимающую (int, int) и возвращающую int.

Скобки здесь не украшение. Без них * привязывается к типу возвращаемого значения:

int (*f)(int);   // указатель на функцию, принимающую int и возвращающую int
int *g(int);     // ФУНКЦИЯ, принимающая int и возвращающая int* - совсем другое

Вот это всё в работе, от начала до конца:

Обе формы вызова работают, потому что обозначение функции автоматически преобразуется в указатель. Пишите op(10, 3); написание (*op)(10, 3) — наследие прошлого.

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

typedef делает это читаемым

Синтаксис объявления быстро становится уродливым, и стандартное лекарство — typedef:

Сравните int apply(BinaryOp op, int x, int y) с сырой формой int apply(int (*op)(int, int), int x, int y). Смысл тот же; но прочитать с первого взгляда можно только одну из них. Общие правила — в статье typedef.

Колбэки: пусть поведение задаёт вызывающий

Колбэк — это функция, которую вы передаёте другой функции, чтобы та могла вызвать вас в нужный момент. Это позволяет одному куску кода управлять структурой работы, пока вызывающий поставляет решение.

Без указателей на функции вам пришлось бы написать count_even, count_positive и count_big — три копии одного и того же цикла. С ними цикл пишется один раз, а параметром становится проверка.

Настоящая отдача: qsort

qsort из стандартной библиотеки — тот пример указателей на функции, которым вы действительно будете пользоваться. Он сортирует массив чего угодно, и удаётся ему это потому, что об элементах он не знает ничего, кроме их размера и предоставленного вами компаратора.

void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

Компаратор получает два const void * — адреса двух элементов — и возвращает отрицательное число, если первый должен идти раньше второго, ноль при равенстве и положительное, если он идёт позже.

Из этого компаратора стоит вынести две привычки. Никогда не пишите return x - y; — выглядит хитро, но переполняется на больших или отрицательных значениях, давая неверный знак и неправильно отсортированный массив. Приводите тип до разыменования, а не после: *(const int *)a, потому что у *a для void * нечего читать.

Та же машинерия сортирует структуры по любому полю:

Идиома (q->score > p->score) - (q->score < p->score) возвращает ровно -1, 0 или 1 без вычитания и без переполнения. А у strcmp уже правильное соглашение о возвращаемом значении, поэтому строковый компаратор умещается в одну строку.

Таблицы указателей на функции

Массив указателей на функции превращает цепочку if/else в поиск по таблице. Именно так интерпретаторы диспетчеризуют опкоды, а системы меню запускают команды.

Добавить операцию теперь значит добавить строку, а не править switch. Соедините это со структурами — и получите стандартную для C форму плагина: структура из указателей на функции и есть то, что в этом языке называется «интерфейсом».

Что может укусить

Сигнатуры должны совпадать точно. Тип указателя на функцию включает типы параметров и тип возвращаемого значения. Присваивание функции с другой сигнатурой — нарушение ограничений, а вызов через указатель неверного типа — неопределённое поведение, даже когда оно «работает». Не приводите сигнатуру компаратора, чтобы заглушить предупреждение, — почините компаратор.

Нулевой указатель на функцию — всё ещё нулевой указатель. Инициализируйте его NULL и проверяйте перед вызовом, ровно как указатели на данные:

if (handler != NULL) {
    handler(event);
}

Почему эта проверка себя оправдывает, объясняют нулевые указатели.

Указатели void лишают типобезопасности, и системой типов становитесь вы. Внутри компаратора для qsort ничто не помешает вам привести аргумент к неверному типу. Если отсортировать массив double компаратором compare_ints, всё скомпилируется без единого замечания и выдаст бессмыслицу.

Синтаксис деклараторов вкладывается. Функция, возвращающая указатель на функцию, выглядит как int (*get_op(char c))(int, int);. Когда обнаружите, что пишете такое, беритесь за typedef:

typedef int (*BinaryOp)(int, int);
BinaryOp get_op(char c);           // то же самое, но читаемо

Часто задаваемые вопросы

Как объявить указатель на функцию в C?

Запишите сигнатуру функции, поставив (*имя) там, где стояло бы имя функции: int (*op)(int, int); объявляет op как указатель на функцию, принимающую два int и возвращающую int. Скобки вокруг *op обязательны — без них вы объявите функцию, возвращающую указатель.

Как вызвать функцию через указатель?

Либо op(3, 4), либо (*op)(3, 4) — оба варианта допустимы и делают одно и то же, потому что обозначение функции автоматически преобразуется в указатель. Современный код на C использует простую форму op(3, 4).

Что такое колбэк в C?

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

Нужен ли & при взятии адреса функции?

Нет. Имя функции само преобразуется в указатель на эту функцию, поэтому op = add; и op = &add; эквивалентны. В большинстве кода на C & опускают.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ