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

   ⁄ 

 Обновлено 4 Янв 2018  ⁄ 

⁄   1990

В 80 уроке мы узнали, что указатель — это переменная, которая содержит адрес другой переменной. Указатели на функции аналогичны, за исключением того, что вместо переменных, они указывают на функции!

Рассмотрим следующий фрагмент кода:

Идентификатор boo — это имя функции. Но какой её тип? Функции имеют свой собственный l-value тип — в этом случае это тип функции, который возвращает целочисленное значение и не принимает никаких параметров. Подобно переменным, функции также имеют свой адрес в памяти.

Когда функция вызывается (с помощью оператора ()), точка выполнения переходит к адресу вызываемой функции:

Одной из распространенных ошибок является:

Вместо вызова функции boo() и вывода возвращаемого значения мы, совершенно случайно, отправили функцию boo непосредственно в std::cout. Что произойдет в этом случае?

Результат на компьютере автора:

002B1050

Ну а у вас может быть и какое-то другое значение (например, 1), в зависимости от того, в какой тип ваш компилятор решит преобразовать указатель на функцию. Если ваш компьютер не вывел адрес функции, то вы можете заставить его это сделать, конвертируя boo в указатель типа void и отправляя его на вывод:

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



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

Синтаксис создания неконстантного указателя на функцию, пожалуй, один из самых уродливых, которые есть в C++:

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

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

Для создания константного указателя на функцию — используйте const после звёздочки:

Если вы поместите const перед int, тогда это будет означать, что функция, на которую указывает указатель, возвращает const int.

Присваивание функции указателю на функцию

Указатель на функцию может быть инициализирован функцией (и неконстантному указателю на функцию тоже можно присвоить функцию):

Одна из распространенных ошибок, которую совершают новички:

Здесь мы фактически присваиваем возвращаемое значение из вызова функции doo() для fcnPtr, чего мы не хотим. Мы хотим, чтобы fcnPtr содержал адрес функции doo, а не возвращаемое значение из doo(). Поэтому скобки не нужны.

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

В отличие от фундаментальных типов данных, C++ неявно конвертирует функцию в указатель на функцию, если это необходимо (поэтому вам не нужно использовать оператор адреса (&) для получения адреса функции). Однако C++ не будет неявно конвертировать указатель на функцию в указатель типа void или наоборот.

Вызов функции через указатель на функцию

Вы также можете использовать указатель на функцию для вызова самой функции. Есть два способа сделать это. Первый — через явное разыменование:

Второй — через неявное разыменование:

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

Примечание: параметры по умолчанию не будут работать с функциями, вызванными через указатели на функции. Параметры по умолчанию обрабатываются во время компиляции (то есть, вам нужно предоставить аргумент для параметра по умолчанию во время компиляции). Однако указатели на функции обрабатываются во время выполнения. Следовательно, параметры по умолчанию не могут обрабатываться при вызове функции через указатель на функцию. В этом случае вам нужно будет явно передать значения для параметров по умолчанию.

Передача функций в качестве аргументов для других функций

Одна из самых полезных вещей, которую вы можете сделать с указателями на функции – это передать функцию в качестве аргумента другой функции. Функции, используемые в качестве аргументов для других функций, называются функциями обратного вызова (callback functions).

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

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

Вот наша сортировка методом выбора из соответствующего урока:

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

А вот уже сортировка методом выбора с функцией ascending() для сравнения чисел:

Теперь, чтобы позволить caller-у решать, каким образом будет выполняться сортировка, вместо использования нашей собственной функции сравнения, мы разрешим caller-у предоставить свою собственную функцию сравнения! Это делается с помощью указателя на функцию.

Поскольку функция сравнения caller-а будет сравнивать два целых числа и возвращать логическое значение, то указатель на такую функцию будет выглядеть примерно так:

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



Вот готовый код сортировки методом выбора с выбором способа сортировки в caller-е (в main):

Результат выполнения программы выше:

8 7 6 5 4 3 2 1
1 2 3 4 5 6 7 8

Прикольно, правда? Мы дали caller-у возможность контролировать процесс сортировки чисел. Caller может  определить любые другие свои функции сравнения:

Результат:

2 4 6 8 1 3 5 7

Как вы можете видеть, использование указателя на функцию позволяет caller-у «подключить» свою собственную функциональность к чему-то, что мы писали и тестировали ранее, что способствует повторному использованию кода! Раньше, если вы хотели отсортировать один массив в порядке убывания, а другой в порядке возрастания, вам понадобилось бы написать несколько версий сортировки массива. Теперь же у вас может быть одна версия, которая будет выполнять сортировку любым способом, которым вы только захотите!

