Скомпилированный код живёт в памяти так же, как и данные, поэтому у функции есть адрес. Указатель на функцию — это переменная, которая такой адрес хранит, а раз функцию можно сохранить, её можно передать аргументом, держать их таблицу или выбирать между ними во время выполнения.
Именно эта возможность стоит за сортировкой с произвольным порядком, обработчиками событий, интерфейсами плагинов и конечными автоматами. Это же и тот кусок синтаксиса 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 & опускают.