Указатели на функции
Указатели на функции
Как уже обсуждалось ранее функции – это набор команд, которые расположены в соответствующей области памяти. Вызов функции – это сохранение состояния, передача аргументов и переход по адресу, где располагается функция. В си есть возможность создавать указатели на функции. Указатели на функции позволяют упростить решение многих задач. Совместно с void указателями можно создавать функции общего назначения (например, сортировки и поиска). Указатели позволяют создавать функции высших порядков (функции, принимающие в качестве аргументов функции): отображение, свёртки, фильтры и пр. Указатели на функции позволяют уменьшать цикломатическую сложность программ, создавать легко масштабируемые конструкции. Рассмотрим пример. Создадим функцию и указатель на эту функцию
#include <conio.h> #include <stdio.h> int sum(int a, int b) { return a + b; } void main () { //Объявляем указатель на функцию int (*fptr)(int, int) = NULL; int result; //Присваиваем указателю значение - адрес функции //Это похоже на работу с массивом: операцию взятия адреса использовать не нужно fptr = sum; //Вызов осуществляется также, как и обычной функции result = fptr(10, 40); printf("%d", result); getch(); }
Синтаксис объявления указателей на функцию
<возвращаемый тип> (* <имя>)(<тип аргументов>);
#include <conio.h> #include <stdio.h> int dble(int a) { return 2*a; } int deleteEven(int a) { if (a % 2 == 0) { return 0; } else { return a; } } //Функция принимает массив, его размер и указатель на функцию, //которая далее применяется ко всем элементам массива void map(int *arr, unsigned size, int (*fun)(int)) { unsigned i; for (i = 0; i < size; i++) { arr[i] = fun(arr[i]); } } void main () { int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; unsigned i; map(a, 10, deleteEven); for (i = 0; i < 10; i++) { printf("%d ", a[i]); } map(a, 10, dble); printf("\n"); for (i = 0; i < 10; i++) { printf("%d ", a[i]); } getch(); }
В этом примере мы создали функцию отображения, которая применяет ко всем элементам массива функцию, которая передаётся ей в качестве аргумента. Когда мы вызываем функцию map, достаточно просто передавать имена функций (они подменяются указателями). Запишем теперь функцию map, которая получает в качестве аргумента массив типа void:
#include <conio.h> #include <stdio.h> void dbleInt(void *a) { *((int*) a) *= 2; } void deleteEvenInt(void* a) { int tmp = *((int*) a); if (tmp % 2 == 0) { *((int*) a) = 0; } } void dbleDouble(void *a) { *((double*) a) *= 2.0; } void deleteEvenDouble(void* a) { int tmp = *((double*) a); if (tmp % 2 == 0) { *((double*) a) = 0; } } //Функция принимает массив, его размер, размер одного элемента и указатель на функцию, //которая далее применяется ко всем элементам массива void map(void *arr, unsigned num, size_t size, void (*fun)(void *)) { unsigned i; char *ptr = (char*) arr; for (i = 0; i < num; i++) { fun((void*) (ptr + i*size)); } } void main () { int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; double b[] = {1., 2., 3., 4., 5., 6., 7., 8., 9., 10.}; unsigned i; //Работаем с массивом типа int map(a, 10, sizeof(int), deleteEvenInt); for (i = 0; i < 10; i++) { printf("%d ", a[i]); } map(a, 10, sizeof(int), dbleInt); printf("\n"); for (i = 0; i < 10; i++) { printf("%d ", a[i]); } printf("\n"); //Работаем с массивом типа double map(b, 10, sizeof(double), deleteEvenDouble); for (i = 0; i < 10; i++) { printf("%.3f ", b[i]); } map(b, 10, sizeof(double), dbleDouble); printf("\n"); for (i = 0; i < 10; i++) { printf("%.3f ", b[i]); } getch(); }
Вот где нам понадобились указатели типа void. Так как map получает указатель на функцию, то все функции должны иметь одинаковые аргументы и возвращать один
и тот же тип. Но аргументы функций должны быть разного типа, поэтому мы делаем их типа void. Функция map получает указатель типа void (*)(void*), поэтому ей теперь можно
передавать любую из четырёх функций.
Пример другой функции: функция filter получает указатель на массив и возвращает размер нового массива, оставляя в нём только те элементы, для которых переданный предикат
возвращает логическую истину (предикат – функция, которая возвращает истину или ложь). Сначала напишем для массива типа int:
#include <conio.h> #include <stdio.h> #include <stdlib.h> int isOdd(int a) { return (a % 2 != 0); } int isGtThree(int a) { return a > 3; } unsigned int filter(int *arr, unsigned size, int (*pred)(int), int** out) { unsigned i; unsigned j; //размер возвращаемого масива *out = (int*) malloc(sizeof(int)*size); for (i = 0, j = 0; i < size; i++) { if (pred(arr[i])) { (*out)[j] = arr[i]; j++; } } *out = (int*) realloc(*out, j*sizeof(int)); return j; } void main () { int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *aOdd = NULL; int *aGtThree = NULL; unsigned i; unsigned size; size = filter(a, 10, isOdd, &aOdd); for (i = 0; i < size; i++) { printf("%d ", aOdd[i]); } printf("\n"); size = filter(a, 10, isGtThree, &aGtThree); for (i = 0; i < size; i++) { printf("%d ", aGtThree[i]); } free(aOdd); free(aGtThree); getch(); }
Теперь для массива типа void
#include <conio.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> int isOddInt(void *a) { return (*((int*) a) % 2 != 0); } int isGtThreeInt(void* a) { return *((int*) a) > 3; } int isOddDouble(void* a) { return (int)*((double*) a) / 2 != 0; } int isGtThreeDouble(void* a) { return *((double*) a) > 3.0; } unsigned int filter(void *arr, unsigned num, size_t size, int (*pred)(void*), void** out) { unsigned i; unsigned j; //размер возвращаемого масива char* ptrIn = (char*) arr; char* ptrOut = NULL; *out = (void*) malloc(num*size); ptrOut = (char*) (*out); for (i = 0, j = 0; i < num; i++) { if (pred(ptrIn + i*size)) { memcpy(ptrOut + j*size, ptrIn + i*size, size); j++; } } *out = (void*) realloc(*out, j*size); return j; } void main () { int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; double b[] = {1., 2., 3., 4., 5., 6., 7., .8, .9, 10.,}; int *aOdd = NULL; int *aGtThree = NULL; double *bOdd = NULL; double *bGtThree = NULL; unsigned i; unsigned size; size = filter(a, 10, sizeof(int), isOddInt, (void**)&aOdd); for (i = 0; i 7lt; size; i++) { printf("%d ", aOdd[i]); } printf("\n"); size = filter(a, 10, sizeof(int), isGtThreeInt, (void**)&aGtThree); for (i = 0; i < size; i++) { printf("%d ", aGtThree[i]); } printf("\n"); size = filter(b, 10, sizeof(double), isOddDouble, (void**)&bOdd); for (i = 0; i < size; i++) { printf("%.3f ", bOdd[i]); } printf("\n"); size = filter(b, 10, sizeof(double), isGtThreeDouble, (void**)&bGtThree); for (i = 0; i < size; i++) { printf("%.3f ", bGtThree[i]); } free(aOdd); free(bOdd); free(aGtThree); free(bGtThree); getch(); }
Ещё одна функция – свёртка. Она получает в качестве аргументов массив и функцию от двух аргументов. Эта функция действует следующим образом: сначала она применяется к первым двум аргументам, затем она применяется к третьему аргументу и результату предыдущего вызова, затем к четвёртому аргументу и результату предыдущего вызова и т.д. С помощью свёртки можно, например, найти сумму всех элементов массива, максимальный элемент массива, факториал числа и т.п.
#include <conio.h> #include <stdio.h> int sum(int a, int b) { return a + b; } int maxx(int a, int b) { return (a > b)? a: b; } int mul(int a, int b) { return a*b; } void fold(int *arr, unsigned size, int (*fun)(int, int), int *acc) { unsigned i; *acc = fun(arr[0], arr[1]); for (i = 2; i < size; i++) { *acc = fun(*acc, arr[i]); } } void main () { int a[] = {1, 2, 3, 4, 5, 10, 9, 8, 7, 6}; int result; fold(a, 10, sum, &result); printf("%d\n", result); fold(a, 10, maxx, &result); printf("%d\n", result); fold(a, 10, mul, &result); printf("%d\n", result); getch(); }
Последний пример: функция сортировки вставками для массива типа void. Так как тип массива не известен, то необходимо передавать функцию сравнения.
#include <conio.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int cmpIntDesc(void *a, void* b) { return *((int*) a) < *((int*) b); } int cmpIntAsc(void *a, void* b) { return *((int*) a) > *((int*) b); } int cmpDoubleAsc(void *a, void* b) { return *((double*) a) < *((double*) b); } int cmpDoubleDesc(void *a, void* b) { return *((double*) a) > *((double*) b); } void insertionSort(void* arr, unsigned num, size_t size, int (*cmp)(void *a, void *b)) { unsigned i, j; char *ptr = (char*) arr; char *tmp = (char*) malloc(size); for (i = 1; i < num; i++) { if (cmp(ptr + i*size, ptr + (i-1)*size)) { j = i; while (cmp(ptr + j*size, ptr + (j-1)*size) && j > 0) { memcpy(tmp, ptr + j*size, size); memcpy(ptr + j*size, ptr + (j-1)*size, size); memcpy(ptr + (j-1)*size, tmp, size); j--; } } } } void main () { int a[] = {1, 2, 3, 4, 5, 10, 9, 8, 7, 6}; double b[] = {1, 2, 3, 4, 5, 10, 9, 8, 7, 6}; int i; insertionSort(a, 10, sizeof(int), cmpIntAsc); for (i = 0; i < 10; i++) { printf("%d ", a[i]); } printf("\n"); insertionSort(a, 10, sizeof(int), cmpIntDesc); for (i = 0; i < 10; i++) { printf("%d ", a[i]); } printf("\n"); insertionSort(b, 10, sizeof(double), cmpDoubleAsc); for (i = 0; i < 10; i++) { printf("%.3f ", b[i]); } printf("\n"); insertionSort(b, 10, sizeof(double), cmpDoubleDesc); for (i = 0; i < 10; i++) { printf("%.3f ", b[i]); } getch(); }
Массив указателей на функции
Массив указателей на функции определяется точно также, как и обычный массив – с помощью квадратных скобок после имени:
#include <conio.h> #include <stdio.h> #include <stdlib.h> #include <math.h> #define ERROR_DIV_BY_ZERO -2 #define EPSILON 0.000001f float doSum(float a, float b) { return a + b; } float doSub(float a, float b) { return a - b; } float doMul(float a, float b) { return a * b; } float doDiv(float a, float b) { if (fabs(b) <= EPSILON) { exit(ERROR_DIV_BY_ZERO); } return a / b; } void main() { float (*menu[4])(float, float); int op; float a, b; menu[0] = doSum; menu[1] = doSub; menu[2] = doMul; menu[3] = doDiv; printf("enter a: "); scanf("%f", &a); printf("enter b: "); scanf("%f", &b); printf("enter operation [0 - add, 1 - sub, 2 - mul, 3 - div]"); scanf("%d", &op); if (op >= 0 && op < 4) { printf("%.6f", menu[op](a, b)); } getch(); }
Точно также можно было создать массив динамически
void main() { float (**menu)(float, float) = NULL; int op; float a, b; menu = (float(**)(float, float)) malloc(4*sizeof(float(*)(float, float))); menu[0] = doSum; menu[1] = doSub; menu[2] = doMul; menu[3] = doDiv; printf("enter a: "); scanf("%f", &a); printf("enter b: "); scanf("%f", &b); printf("enter operation [0 - add, 1 - sub, 2 - mul, 3 - div]"); scanf("%d", &op); if (op >= 0 && op < 4) { printf("%.6f", menu[op](a, b)); } free(menu); getch(); }
Часто указатели на функцию становятся громоздкими. Работу с ними можно упростить, если ввести новый тип. Предыдущий пример можно переписать так
#include <conio.h> #include <stdio.h> #include <stdlib.h> #include <math.h> #define ERROR_DIV_BY_ZERO -2 #define EPSILON 0.000001f typedef float (*operation)(float, float); float doSum(float a, float b) { return a + b; } float doSub(float a, float b) { return a - b; } float doMul(float a, float b) { return a * b; } float doDiv(float a, float b) { if (fabs(b) <= EPSILON) { exit(ERROR_DIV_BY_ZERO); } return a / b; } void main() { operation *menu = NULL; int op; float a, b; menu = (operation*) malloc(4*sizeof(operation)); menu[0] = doSum; menu[1] = doSub; menu[2] = doMul; menu[3] = doDiv; printf("enter a: "); scanf("%f", &a); printf("enter b: "); scanf("%f", &b); printf("enter operation [0 - add, 1 - sub, 2 - mul, 3 - div]"); scanf("%d", &op); if (op >= 0 && op < 4) { printf("%.6f", menu[op](a, b)); } free(menu); getch(); }
Ещё один пример: функция any возвращает 1, если в переданном массиве содержится хотя бы один элемент, удовлетворяющий условию pred и 0 в противном случае.
#include <conio.h> #include <stdio.h> typedef int (*Predicat)(void*); int isBetweenInt(void* a) { return *((int*) a) > 10 && *((int*) a) < 12; } int isBetweenDouble(void* a) { return *((double*) a) > 10.0 && *((double*) a) < 12.0; } int any(void* arr, unsigned num, size_t size, Predicat pred) { unsigned i; char* ptr = (char*) arr; for (i = 0; i < num; i++) { if (pred(ptr + i*size)) { return 1; } } return 0; } void main() { int a[10] = {1, 1, 2, 2, 3, 0, 1, 2, 1, 3}; double b[10] = {1, 2, 11, 2, 3, 4, 5, 6, 7, 10}; printf("has 'a' any value > 10 and < 12? %d\n", any(a, 10, sizeof(int), isBetweenInt)); printf("has 'b' any value > 10 and < 12? %d", any(b, 10, sizeof(double), isBetweenDouble)); getch(); }
qsort и bsearch
В библиотеке stdlib си имеется несколько функций, которые получают в качестве аргументов указатели на функции. Функция qsort получает такие же аргументы, как и написанная нами функция insertionSort: массив типа void, размер массива, размер элемента и указатель на функцию сравнения. Давайте посмотрим простой пример - сортировка массива строк:
#include <stdio.h> #include <conio.h> #include <stdlib.h> #include <string.h> #define SIZE 10 //qsort передаёт функции сравнения указатели на элементы. //но наш массив - это массив указателей, так что qsort будет //передавать указатели на указатели int cmpString(const void* a, const void* b) { return strcmp(*(const char**) a, *(const char**) b); } void main() { char *words[SIZE]; char buffer[128]; int i, length; printf("Enter %d words:", SIZE); for (i = 0; i < SIZE; i++) { scanf("%127s", buffer); length = strlen(buffer); words[i] = (char*) malloc(length+1); strcpy(words[i], buffer); } printf("\n"); qsort(words, SIZE, sizeof(char*), cmpString); for (i = 0; i < SIZE; i++) { printf("%s\n", words[i]); free(words[i]); } getch(); }
Функция bsearch проводит бинарный поиск в отсортированном массиве и получает указатель на функцию сравнения, такую же, как и функция qsort. В случае, если элемент найден, то она возвращает указатель на этот элемент, если элемент не найден, то NULL.
#include <stdio.h> #include <conio.h> int cmpInt(const void* a, const void* b) { return *(int*) a - *(int*) b; } void main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int elm; int *index; printf("Enter number to search: "); scanf("%d", &elm); index = (int*) bsearch(&elm, a, 10, sizeof(int), cmpInt); if (index == NULL) { printf("element not found"); } else { printf("index = %d", (index - a)); } getch(); }
