На цьому уроці ми розглянемо, що таке попереднє оголошення і прототип функції в мові С++.
Наявність проблеми
Подивіться на цей, здавалося б, невинний шматочок коду під назвою add.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <iostream> int main() { std::cout << "The sum of 3 and 4 is: " << add(3, 4) << std::endl; return 0; } int add(int x, int y) { return x + y; } |
Ви, напевно, очікуєте побачити приблизно наступний результат:
The sum of 3 and 4 is: 7
Але по факту ця програма навіть не скомпілюється. Причиною цьому є те, що компілятор читає код послідовно. Коли він зустрічає виклик функції add() в рядку №5 функції main(), то він навіть не знає, що таке add(), тому що ми цю функцію ще не визначили! Через це ми отримаємо наступну помилку:
add: идентификатор не найден
Щоб усунути цю проблему, ми повинні врахувати той факт, що компілятор не знає, що таке add(). Є два шляхи вирішення даної проблеми.
Рішення №1: Розмістити визначення функції add() вище її виклику (тобто, перед функцією main()):
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <iostream> int add(int x, int y) { return x + y; } int main() { std::cout << "The sum of 3 and 4 is: " << add(3, 4) << std::endl; return 0; } |
Таким чином, при виконанні функції add() у функції main(), компілятор знатиме, що це таке. Оскільки це простенька програма, то внести подібні зміни нескладно. Однак в більших програмах, де коду набагато більше, це може бути не так вже й легко — дізнаватися хто кого викликає і в якому порядку, щоб дотриматися правильної послідовності.
Крім того, цей варіант не завжди можливий. Наприклад, ми пишемо програму, яка має дві функції: А()
і В()
. Якщо функція А()
викликає функцію В()
, а функція В()
викликає функцію А()
, то немає ніякого способу впорядкувати ці функції таким чином, щоб всі були щасливі. Якщо ви оголосите спочатку А()
, то компілятор скаржитиметься, що не знає, що таке В()
. Якщо ви оголосите спочатку В()
, то компілятор скаржитиметься, що не знає, що таке А()
.
Прототипи функцій і попереднє оголошення
Рішення №2: Використовувати попереднє оголошення.
Попереднє оголошення повідомляє компілятору про існування ідентифікатора ДО його фактичного визначення.
У випадку з функціями, ми можемо повідомити компілятор про існування функції до її фактичного визначення. Для цього нам потрібно використати прототип цієї функції. Прототип функції складається з типу повернення функції, її імені та параметрів. Основна частина (між фігурними дужками) пропускається. А оскільки прототип функції є стейтментом, то він також закінчується крапкою з комою.
Ось прототип функції add():
1 |
int add(int x, int y); // прототип функції складається з типу повернення функції, її імені, параметрів і крапки з комою |
А ось вищенаведена програма, але вже з прототипом функції в якості попереднього оголошення аdd():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int add(int x, int y); // попереднє оголошення функції add() (використовується її прототип) int main() { std::cout << "The sum of 3 and 4 is: " << add(3, 4) << std::endl; // це працює через те, що ми попередньо оголосили функцію add() вище return 0; } int add(int x, int y) // хоча її визначення знаходиться нижче її виклику { return x + y; } |
Тепер, коли компілятор зустрічає виклик функції add() в функції main(), він знає, що це таке і де його шукати.
Варто відзначити, що в прототипах функцій можна і не вказувати імена параметрів. Наприклад, вищенаведений прототип ми можемо записати наступним чином:
1 |
int add(int, int); |
Проте краще вказувати імена параметрів, щоб не плутатися зайвий раз.
Лайфхак: Прототипи функцій можна легко створювати за допомогою копіювання/вставки з фактичного визначення функції. Просто не забувайте вказувати крапку з комою в кінці.
Попередньо оголосили, але не визначили
Питання: «А що буде, якщо ми попередньо оголосимо функцію, але не запишемо її визначення?”. Однозначної відповіді немає. Якщо попереднє оголошення записано, але функція ніколи не викликається, то програма може запуститися без помилок. Однак, якщо попереднє оголошення записано, функція викликається, але її визначення немає, то ви отримаєте помилку на етапі лінкінгу: програма просто не зможе опрацювати виклик цієї функції.
Розглянемо наступну програму:
1 2 3 4 5 6 7 8 9 |
#include <iostream> int add(int x, int y); // попереднє оголошення функції add() (використовується її прототип) int main() { std::cout << "The sum of 3 and 4 is: " << add(3, 4) << std::endl; return 0; } |
У цій програмі ми попередньо оголосили функцію add(), викликали її в функції main(), але не записали її визначення. При спробі компіляції цієї програми ми отримаємо помилку від лінкера.
Оголошення vs. Визначення
У мові C++ ви часто будете чути слова “оголошення” і “визначення”. Що це таке?
Визначення фактично реалізує (спричиняє виділення пам’яті) ідентифікатор. Ось приклади визначень:
1 2 3 4 5 6 |
int add(int x, int y) // визначаємо функцію add() { int z = x + y; // визначаємо змінну z return z; } |
Визначення необхідно, щоб лінкер не мав до нас ніяких запитань. Якщо ви використовуєте ідентифікатор без його визначення, то лінкер видасть вам помилку.
У мові C++ є правило “одного визначення”, яке складається з наступних трьох частин:
Всередині файлу: функція, об’єкт, тип чи шаблон можуть мати тільки одне визначення.
Всередині програми: об’єкт чи звичайна функція можуть мати тільки одне визначення.
Всередині програми: типи, шаблони функцій і вбудовані функції можуть мати кілька визначень, якщо вони ідентичні.
Порушення першої частини правила призведе до помилки компіляції. Порушення другої і третьої частин правила призведе до помилки лінкінгу.
Оголошення — це стейтмент, який повідомляє компілятор про існування ідентифікатора і його тип. Ось приклади оголошень:
1 2 |
int add(int x, int y); // повідомляємо компілятор про існування функції add(), яка має два параметри типу int і повертає цілочисельне значення int x; // оголошуємо цілочисельну змінну х |
Оголошення — це все, що необхідно для задоволення компілятора, але недостатньо для задоволення лінкера. Визначення — це те, що робить щасливим як компілятора, так і лінкера.
Тест
Завдання №1: У чому різниця між прототипом функції та її попереднім оголошенням?
Завдання №2: Запишіть прототип наступної функції:
1 2 3 4 |
int doMath(int first, int second, int third, int fourth) { return first + second * third / fourth; } |
Завдання №3: З’ясуйте, які з наступних програм не пройдуть етап компіляції, які не пройдуть етап лінкінга, а які не пройдуть і те, і інше.
Програма №1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int add(int x, int y); int main() { std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << std::endl; return 0; } int add(int x, int y) { return x + y; } |
Програма №2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int add(int x, int y); int main() { std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << std::endl; return 0; } int add(int x, int y, int z) { return x + y + z; } |
Програма №3:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int add(int x, int y); int main() { std::cout << "3 + 4 + 5 = " << add(3, 4) << std::endl; return 0; } int add(int x, int y, int z) { return x + y + z; } |
Програма №4:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int add(int x, int y, int z); int main() { std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << std::endl; return 0; } int add(int x, int y, int z) { return x + y + z; } |
Відповіді
Щоб переглянути відповідь, клікніть на неї мишкою.
Відповідь №1
Прототип функції — це стейтмент оголошення функції, який включає її ім’я, тип повернення і параметри. Тіло функції не записується.
Попереднє оголошення повідомляє компілятор про існування ідентифікатора до його фактичного визначення.
Для функцій прототип є попереднім оголошенням.
Відповідь №2
1 2 3 4 5 |
// Будь-який з наступних прототипів є правильним. // Не забувайте вказувати крапку з комою в кінці int doMath(int first, int second, int third, int fourth); // краще рішення int doMath(int, int, int, int); // альтернативне рішення |
Відповідь №3
Програма №1: Не скомпілюється. Компілятор скаржитиметься, що у виклику функції add() занадто багато аргументів.
Програма №2: Не скомпілюється. Компілятор скаржитиметься, що виклик функції add() не може прийняти стільки аргументів.
Програма №3: Провал на етапі лінкінгу. Функція аdd(), яка приймає два параметри, не була визначена (ми визначили функцію, яка приймає 3 параметри).
Програма №4: Успішні компіляція та лінкінг. Виклик функції add() відповідає прототипу, який був оголошений і визначенню функції.