Параметры по умолчанию в функциях

Если вы позволите caller-у передавать функцию в качестве параметра, то полезным будет предоставить и некоторые стандартные функции для удобства caller-а. Например, в примере сортировки методом выбора выше, было бы проще установить дефолтный (по умолчанию) способ сравнения чисел. Например:

В этом случае, до тех пор, пока пользователь вызывает selectionSort обычно (не через указатель на функцию), то параметр comparisonFcn будет по умолчанию соответствовать функции ascending().

Указатели на функции с typedef и type aliase

Посмотрим правде в глаза — синтаксис указателей на функции уродлив. Тем не менее, с помощью typedefs мы можем исправить эту ситуацию:

Здесь мы определили псевдоним типа (typedef) под названием validateFcn, который является указателем на функцию, которая принимает два int-а и возвращает bool.

Теперь вместо написания этого:

Мы можем писать так:

Так гораздо лучше, не так ли? Однако синтаксис определения самого typedef может быть несколько трудным для запоминания. В C++11 вместо typedef вы можете использовать type alias для создания псевдонима типа указателя на функцию:

Это уже читается более естественно, чем с typedef, так как имя псевдонима и определение псевдонима расположены на противоположных сторонах от знака =.

Использование type alias идентично использованию typedef:

Использование std::function в C++11

В C++11 ввели альтернативный способ определения и хранения указателей на функции, который выполняется с использованием std::function. std::function является частью заголовочного файла <functional> стандартной библиотеки C++. Для определения указателя на функцию с помощью этого способа — объявите объект std::function следующим образом:

Как вы можете видеть, тип возврата и параметры находятся в угловых скобках, а параметры еще и внутри других скобок. Если параметров нет, то внутренние скобки можно оставить пустыми. Здесь становится более явным какой тип возвращаемого значения и какие ожидаемые параметры функции (тогда как с typedef это не так явно).

Обновим наш предыдущий пример выше, но уже с использованием std::function:

Итого

Указатели на функции полезны, прежде всего, когда вы хотите хранить функции в массиве (или другой структуре) или когда вам нужно передать одну функцию в качестве параметра другой функции. Поскольку синтаксис объявления указателей на функции является несколько уродливым и подверженным ошибкам, то мы рекомендуем использовать type alias (или std::function в C++11).

Тест

1. В этот раз мы попытаемся написать версию базового калькулятора с помощью указателей на функции.

a) Напишите короткую программу, которая просит пользователя ввести два целых числа и выбрать математическую операцию (‘+’, ‘-‘, ‘*’ или ‘/’). Убедитесь, что пользователь ввел корректную операцию (используйте проверку).

Ответ a)

b) Напишите функции add(), subtract(), multiply() и divide(). Они должны принимать два целочисленных параметра и возвращать целочисленное значение.

Ответ b)

c) Создайте typedef с именем arithmeticFcn для указателя на функцию, которая принимает два целочисленных параметра и возвращает целочисленное значение.

Ответ c)

d) Напишите функцию с именем getArithmeticFcn(), которая принимает символ выбранного математического оператора и возвращает соответствующую функцию в качестве указателя на функцию.

Ответ d)

e) Добавьте в main() вызов getArithmeticFcn().

Ответ e)

f) Соедините все части вместе.

Полная программа:

Показать

2. Теперь давайте изменим программу, которую мы написали в 1-ом задании, чтобы переместить логику из getArithmeticFcn в массив.

a) Создайте структуру с именем arithmeticStruct, которая имеет два члена: математический оператор char и указатель на функцию arithmeticFcn.

Ответ a)

b) Создайте статический глобальный массив arithmeticArray, используя структуру arithmeticStruct, который будет инициализирован каждой из четырех математических операций.

Ответ b)

c) Измените getArithmeticFcn для выполнения цикла по массиву и возврата соответствующего указателя на функцию.

Подсказка: Используйте цикл foreach.

Ответ c)

d) Соедините все части вместе.

Полная программа:

Показать

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (8 оценок, среднее: 4,75 из 5)
Загрузка...
Подписаться на обновления:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

ПОДПИСЫВАЙТЕСЬ

НА КАНАЛ RAVESLI В TELEGRAM

@ravesli

ПОДПИСАТЬСЯ БЕСПЛАТНО