Указатели на функции

Теги: Указатели на функции. Свёртки, фильтры, отображения, qsort, bsearch.



Указатели на функции

Как уже обсуждалось ранее функции – это набор команд, которые расположены в соответствующей области памяти. Вызов функции – это сохранение состояния, передача аргументов и переход по адресу, где располагается функция. В си есть возможность создавать указатели на функции. Указатели на функции позволяют упростить решение многих задач. Совместно с 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();
}
Q&A

Всё ещё не понятно? – пиши вопросы на ящик email
Указатели типа void