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

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

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

К ак уже обсуждалось ранее функции – это набор команд, которые расположены в соответствующей области памяти. Вызов функции – это сохранение состояния, передача аргументов и переход по адресу, где располагается функция. В си есть возможность создавать указатели на функции. Указатели на функции позволяют упростить решение многих задач. Совместно с void указателями можно создавать функции общего назначения (например, сортировки и поиска). Указатели позволяют создавать функции высших порядков (функции, принимающие в качестве аргументов функции): отображение, свёртки, фильтры и пр. Указатели на функции позволяют уменьшать цикломатическую сложность программ, создавать легко масштабируемые конструкции. Рассмотрим пример. Создадим функцию и указатель на эту функцию

Синтаксис объявления указателей на функцию
(* )( );

В этом примере мы создали функцию отображения, которая применяет ко всем элементам массива функцию, которая передаётся ей в качестве аргумента. Когда мы вызываем функцию map, достаточно просто передавать имена функций (они подменяются указателями). Запишем теперь функцию map, которая получает в качестве аргумента массив типа void:

Вот где нам понадобились указатели типа void. Так как map получает указатель на функцию, то все функции должны иметь одинаковые аргументы и возвращать один и тот же тип. Но аргументы функций должны быть разного типа, поэтому мы делаем их типа void. Функция map получает указатель типа void (*)(void*), поэтому ей теперь можно передавать любую из четырёх функций.
Пример другой функции: функция filter получает указатель на массив и возвращает размер нового массива, оставляя в нём только те элементы, для которых переданный предикат возвращает логическую истину (предикат – функция, которая возвращает истину или ложь). Сначала напишем для массива типа int:

Теперь для массива типа void

Ещё одна функция – свёртка. Она получает в качестве аргументов массив и функцию от двух аргументов. Эта функция действует следующим образом: сначала она применяется к первым двум аргументам, затем она применяется к третьему аргументу и результату предыдущего вызова, затем к четвёртому аргументу и результату предыдущего вызова и т.д. С помощью свёртки можно, например, найти сумму всех элементов массива, максимальный элемент массива, факториал числа и т.п.

Последний пример: функция сортировки вставками для массива типа void. Так как тип массива не известен, то необходимо передавать функцию сравнения.

Массив указателей на функции

М ассив указателей на функции определяется точно также, как и обычный массив – с помощью квадратных скобок после имени:

Точно также можно было создать массив динамически

Часто указатели на функцию становятся громоздкими. Работу с ними можно упростить, если ввести новый тип. Предыдущий пример можно переписать так

Ещё один пример: функция any возвращает 1, если в переданном массиве содержится хотя бы один элемент, удовлетворяющий условию pred и 0 в противном случае.

qsort и bsearch

В библиотеке stdlib си имеется несколько функций, которые получают в качестве аргументов указатели на функции. Функция qsort получает такие же аргументы, как и написанная нами функция insertionSort: массив типа void, размер массива, размер элемента и указатель на функцию сравнения. Давайте посмотрим простой пример — сортировка массива строк:

Функция bsearch проводит бинарный поиск в отсортированном массиве и получает указатель на функцию сравнения, такую же, как и функция qsort. В случае, если элемент найден, то она возвращает указатель на этот элемент, если элемент не найден, то NULL.

Читайте также:  Как восстановить пароль в аппсторе на айфоне

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

Теперь давайте поработаем с нашими студентами.

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

Подключим в файле student.h следующие библиотеки

А в файле student.c добавим вот такую функцию, которая, используя указатель на тип структуры и будет выводить в консоль данные переданного студента

Как видим, ничего сложного здесь нет, мы пользуемся операцией взятия адреса того или иного поля структуры и выводим значение данных полей по очереди в консоль.

Создадим на данную функцию прототип в заголовочном файле, вернёмся в функцию main() файла main.c, закомментируем там предыдущий код, создадим студента, проинициализируем его поля обычным способом (не через указатель) и передадим с помощью нашей новой функции с помощью взятия адреса указатель на него

Проверим, как всё сработало

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

Оказывается, есть ещё один способ передачи универсального указателя на любые данные, даже на те типы, которые глобально нигде не объявлены. Мы помним про тип данных void. Мы его используем в качестве формального типа данных, когда нам вообще не нужны никакие данные, а они должны быть, как в случае с параметрами функции — входящими и возвращаемыми.

Но мы можем также создать и указатель на такой тип данных. Это будет такой себе универсальный указатель, который затем с помощью приведения типа мы можем превратить в указатель на любые данные, в т.ч. и структуры, и массивы и т.д.

Давайте добавим ещё одну подобную функцию в файле student.c, которая будет принимать в качестве аргумента именно такой указатель

Получилось довольно-таки красиво. Мы, зная о том, что мы передаём именно указатель на переменную структуры, передали его в виде указателя на void или на ничего.

Вроде всё красиво, но при этом теряется смысл void. Мы всё равно используем глобальный тип данных. Нам нужно не это. Мы должны представить, что нигде про существование такой структуры как студент, кроме наших функций main() и этой функции, неизвестно.

Поэтому мы объявляем структуру прямо здесь, затем, применив приведение типа указателя на void к указателю на тип нашей структуры, и функция теперь приобретёт вот такой вид

Теперь ей не нужны никакие глобальные структуры.

