Указатели на функции[1] — очень мощное средство языка С. Хотя нельзя не отметить, что это весьма трудный для понимания термин. Функция располагается в памяти по определенному адресу, который можно присвоить указателю в качестве его значения. Адресом функции является ее точка входа. Именно этот адрес используется при вызове функции. Так как указатель хранит адрес функции, то она может быть вызвана с помощью этого указателя. Он позволяет также передавать ее другим функциям в качестве аргумента.
В программе на С адресом функции служит ее имя без скобок и аргументов (это похоже на адрес массива, который равен имени массива без индексов). Рассмотрим следующую программу, в которой сравниваются две строки, введенные пользователем. Обратите внимание на объявление функции check() и указатель p внутри main(). Указатель p, как вы увидите, является указателем на функцию.
#include <stdio.h> #include <string.h> void check(char *a, char *b, int (*cmp)(const char *, const char *)); int main(void) { char s1[80], s2[80]; int (*p)(const char *, const char *); /* указатель на функцию */ p = strcmp; /* присваивает адрес функции strcmp указателю p */ printf("Введите две строки.\n"); gets(s1); gets(s2); check(s1, s2, p); /* Передает адрес функции strcmp посредством указателя p */ return 0; } void check(char *a, char *b, int (*cmp)(const char *, const char *)) { printf("Проверка на совпадение.\n"); if(!(*cmp)(a, b)) printf("Равны"); else printf("Не равны"); }
Проанализируем эту программу подробно. В первую очередь рассмотрим объявление указателя p в main():
int (*p)(const char *, const char *);
Это объявление сообщает компилятору, что p — это указатель на функцию, имеющую два параметра типа const char * и возвращающую значение типа int. Скобки вокруг p необходимы для правильной интерпретации объявления компилятором. Подобная форма объявления используется также для указателей на любые другие функции, нужно лишь внести изменения в зависимости от возвращаемого типа и параметров функции.
Теперь рассмотрим функцию check(). В ней объявлены три параметра: два указателя на символьный тип (a и b) и указатель на функцию cmp. Обратите внимание на то, что указатель функции cmp объявлен в том же формате, что и p. Поэтому в cmp можно хранить значение указателя на функцию, имеющую два параметра типа const char * и возвращающую значение int. Как и в объявлении p, круглые скобки вокруг *cmp необходимы для правильной интерпретации этого объявления компилятором.
Вначале в программе указателю p присваивается адрес стандартной библиотечной функции strcmp(), которая сравнивает строки. Потом программа просит пользователя ввести две строки и передает указатели на них функции check(), которая их сравнивает. Внутри check() выражение
(*cmp)(a, b)
вызывает функцию strcmp(), на которую указывает cmp, с аргументами a и b. Скобки вокруг *cmp обязательны. Существует и другой, более простой, способ вызова функции с помощью указателя:
cmp(a, b);
Однако первый способ используется чаще (и мы рекомендуем использовать именно его), потому что при втором способе вызова указатель cmp очень похож на имя функции, что может сбить с толку читающего программу. В то же время у первого способа записи есть свои преимущества, например, хорошо видно, что функция вызывается с помощью указателя на функцию, а не имени функции. Следует отметить, что первоначально в С был определен именно первый способ вызова.
Вызов функции check() можно записать, используя непосредственно имя strcmp():
check(s1, s2, strcmp);
В этом случае вводить в программу дополнительный указатель p нет необходимости.
У читателя может возникнуть вопрос: какая польза от вызова функции с помощью указателя на функцию? Ведь в данном случае никаких преимуществ не достигнуто, этим мы только усложнили программу. Тем не менее, во многих случаях оказывается более выгодным передать имя функции как параметр или даже создать массив функций. Например, в программе интерпретатора синтаксический анализатор (программа, анализирующая выражения) часто вызывает различные вспомогательные функции, такие как вычисление математических функций, процедуры ввода-вывода и т.п. В таких случаях чаще всего создают список функций и вызывают их с помощью индексов.
Альтернативный подход — использование оператора switch с длинным списком меток case — делает программу более громоздкой и подверженной ошибкам.
В следующем примере рассматривается расширенная версия предыдущей программы. В этой версии функция check() устроена так, что может выполнять разные операции над строками s1 и s2 (например, сравнивать каждый символ с соответствующим символом другой строки или сравнивать числа, записанные в строках) в зависимости от того, какая функция указана в списке аргументов. Например, строки "0123" и "123" отличаются, однако представляют одно и то же числовое значение.
#include <stdio.h> #include <ctype.h> #include <stdlib.h> #include <string.h> void check(char *a, char *b, int (*cmp)(const char *, const char *)); int compvalues(const char *a, const char *b); int main(void) { char s1[80], s2[80]; printf("Введите два значения или две строки.\n"); gets(s1); gets(s2); if(isdigit(*s1)) { printf("Проверка значений на равенство.\n"); check(s1, s2, compvalues); } else { printf("Проверка строк на равенство.\n"); check(s1, s2, strcmp); } return 0; } void check(char *a, char *b, int (*cmp)(const char *, const char *)) { if(!(*cmp)(a, b)) printf("Равны"); else printf("Не равны"); } int compvalues(const char *a, const char *b) { if(atoi(a)==atoi(b)) return 0; else return 1; }
Если в этом примере ввести первый символ первой строки как цифру, то check() использует compvalues(), в противном случае — strcmp(). Функция check() вызывает ту функцию, имя которой указано в списке аргументов при вызове check(), поэтому она в разных ситуациях может вызывать разные функции. Ниже приведены результаты работы этой программы в двух случаях:
Введите два значения или две строки. тест тест Проверка строк на равенство. Равны Введите два значения или две строки. 0123 123 Проверка значений на равенство. Равны
Сравнение строк 0123[2] и 123 показывает равенство их значений.
[1]Иногда их называют просто указателями функций. Но следует помнить, что в языках программирования под этим термином подразумевается также средство обращения к подпрограмме-функции или встроенной функции, имеющее конструкцию <имя-функции> (<список-аргументов>).
[2]Обратите внимание, что в языке С нулем начинаются восьмеричные константы. Если бы эта запись была в выражении, то 0123 не было бы равно 123. Однако здесь функция atoi() обрабатывает это число как десятичное.