Урок №110. Вказівники на функції

  Юрій  | 

  Оновл. 19 Січ 2021  | 

 286

Ми вже знаємо, що вказівник — це змінна, яка містить адресу іншої змінної. Вказівники на функції аналогічні, за винятком того, що замість звичайних змінних, вони вказують на функції!

Вказівники на функції

Розглянемо наступний фрагмент коду:

Ідентифікатор 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().

Вказівники на функції і псевдоніми типів

Подивимося правді в очі — синтаксис вказівників на функції потворний. Проте, за допомогою 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 наступним чином:

Як ви можете бачити, тип повернення і параметри знаходяться в кутових дужках, а параметри ще і всередині круглих дужок. Якщо параметрів немає, то внутрішні дужки можна залишити порожніми. Тут вже зрозуміліше, який тип значення, що повертається, і які очікувані параметри функції.

Оновимо наш попередній приклад з розділу «Присвоювання функції вказівника на функцію» поточного уроку, але вже з використанням 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

Тепер давайте змінимо програму, яку ми написали в першому завданні, щоб перемістити логіку з getArithmeticFcn в масив.

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

Відповідь 2.a)

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

Відповідь 2.b)

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

Підказка: Використовуйте цикл foreach.

Відповідь 2.c)

d) З’єднайте всі частини разом.

Повна програма

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (1 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

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