Создадим в заголовочном файле прототип на нашу функцию, вернёмся в функцию main() файла main.c, закомментируем там предыдущий код, объявим подобную структуру (имя уже не важно, главное чтобы поля были таких же типов, причём их имена тоже не важны), создадим переменную её типа, проинициализируем её поля и вызовем нашу новую функцию, передав ей в качестве параметра указатель на нашего студента, приведя его к типу void

Читайте также:  Что открывает кнопка пуск

Мы создали нашего студента через анонимную структуру, не присвоив никаких имён самой структуре, а сразу создав переменную, мы так тоже можем делать. Хотя можем создать и стандартным образом, объявив структуру и используя имя структуры создать переменную.

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

printStudentvoid(&st);

Но в то же время мы должны помнить, что мы можем применить и приведение типа, а то, мало ли, попадётся какой-нибудь привередливый компилятор и начнёт ругаться.

Запустим на выполнение наш код и увидим, что у нас всё также прекрасно работает через тип указателя на void

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

Теперь на закуску поработаем с массивом студентов. Мы помним, что у нас была функция добавления студента в массив, теперь мы подобную функцию добавим в файл student.c

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

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

Чаще всего к ошибкам приводит использование указателей па функцию. Хотя функция — это не переменная, она по-прежнему имеет физическое положение в памяти, которое может быть присвоено указателю. Адрес, присвоенный указателю, является входной точкой в функцию. Указатель может использоваться вместо имени функции. Он также позволяет передавать функции как обычные аргументы в другие функции.

Чтобы понять, как работают указатели на функции, необходимо понять, как компилируются и вызываются функции в С. По мере компиляции функций исходный код преобразуется в объектный и устанавливается точка входа. При вызове функции происходит обращение к данной точке входа. Поэтому указатель на функцию может использоваться для вызова функции.

Адрес функции получается при использовании имени функции без каких-либо скобок или аргументов. (Очень похоже на массивы, где адрес получается с использованием имени массива без индексов.) Рассмотрим следующую программу, предназначенную для демонстрации нюансов объявления:

#include
#include
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() */
gets(s1);
gets (s2);
check(s1, s2, p);
return 0;
>

Читайте также:  Как в excel напечатать на одном листе

void check (char *a, char *b, int (*cmp) (const char *, const char *))
<
printf("Testing for equality.
");
if(!(*cmp) (a, b)) printf("Equal");
else printf("Not equal");
>

Когда вызывается check(), ей передаются два указателя на символы и один указатель на функцию. В функции check() аргументы объявляются как указатели на символы и указатель на функцию. Надо обратить внимание на способ объявления указателя на функцию. Следует использовать аналогичный метод при объявлении других указателей на функцию, за исключением тех случаев, когда отличается возвращаемый тип или передаваемые параметры. Скобки вокруг *cmp необходимы для правильной интерпретации компилятором данного выражения.

При объявлении указателя на функцию можно по-прежнему использовать прототип, как показано в предыдущей программе. Тем не менее в большинстве случаев имена настоящих параметров неизвестны. Поэтому можно оставить имена пустыми или можно использовать любые понравившиеся имена.

Рассмотрим работу функции strcmp() в функции check(). Оператор

if (!(*cmp) (a, b) ) printf("Equal");

осуществляет вызов функции, в данном случае strcmp(), с помощью cmp, который указывает на данную функцию. Вызов происходит с аргументами a и b. Данный оператор демонстрирует общий вид использования указателя на функцию для вызова функции, на которую он указывает. Круглые скобки вокруг *cmp необходимы вследствие наличия приоритетов. На самом деле можно напрямую использовать cmp, как показано ниже:

if (!cmp(a, b)) printf("Equal");

Данная версия также вызывает функцию, на которую указывает cmp, но она использует нормальный синтаксис. Использование ( cmp) помогает всем, читающим программу, понять, что указатель на функцию используется для вызова функции вместо вызова функции cmp. Возможно вызвать напрямую check(), используя strcmp, как показано ниже:

check(s1, s2, strcmp);

Данный оператор устраняет необходимость наличия дополнительной переменной-указателя.

Можно задаться вопросом, почему кто-то хочет написать программу таким способом. В данном примере ничего не достигается, но появляются большие проблемы. Тем не менее иногда бывают моменты, когда выгодно передавать функции в процедуры или хранить массивы функций. Следующий пример демонстрирует использование указателей на функции. При написании интерпретатора наиболее типично использование вызовов функций для различных подпрограмм поддержки, типа синус, косинус и тангенс. Вместо использования большого списка для оператора switch, можно использовать массив указателей на функции, в котором доступ к необходимой функции осуществляется с помощью индекса. В данной программе check() может использоваться для проверки как алфавитного, так и численного равенства простым вызовом различных функций сравнения.

#include
#include
#include
#include
void check(char *a, char *b, int (*cmp) (const char *, const char *));
int numcmp (const char *a, const char *b) ;
int main(void)
<
char s1[80], s2 [80];
gets (s1);
gets (s2);
if(isalpha(*s1))
check (s1, s2, strcmp);
else
check(s1, s2, numcmp);
return 0;
>

void check(char *a, char *b, int (*cmp) (const char *,const char *))
<
printf("Testing for equality.
");
if(!(*cmp) (a, b)) printf ("Equal");
else printf("Hot equal");
>

int numcmp (const char *a, const char *b)
<
If(atoi(a)==atoi(b)) return 0;
else return 1;
>

Ссылка на основную публикацию
Adblock detector