Ми вже знаємо, що вказівник — це змінна, яка містить адресу іншої змінної. Вказівники на функції аналогічні, за винятком того, що замість звичайних змінних, вони вказують на функції!
Вказівники на функції
Розглянемо наступний фрагмент коду:
1 2 3 4 |
int boo() { return 7; } |
Ідентифікатор boo
— це ім’я функції. Але який її тип? Функції мають свій власний l-value тип. У цьому випадку це тип функції, який повертає цілочисельне значення і не приймає ніяких параметрів. Подібно змінним, функції також мають свої адреси в пам’яті.
Коли функція викликається (за допомогою оператора ()
), точка виконання переходить до адреси функції, що викликається:
1 2 3 4 5 6 7 8 9 10 11 |
int boo() // код функції boo() знаходиться в комірці пам'яті 002B1050 { return 7; } int main() { boo(); // переходимо до адреси 002B1050 return 0; } |
Однією з найпоширеніших помилок початківців є:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> int boo() // код функції boo() знаходиться в комірці пам'яті 002B1050 { return 7; } int main() { std::cout << boo; // ми хочемо викликати boo(), але замість цього ми просто виводимо boo! return 0; } |
Замість виклику функції boo() і виводу значення, що повертається, ми, абсолютно випадково, відправили вказівник на функцію boo() безпосередньо в std::cout. Що станеться в такому випадку?
Результат на моєму комп’ютері:
002B1050
У вас може бути і інше значення, в залежності від того, в який тип даних ваш компілятор вирішить конвертувати вказівник на функцію. Якщо ваш комп’ютер не вивів адресу функції, то ви можете змусити його це зробити, конвертуючи boo
у вказівник типу void і відправляючи його на вивід:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> int boo() // код функції boo() знаходиться в комірці пам'яті 002B1050 { return 7; } int main() { std::cout << reinterpret_cast<void*>(boo); // вказуємо C++ конвертувати функцію boo() у вказівник типу void return 0; } |
Так само, як можна оголосити неконстантний вказівник на звичайну змінну, можна оголосити і неконстантний вказівник на функцію. Синтаксис створення неконстантного вказівника на функцію, мабуть, один з найбільш “потворних” в мові C++:
1 2 |
// fcnPtr - це вказівник на функцію, яка не приймає ніяких аргументів і повертає цілочисельне значення int (*fcnPtr)(); |
У прикладі, наведеному вище, fcnPtr
— це вказівник на функцію, яка не має параметрів і повертає цілочисельне значення. fcnPtr
може вказувати на будь-яку іншу функцію, яка відповідає цьому типу.
Дужки навколо *fcnPtr
необхідні для дотримання пріоритету операцій, в іншому випадку int *fcnPtr()
буде інтерпретуватися як попереднє оголошення функції fcnPtr
, яка не має параметрів і повертає вказівник на цілочисельне значення.
Для створення константного вказівника на функцію використовуйте const після зірочки:
1 |
int (*const fcnPtr)(); |
Якщо ви розмістите const перед int, то це означатиме, що функція, на яку вказує вказівник, повертає const int.
Присвоювання функції вказівника на функцію
Вказівник на функцію може бути ініціалізований функцією (і неконстантному вказівнику на функцію теж можна присвоїти функцію):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
int boo() { return 7; } int doo() { return 8; } int main() { int (*fcnPtr)() = boo; // fcnPtr вказує на функцію boo() fcnPtr = doo; // fcnPtr тепер вказує на функцію doo() return 0; } |
Одна з поширених помилок, яку роблять початківці:
1 |
fcnPtr = doo(); |
Тут ми фактично присвоюємо значення, що повертається з виклику функції doo(), вказівнику fcnPtr
, чого ми не хочемо робити. Ми хочемо, щоб fcnPtr
містив адресу функції doo(), а не значення, що повертається з функції doo(). Тому дужки тут не потрібні.
Зверніть увагу, у вказівника на функцію і самої функції повинні збігатися тип, параметри і тип значення, що повертається. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 |
// Прототипи функцій int boo(); double doo(); int moo(int a); // Присвоювання значень вказівникам на функції int (*fcnPtr1)() = boo; // ок int (*fcnPtr2)() = doo; // не ок: тип вказівника і тип повернення функції не співпадають! double (*fcnPtr4)() = doo; // ок fcnPtr1 = moo; // не ок: fcnPtr1 не має параметрів, але moo() має int (*fcnPtr3)(int) = moo; // ок |
На відміну від фундаментальних типів даних, мова C++ неявно конвертує функцію у вказівник на функцію, якщо це необхідно (тому вам не потрібно використовувати оператор адреси &
для отримання адреси функції). Однак, мова C++ НЕ буде неявно конвертувати вказівник на функцію у вказівник типу void або навпаки.
Виклик функції через вказівник на функцію
Ви також можете використати вказівник на функцію для виклику самої функції. Є два способи зробити це. Перший — через явне розіменування:
1 2 3 4 5 6 7 8 9 10 11 12 |
int boo(int a) { return a; } int main() { int (*fcnPtr)(int) = boo; // присвоюємо функцію boo() вказівнику fcnPtr (*fcnPtr)(7); // викликаємо функцію boo(7), використовуючи fcnPtr return 0; } |
Другий — через неявне розіменування:
1 2 3 4 5 6 7 8 9 10 11 12 |
int boo(int a) { return a; } int main() { int (*fcnPtr)(int) = boo; // присвоюємо функцію boo() вказівнику fcnPtr fcnPtr(7); // викликаємо функцію boo(7), використовуючи fcnPtr return 0; } |
Як ви можете бачити, спосіб неявного розіменування виглядає так само, як і звичайний виклик функції, так як звичайні імена функцій є вказівниками на функції!
Примітка: Параметри за замовчуванням не працюватимуть з функціями, викликаними через вказівники на функції. Параметри за замовчуванням обробляються під час компіляції (тобто вам потрібно надати аргумент для параметра за замовчуванням під час компіляції). Однак вказівники на функції обробляються під час виконання. Отже, параметри за замовчуванням не можуть оброблятися при виконанні функції через вказівник на функцію. В цьому випадку вам потрібно буде явно передати значення для параметрів за замовчуванням.
Передача функцій в якості аргументів іншим функціям
Одна з найбільш корисних речей, яку ви можете зробити з вказівниками на функції — це передати функцію в якості аргументу іншій функції. Функції, які використовуються в якості аргументів для інших функцій, називаються функціями зворотного виклику.
Припустимо, що ви пишете функцію для виконання певного завдання (наприклад, сортування масиву), але ви хочете, щоб користувач міг визначити, яким чином виконувати це сортування (наприклад, по зростанню чи по спаданню). Розглянемо детально цей випадок.
Всі алгоритми сортування працюють за однаковою схемою: алгоритм виконує ітерацію по списку чисел, порівнює пари чисел і міняє їх місцями, виходячи з результатів цих порівнянь. Отже, змінюючи алгоритм порівняння чисел, ми можемо змінити спосіб сортування, не зачіпаючи при цьому інші частини коду.
Ось наше сортування методом вибору, розглянуте на відповідному уроці:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <algorithm> // для std::swap() (використовуйте <utility>, якщо підтримується C++11) void SelectionSort(int *array, int size) { // Перебираємо кожний елемент масиву for (int startIndex = 0; startIndex < size; ++startIndex) { // smallestIndex - це індекс найменшого елементу, який ми виявили до цього моменту int smallestIndex = startIndex; // Шукаємо найменший елемент серед решти елементів масиву (починаємо з startIndex+1) for (int currentIndex = startIndex + 1; currentIndex < size; ++currentIndex) { // Якщо поточний елемент менше нашого попереднього знайденого найменшого елементу, if (array[smallestIndex] > array[currentIndex]) // ПОРІВНЯННЯ ВИКОНУЄТЬСЯ ТУТ // то це наш новий найменший елемент в цій ітерації smallestIndex = currentIndex; } // Міняємо місцями наш стартовий елемент зі знайденим найменшим елементом std::swap(array[startIndex], array[smallestIndex]); } } |
Давайте замінимо порівняння чисел на функцію порівняння. Оскільки наша функція порівняння порівнюватиме два цілих числа і повертатиме логічне значення для вказівки того, чи слід виконувати заміну, вона виглядатиме наступним чином:
1 2 3 4 |
bool ascending(int a, int b) { return a > b; // умова, при якій міняються місцями елементи масиву } |
А ось уже сортування методом вибору з функцією ascending() для порівняння чисел:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <algorithm> // для std::swap() (використовуйте <utility>, якщо підтримується C++11) void SelectionSort(int *array, int size) { // Перебираємо кожний елемент масиву for (int startIndex = 0; startIndex < size; ++startIndex) { // smallestIndex - це індекс найменшого елементу, який ми виявили до цього моменту int smallestIndex = startIndex; // Шукаємо найменший елемент серед решти елементів масиву (починаємо з startIndex+1) for (int currentIndex = startIndex + 1; currentIndex < size; ++currentIndex) { // Якщо поточний елемент менше нашого попереднього знайденого найменшого елементу, if (ascending(array[smallestIndex], array[currentIndex])) // ПОРІВНЯННЯ ВИКОНУЄТЬСЯ ТУТ // то це наш новий найменший елемент в цій ітерації smallestIndex = currentIndex; } // Міняємо місцями наш стартовий елемент зі знайденим найменшим елементом std::swap(array[startIndex], array[smallestIndex]); } } |
Тепер, щоб дозволити caller-у вирішити, яким чином виконуватиметься сортування, замість використання нашої функції порівняння, ми дозволяємо caller-у надати свою власну функцію порівняння! Це робиться за допомогою вказівника на функцію.
Оскільки функція порівняння caller-а порівнюватиме два цілих числа і повертатиме логічне значення, то вказівник на цю функцію виглядатиме наступним чином:
1 |
bool (*comparisonFcn)(int, int); |
Ми дозволяємо caller-у передавати спосіб сортування масиву за допомогою вказівника на функцію в якості третього параметру в нашу функцію сортування.
Ось готовий код сортування методом вибору з вибором способу сортування в caller-і (тобто в функції main()):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
#include <iostream> #include <algorithm> // для std::swap() (використовуйте <utility>, якщо підтримується C++11) // Зверніть увагу, третім параметром є користувацький вибір виконання сортування void selectionSort(int *array, int size, bool (*comparisonFcn)(int, int)) { // Перебираємо кожний елемент масиву for (int startIndex = 0; startIndex < size; ++startIndex) { // bestIndex - це індекс найменшого/найбільшого елементу, який ми виявили до цього моменту int bestIndex = startIndex; // Шукаємо найменший/найбільший елемент серед решти елементів масиву (починаємо з startIndex+1) for (int currentIndex = startIndex + 1; currentIndex < size; ++currentIndex) { // Якщо поточний елемент менше/більше нашого попереднього знайденого найменшого/найбільшого елементу, if (comparisonFcn(array[bestIndex], array[currentIndex])) // ПОРІВНЯННЯ ВИКОНУЄТЬСЯ ТУТ // то це наш новий найменший/найбільший елемент в цій ітерації bestIndex = currentIndex; } // Міняємо місцями наш стартовий елемент зі знайденим найменшим/найбільшим елементом std::swap(array[startIndex], array[bestIndex]); } } // Ось функція порівняння, яка виконує сортування в порядку зростання (зверніть увагу, це та ж функція ascending(), що у прикладі, наведеному вище) bool ascending(int a, int b) { return a > b; // міняємо місцями, якщо перший елемент більше другого } // Ось функція порівняння, яка виконує сортування в порядку спадання bool descending(int a, int b) { return a < b; // міняємо місцями, якщо другий елемент більше першого } // Ця функція виводить значення масиву void printArray(int *array, int size) { for (int index=0; index < size; ++index) std::cout << array[index] << " "; std::cout << '\n'; } int main() { int array[8] = { 4, 8, 5, 6, 2, 3, 1, 7 }; // Сортуємо масив в порядку спадання, використовуючи функцію descending() selectionSort(array, 8, descending); printArray(array, 8); // Сортуємо масив в порядку зростання, використовуючи функцію ascending() selectionSort(array, 8, ascending); printArray(array, 8); return 0; } |
Результат виконання програми:
8 7 6 5 4 3 2 1
1 2 3 4 5 6 7 8
Прикольно, правда? Ми надали caller-у можливість контролювати процес сортування чисел (caller може визначити і будь-які інші функції порівняння):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
bool evensFirst(int a, int b) { // Якщо a - парне число, а b - непарне число, то a йде першим (ніякої перестановки не відбувається) if ((a % 2 == 0) && !(b % 2 == 0)) return false; // Якщо a - непарне число, а b - парне число, то b йде першим (тут вже потрібна перестановка) if (!(a % 2 == 0) && (b % 2 == 0)) return true; // В іншому випадку, сортуємо в порядку зростання return ascending(a, b); } int main() { int array[8] = { 4, 8, 6, 3, 1, 2, 5, 7 }; selectionSort(array, 8, evensFirst); printArray(array, 8); return 0; } |
Результат виконання програми:
2 4 6 8 1 3 5 7
Як ви можете бачити, використання вказівника на функцію дозволяє caller-у «підключити» свій власний функціонал до чогось, що ми писали і тестували раніше, що сприяє повторному використанню коду! Раніше, якщо ви хотіли відсортувати один масив в порядку спадання, а інший — в порядку зростання, вам знадобилося б написати кілька версій сортування масиву. Тепер же у вас може бути одна версія, яка виконуватиме сортування будь-яким способом, яким ви тільки захочете!
Параметри за замовчуванням у функціях
Якщо ви дозволите caller-у передавати функцію в якості параметру, то корисним буде надати і деякі стандартні функції для зручності caller-а. Наприклад, у вищенаведеному прикладі з сортуванням методом вибору, було б простіше встановити дефолтний (за замовчуванням) спосіб порівняння чисел. Наприклад:
1 2 |
// Сортування за замовчуванням виконується в порядку зростання void selectionSort(int *array, int size, bool (*comparisonFcn)(int, int) = ascending); |
У цьому випадку, до тих пір, поки користувач викликає selectionSort() як зазвичай (а не через вказівник на функцію), параметр comparisonFcn
за замовчуванням відповідатиме функції ascending().
Вказівники на функції і псевдоніми типів
Подивимося правді в очі — синтаксис вказівників на функції потворний. Проте, за допомогою typedef ми можемо виправити цю ситуацію:
1 |
typedef bool (*validateFcn)(int, int); |
Тут ми визначили псевдонім типу під назвою validateFcn
, який є вказівником на функцію, яка приймає два значення типу int і повертає значення типу bool.
Тепер замість написання наступного:
1 |
bool validate(int a, int b, bool (*fcnPtr)(int, int)); // фу, який синтаксис |
Ми можемо написати наступне:
1 |
bool validate(int a, int b, validateFcn pfcn) // ось це інша справа |
Так набагато краще, правда? Однак синтаксис визначення самого typedef може бути трохи важким для запам’ятовування. У C++11 замість typedef ви можете використовувати type alias для створення псевдоніма типу вказівника на функцію:
1 |
using validateFcn = bool(*)(int, int); // type alias |
Це вже читабельніше, ніж з typedef, так як ім’я псевдонім і його визначення розташовані на протилежних сторонах від оператора =
.
Використання type alias ідентичне використанню typedef:
1 |
bool validate(int a, int b, validateFcn pfcn) // круто, правда? |
Використання std::function в C++11
У C++11 ввели альтернативний спосіб визначення і зберігання вказівників на функції, який виконується з використанням std::function. std::function є частиною заголовку functional Стандартної бібліотеки C++. Для визначення вказівника на функцію за допомогою цього способу вам потрібно оголосити об’єкт std::function наступним чином:
1 2 3 |
#include <functional> bool validate(int a, int b, std::function<bool(int, int)> fcn); // вказуємо вказівник на функцію за допомогою std::function, який повертає bool і приймає два int-а |
Як ви можете бачити, тип повернення і параметри знаходяться в кутових дужках, а параметри ще і всередині круглих дужок. Якщо параметрів немає, то внутрішні дужки можна залишити порожніми. Тут вже зрозуміліше, який тип значення, що повертається, і які очікувані параметри функції.
Оновимо наш попередній приклад з розділу «Присвоювання функції вказівника на функцію» поточного уроку, але вже з використанням std::function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> #include <functional> int boo() { return 7; } int doo() { return 8; } int main() { std::function<int()> fcnPtr; // оголошуємо вказівник на функцію, який повертає int і не приймає ніяких параметрів fcnPtr = doo; // fcnPtr тепер вказує на функцію doo() std::cout << fcnPtr(); // викликаємо функцію як зазвичай return 0; } |
Висновки
Вказівники на функції корисні, перш за все, коли ви хочете зберігати функції в масиві (або в структурі) або коли вам потрібно передати одну функцію в якості аргументу іншій функції. Оскільки синтаксис оголошення вказівників на функції є трохи потворним і вразливим до помилок, то рекомендується використовувати type alias (або std::function в C++11).
Тест
Завдання №1
Цього разу ми спробуємо написати версію базового калькулятора за допомогою вказівників на функції.
a) Напишіть коротку програму, яка просить користувача ввести два цілих числа і вибрати математичну операцію: +
, -
, *
або /
. Переконайтеся, що користувач ввів коректний символ математичної операції (виконайте перевірку).
Відповідь 1.a)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <iostream> int getInteger() { std::cout << "Enter an integer: "; int a; std::cin >> a; return a; } char getOperation() { char op; do { std::cout << "Enter an operation ('+', '-', '*', '/'): "; std::cin >> op; } while (op!='+' && op!='-' && op!='*' && op!='/'); return op; } int main() { int a = getInteger(); char op = getOperation(); int b = getInteger(); return 0; } |
b) Напишіть функції add(), subtract(), multiply() і divide(). Вони повинні приймати два цілочисельних параметри і повертати цілочисельне значення.
Відповідь 1.b)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } int multiply(int a, int b) { return a * b; } int divide(int a, int b) { return a / b; } |
c) Створіть typedef з ім’ям arithmeticFcn
для вказівника на функцію, яка приймає два цілочисельних параметри і повертає цілочисельне значення.
Відповідь 1.c)
1 |
typedef int (*arithmeticFcn)(int, int); |
d) Напишіть функцію з ім’ям getArithmeticFcn(), яка приймає символ обраного математичного оператора і повертає відповідну функцію в якості вказівника на функцію.
Відповідь 1.d)
1 2 3 4 5 6 7 8 9 10 11 |
arithmeticFcn getArithmeticFcn(char op) { switch (op) { default: // функцією за замовчуванням буде add() case '+': return add; case '-': return subtract; case '*': return multiply; case '/': return divide; } } |
e) Додайте в функцію main() виклик функції getArithmeticFcn().
Відповідь 1.e)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> int main() { int a = getInteger(); char op = getOperation(); int b = getInteger(); arithmeticFcn fcn = getArithmeticFcn(op); std::cout << a << ' ' << op << ' ' << b << " = " << fcn(a, b) << '\n'; return 0; } |
f) З’єднайте всі частини разом.
Повна програма
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
#include <iostream> int getInteger() { std::cout << "Enter an integer: "; int a; std::cin >> a; return a; } char getOperation() { char op; do { std::cout << "Enter an operation ('+', '-', '*', '/'): "; std::cin >> op; } while (op!='+' && op!='-' && op!='*' && op!='/'); return op; } int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } int multiply(int a, int b) { return a * b; } int divide(int a, int b) { return a / b; } typedef int (*arithmeticFcn)(int, int); arithmeticFcn getArithmeticFcn(char op) { switch (op) { default: // функцією за замовчуванням буде add() case '+': return add; case '-': return subtract; case '*': return multiply; case '/': return divide; } } int main() { int a = getInteger(); char op = getOperation(); int b = getInteger(); arithmeticFcn fcn = getArithmeticFcn(op); std::cout << a << ' ' << op << ' ' << b << " = " << fcn(a, b) << '\n'; return 0; } |
Завдання №2
Тепер давайте змінимо програму, яку ми написали в першому завданні, щоб перемістити логіку з getArithmeticFcn в масив.
a) Створіть структуру з ім’ям arithmeticStruct
, яка має два члени: математичний оператор типу char і вказівник на функцію arithmeticFcn
.
Відповідь 2.a)
1 2 3 4 5 |
struct arithmeticStruct { char op; arithmeticFcn fcn; }; |
b) Створіть статичний глобальний масив arithmeticArray
, використовуючи структуру arithmeticStruct
, який буде ініціалізовано кожною з 4 математичних операцій.
Відповідь 2.b)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Версія до C++11: static arithmeticStruct arithmeticArray[] = { { '+', add }, { '-', subtract }, { '*', multiply }, { '/', divide } }; // Версія C++11 з використанням uniform-ініціалізації static arithmeticStruct arithmeticArray[] { { '+', add }, { '-', subtract }, { '*', multiply }, { '/', divide } }; |
c) Змініть getArithmeticFcn
для виконання циклу по масиву і повернення відповідного вказівника на функцію.
Підказка: Використовуйте цикл foreach.
Відповідь 2.c)
1 2 3 4 5 6 7 8 9 10 |
arithmeticFcn getArithmeticFcn(char op) { for (auto &arith : arithmeticArray) { if (arith.op == op) return arith.fcn; } return add; // функцією за замовчуванням буде add() } |
d) З’єднайте всі частини разом.
Повна програма
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
#include <iostream> int getInteger() { std::cout << "Enter an integer: "; int a; std::cin >> a; return a; } char getOperation() { char op; do { std::cout << "Enter an operation ('+', '-', '*', '/'): "; std::cin >> op; } while (op != '+' && op != '-' && op != '*' && op != '/'); return op; } int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } int multiply(int a, int b) { return a * b; } int divide(int a, int b) { return a / b; } typedef int(*arithmeticFcn)(int, int); struct arithmeticStruct { char op; arithmeticFcn fcn; }; static arithmeticStruct arithmeticArray[] { { '+', add }, { '-', subtract }, { '*', multiply }, { '/', divide } }; arithmeticFcn getArithmeticFcn(char op) { for (auto &arith : arithmeticArray) { if (arith.op == op) return arith.fcn; } return add; // функцією за замовчуванням буде add() } int main() { int a = getInteger(); char op = getOperation(); int b = getInteger(); arithmeticFcn fcn = getArithmeticFcn(op); std::cout << a << ' ' << op << ' ' << b << " = " << fcn(a, b) << '\n'; return 0; } |