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

  Юрий  | 

  |

  Обновл. 13 Сен 2021  | 

 136800

 ǀ   43 

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

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

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

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

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

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

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

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

002B1050

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

А вот уже сортировка методом выбора с функцией 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().

Указатели на функции и псевдонимы типов


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

Здесь мы определили псевдоним типа под названием 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тандартной библиотеки C++. Для определения указателя на функцию с помощью этого способа вам нужно объявить объект std::function следующим образом:

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

Обновим наш предыдущий пример из раздела «Присваивание функции указателю на функцию» текущего урока, но уже с использованием std::function:

Заключение

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

Тест

Задание №1

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

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

Ответ 1.a)

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

Ответ 1.b)

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

Ответ 1.c)

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

Ответ 1.d)

e) Добавьте в функцию main() вызов функции getArithmeticFcn().

Ответ 1.e)

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

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

Задание №2

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

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

Ответ 2.a)

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

Ответ 2.b)

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

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

Ответ 2.c)

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

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

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

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (291 оценок, среднее: 4,81 из 5)
Загрузка...

Комментариев: 43

  1. Артем:

    Всем здравствуйте!
    Первый раз так погряз, помогите, пожалуйста. Не могу понять почему работает (пр.1):

    Но реализуя через присваивание в глобальной области видимости следующим образом, ругается VS (пр.2):

    А также, если вдруг знаете — подскажите: почему синтаксис, указанный в (пр.1) не работает с std::array?  Заранее спасибо!

  2. Артём:

    Добрый день, друзья! Появился вопрос после выполнения теста 2b, может кто-то знает: почему при инициализации массива arithmeticArray с помощью std::array требуется вложенность двух пар фигурных скобок?

  3. Алекс:

    С typedef следующая функция записывается так:

    Как записать эту же функцию без typedef?

    Так не пропускает:

    1. Павел:

  4. An:

    Почему здесь && работает как | | ?

    1. Ольга:

      Я тоже было затупила, потом просто прочитала:
      "До тех пор пока: op НЕ равно + И op НЕ равно — И ор НЕ равно * И ор НЕ равно /". И стало понятно. Иными словами оператор while пробегается по всем этим условиям, и если видит, что op не удовлетворяет ни одному из условий, т.е. И ни тому, И не этому, И не последнему, цикл повторяется. Иначе завершается.

  5. Вячеслав:

    Подскажите а возможно сделать хитрость и считывать название функции с текстового файла или со строки?

    так как foo — адрес функции ничего не работает

  6. Алексей:

    Здравствуйте уважаемые пользователи и автор. Никак не могу понять что происходит в этих строках:

    Получается мы объявили указатель на функцию типа int псевдонимом typedef, который принимает 2 целочисленных параметра. Как я понял:

    arithmeticFcn getArithmeticFcn(char op) этот указатель указывает на функции add, subtract,multiply,devide, в зависимости от того какой case (оператор) передастся в функцию getArithmeticFcn(char op).

    Но вот эта запись: arithmeticFcn fcn = getArithmeticFcn(op); не понятна, т.к. указателю можно вроде присвоить только тот тип функции, которым он объявлен. Соответственно не могу понять как правильно записать места использования указателя на функцию без typedef.

    Также не ясна эта запись arithmeticFcn getArithmeticFcn(char op). Уважаемый автор или участники чата, если сможете пояснить, то прошу поясните.

    1. Юрий:

      Воспринимай это, как тип данных, который является указателем на функцию, которая принимает два значения типа int и возвращает значение типа int.(есть int, есть char и есть arithmeticFcn)

      Здесь функция, которая принимает переменнную типо char и возвращает переменную типо arithmeticFcn, который является указателем на функцию, которая принимает два значения типа int и возвращает значение типа int.

      Здесь мы создаем и инициализируем копированием переменную типа arithmeticFcn, функцией возвращаемым значением типа arithmeticFcn, который является указателем на функцию, которая принимает два значения типа int и возвращает значение типа int.

      1. Алексей:

        Юрий спасибо за пояснения, есть понимание. Доходит к сожалению не с первого раза.

      2. Павел:

        Без typedef наверно понятнее будет.

  7. Данте:

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

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

    Пропущено название псевдонима после "bool (*validateFcn)(int, int);"?

    1. Фото аватара Юрий:

      Здесь всё ок, ничего не пропущено. Мы же определили псевдоним типа, а идентификатор псевдонима типа (в данном случае pfcn) используется уже непосредственно в коде при применении псевдонима типа.

  8. Ironsaid:

    Худо-бедно что-то написал. Естественно подсматривал и изучал почему так. Критикуйте, предлагайте.

    1. Pivan:

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

  9. Alex:

    Не понял. А зачем делать массив static? Для того чтобы область видимости ограничивалась только этим файлом?

    1. Артурка:

      Ну в данном случае static несет в себе именно этот посыл. Просто для того что бы другие юниты трансляции не имели доступа к этой переменной. С таким же успухом можно его объявить спецификатором const вместо static.

  10. Bayrmaa:

    Написать функцию, которая считает количество слов в переданной строке. Слова отделены друг от друга пробелами и знаками препинания (.,!?-). Учтите, что эти знаки могут идти друг за другом. К примеру, «Привет … Мир!» – здесь 2 слова.

    1. someone:

      Не знаю нужен ли тебе ещё код по твоей задачке (скорее всего нет), но вот мой, вроде как рабочий, прототип:

  11. Даниил:

    Объясните пожалуйста про псевдоним в тесте.(1c) тут просто не недопонимания у меня

  12. Peter:

    Где я провтыкал с указателем? Подскажите..

    1. Andrew Gulenko:

      Все нормально с указателями. Переименуй функции minus() и plus(). Потому что такие названия для функций, являются неоднозначными в некоторых компиляторах.

  13. Cv287:

    Спасибо огромное за ваш непомерно большой труд! Ваша коллекция статей — это один из лучших русскоязычных ресурсов для изучения С++. Все очень удобно и доступно, UX у меня сложился очень хороший, еще раз, большое спасибо!

    1. Фото аватара Юрий:

      Очень приятно, что Равесли вам помог, за UX отдельное спасибо))

  14. Kinonik:

    Как такое величественное в книге за час изучить? XD
    Огромное спасибо автору за все уроки, они просто прекрасны!

    1. Фото аватара Юрий:

      Пожалуйста))

  15. Ya:

    Чем дальше в лес, тем злее волки 🙁 В голове каша от всех этих указателей, ссылок, указателей на ссылки, указателей на указатели

    1. Алексей:

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

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

      Вот, далее мы инициализируем функции на математические вычисления. Называем соответственно.

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

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

      После этого инициализация массива через эту структуру. Определенному оператору, определенная функция.

      Тут самое интересное — передача данных другой функции.

      Тут foreach, в котором идет объявление ссылки на переменную, для быстродействия, которая принимает каждое значение массива и проверяется ввод — найден, держи функцию.

      В main() идет выполнения нашего ввода и передача данных с действий описанных выше и передача в fcn.
      fcn(a, b) просто принимает ввод и идет вычисление.

      1. Lore:

        Почему мы во втором задании, когда возвращаем при нахождении оп
        arith.fcn;?
        допустим arith сразу нашёл оп (op) под строкой 0 (т.е +) и вот он сравнил и оказалось что ввели и в правду + и он возвращает return arith.fcn;
        и на что он указывает и что он означает ?

        1. Ростислав:

          Он указывает как раз таки на функцию add()

  16. deb:

    Спасибо за содержательный урок! Особенно понравились typedef, <functional> и alias

    1. Фото аватара Юрий:

      Пожалуйста)

      1. Давид:

        Юрий ты красава, уважаю от души

  17. Александр:

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

    Я бы советовал эту строку убрать или скорректировать. Это ведь неправда 🙂
    По такой схеме работают сортировки, основанные на сравнении и обмене. Есть сортировки, основанные на сравнении, но в которых нет обменов. Есть сортировки, НЕ основанные на сравнении.

    Например, сортировка слиянием — сравнения есть, обменов нет
    некоторые реализации "быстрой сортировки" — нет сравнения между элементами (хотя есть сравнения в принципе), обменов может не быть
    Сортировка подсчетом (а так же корзинная и радикс) — нет сравнений вообще
    Сортировка вставками — нет сравнений между элементами (но есть сравнения вообще), нет обменов

  18. kmish:

    А я не про копировать-вставить говорю, а про синтаксис. Причем тут думать, если синтаксис не давался? Как я могу пониманием придти вот к этому?:

    Это должно было быть прокомментировано в уроке о массивах или структурах.
    Это не камень в огород, а мои замечания. После просмотра ответов я это понял синтаксис, но его нельзя получить "пониманием". Синтаксис — это правила, а не выводы. А правила должны быть описаны.

    1. Михаил:

      Вполне возможно, что этот синтаксис и не разбирался в данном пособии. Хотя не факт, я не проверял просто. Но кто мешает Вам написать 8 строк и присвоить эти значения по отдельности каждому полю? Потом посмотреть в ответ и уяснить для себя, что возможен и такой синтаксис?
      Нет в мире ничего идеального и данное пособие, скорее всего, тоже страдает огрехами. Хотя более полного БЕСПЛАТНОГО труда я не видел! И огромное спасибо Юрию за его бескорыстное вложение сил и средств!
      Но, что самое смешное, Вы пытаетесь предъявить претензии переводчику, а не автору!

    2. Павел:

      Нашел этот синтаксис в уроке 78. Многомерные массивы.  Инициализация двумерных массивов.

      Получается каждый член структуры — это столбец, а количество математических операций — строки.

      Нужно подсказку давать.

  19. Владимир:

  20. Anastasia:

    Как в данном задании записать массив через std::array? Т.к. ругается, что "слишком много инициализаторов" (с 3-й строчки).

    1. Алексей:

  21. Petr:

    А зачем в строке (62) "for (auto &arith : arithmeticArray)" использовать "&" ведь и без него будет работать "for (auto arith : arithmeticArray)".

    1. Petr:

      Забыл добавить, что при таком объёме данных ссылку нет смысла использовать. там нет таких затрат память, что бы ссылка хоть как то помогала.

      1. Алексей:

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

        Скажем пользователь и пароль для системы, а это уже тысячи, забыл поставить, ибо в таких маленьких не ставил и пиши пропало.

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

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