Як ми вже знаємо з попередніх уроків, виконання програми в мові C++ відбувається послідовно, рядок за рядком, починаючи з функції main(). Коли компілятор зустрічає виклик функції, то точка виконання переходить до початку коду функції, що викликається. Звідки компілятор знає, що це потрібно зробити?
При компіляції програми компілятор конвертує кожен стейтмент програми в один або кілька рядків машинного коду. Кожному рядку машинного коду присвоюється власна унікальна адреса. Так само і з функціями: коли компілятор зустрічає функцію, вона конвертується в машинний код і отримує свою адресу.
Зв’язування — це процес, який використовується для конвертації ідентифікаторів (таких як імена змінних або функцій) в адреси. Хоча зв’язування використовується як для змінних, так і для функцій, на цьому уроці ми зосередимося тільки на функціях.
Раннє зв’язування
Більшість викликів функцій, які зустрічає компілятор, є прямими викликами функцій. Прямий виклик функції — це стейтмент, який напряму викликає функцію. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <iostream> void printValue(int value) { std::cout << value; } int main() { printValue(7); // це прямий виклик функції 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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
#include <iostream> 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 main() { int a; std::cout << "Enter a number: "; std::cin >> a; int b; std::cout << "Enter another number: "; std::cin >> b; int op; do { std::cout << "Enter an operation (0 = add, 1 = subtract, 2 = multiply): "; std::cin >> op; } while (op < 0 || op > 2); int result = 0; switch (op) { // Викликаємо конкретну функцію напряму. Використовується раннє зв'язування case 0: result = add(a, b); break; case 1: result = subtract(a, b); break; case 2: result = multiply(a, b); break; } std::cout << "The answer is: " << result << std::endl; return 0; } |
Оскільки add(a, b)
, subtract(a, b)
і multiply(a, b)
є прямими викликами функцій, то компілятор використовуватиме раннє зв’язування. Він замінить виклик add(a, b)
інструкцією, яка повідомить процесору перейти до адреси add(). Те ж саме виконається і для subtract(a, b)
, і для multiply(a, b)
.
Пізнє зв’язування
У деяких програмах неможливо знати наперед, яка функція викликатиметься першою. В такому випадку використовується пізнє зв’язування (або «динамічна прив’язка»). У мові C++ для виконання пізнього зв’язування використовуються вказівники на функції. Якщо коротко, вказівник на функцію — це тип вказівника, який вказує на функцію замість змінної. Функція, на яку вказує вказівник, може викликатися через вказівник і оператор виклику функції. Наприклад, викличемо функцію add():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> int add(int a, int b) { return a + b; } int main() { // Створюємо вказівник на функцію add int (*pFcn)(int, int) = add; std::cout << pFcn(4, 5) << std::endl; // виклик add(4 + 5) 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 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 |
#include <iostream> 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 main() { int a; std::cout << "Enter a number: "; std::cin >> a; int b; std::cout << "Enter another number: "; std::cin >> b; int op; do { std::cout << "Enter an operation (0 = add, 1 = subtract, 2 = multiply): "; std::cin >> op; } while (op < 0 || op > 2); // Створюємо вказівник на функцію з іменем pFcn (погоджуюся, синтаксис жахливий) int (*pFcn)(int, int) = nullptr; // Вказуємо pFcn вказувати на функцію, яку вибере користувач switch (op) { case 0: pFcn = add; break; case 1: pFcn = subtract; break; case 2: pFcn = multiply; break; } // Викликаємо функцію, на яку вказує pFcn з параметрами a і b. // Використовується пізнє зв'язування std::cout << "The answer is: " << pFcn(a, b) << std::endl; return 0; } |
Тут ми вказуємо pFcn
вказувати на функцію, яку вибере користувач. Потім через вказівник ми викликаємо функцію, яку вибрав користувач. Компілятор не може використовувати раннє зв’язування для виконання виклику функції pFcn(a, b)
, так як він не може наперед визначити, на яку функцію вказуватиме pFcn
!
Пізнє зв’язування менш ефективне, тому що присутній “посередник” між процесором і функцією. З раннім зв’язуванням процесор може перейти безпосередньо до адреси функції. З пізнім зв’язуванням процесор повинен прочитати адресу, яка зберігається у вказівнику, а тільки потім перейти до цієї адреси. Цей додатковий крок і уповільнює весь процес. Однак перевага пізнього зв’язування полягає в тому, що воно більш гнучке, ніж раннє зв’язування, тому що не потрібно вирішувати, яку функцію слід викликати до запуску самої програми.
На наступному уроці ми розглянемо, як пізнє зв’язування використовується для реалізації віртуальних функцій.