1. Анатомія сигнатури: з яких частин вона складається
Коли ви вперше чуєте слово «сигнатура», то можете згадати підпис у паспорті чи автограф. І це, загалом, не так уже й далеко від істини: сигнатура функції — це її «офіційний підпис» у коді. Вона пояснює і компілятору, і людям, як цю функцію можна викликати, які вхідні дані їй потрібні та що вона обіцяє повернути. Якщо сигнатура незрозуміла або «бреше», далі починає ламатися все: виклик, читання коду, пошук помилок.
У C++ сигнатура — не просто формальність. Саме за нею компілятор перевіряє виклики функцій: чи збігається кількість аргументів, чи підходять типи, чи можна використати результат як значення. Крім того, сигнатура — це частина «контракту» між вашим main() і рештою програми: якщо домовилися, що функція повертає bool, отже, її можна без жодних хитрощів поставити в if.
Якщо дивитися на визначення функції як на «шапку + тіло», то сигнатура — це майже вся «шапка». Вона описує, що це за функція і як із нею взаємодіяти. Важливо навчитися бачити в сигнатурі структуру, а не просто набір символів. Тоді ви перестанете боятися довгих рядків і почнете читати їх як звичайне речення: «ця функція бере ось це й повертає ось те».
Сигнатуру можна уявити як три основні частини: тип повернення, імʼя функції і список параметрів в круглих дужках. Тіло { ... } — це вже реалізація, але про неї компілятор «дізнається» пізніше. Спочатку він має зрозуміти саме підпис.
Загальна форма
Поки що працюємо без складніших конструкцій, тож достатньо тримати в голові просту форму:
тип_повернення імʼя_функції(параметри) {
// тіло функції
}
Наприклад:
int sum(int a, int b) {
return a + b;
}
Табличка-підказка: як читати сигнатуру
Щоб не плутатися, зручно один раз зафіксувати, яку роль має кожна частина сигнатури.
| Частина | Приклад | Зміст |
|---|---|---|
| Тип повернення | |
Який тип значення повертає функція (або , якщо нічого не повертає) |
| Імʼя | |
Як ми викликатимемо функцію |
| Параметри | |
Які значення потрібно передати у функцію |
2. Параметри: що це таке і як правильно їх вибирати
Параметри — це вхідні змінні функції. Вони створюються щоразу під час виклику й існують лише всередині функції. З практичного погляду параметри — це спосіб зробити функцію універсальною: замість «порахувати суму 3 і 4» ми пишемо «порахувати суму будь-яких двох чисел». І що вдаліше ви добираєте параметри, то менше спокуси лізти в зовнішні змінні й влаштовувати в програмі телепатію.
Водночас параметри — не просто «змінні в дужках». Вони задають правила виклику: важливі порядок, тип і кількість. Компілятор не намагається вгадати, що ви «мали на увазі». Він вірить сигнатурі буквально: що написали, за те й відповідаєте.
Параметр = тип + імʼя
Мінімально зрозумілий параметр має такий вигляд: int count, char ch, std::string text.
#include <string>
int count_digits(std::string text) {
int cnt = 0;
for (char c : text) {
if (c >= '0' && c <= '9') cnt += 1;
}
return cnt;
}
Тут text — параметр. Він існує лише всередині count_digits(); зовні його ніби й немає.
Поки ми не заглиблювалися в тему «передавання за посиланням», будемо чесні: std::string тут передається за значенням, тобто копіюється. Для навчальних прикладів це нормально. Оптимізацію та способи не створювати зайвих копій ми розберемо пізніше.
Порядок параметрів — частина домовленості
Дуже поширена помилка новачків: «ну я ж передав два числа, яка різниця, у якому порядку?». Різниця велика, бо параметри в сигнатурі стоять у конкретному порядку.
int rectangle_area(int width, int height) {
return width * height;
}
Якщо ви викликаєте rectangle_area(2, 10), то width == 2, а height == 10. Компілятор не може здогадатися, що ви «взагалі-то мали на увазі навпаки».
Скільки параметрів варто робити
Є практичне правило «на відчуття»: якщо у функцію потрібно передати 6–7 параметрів, то ви майже напевно намагаєтеся покласти на неї забагато відповідальності. Але зараз ми ще не обговорюємо проєктування великих моделей даних, тому діємо простіше: параметрів має бути рівно стільки, скільки потрібно для роботи функції, без магії та глобального контексту.
3. Аргументи: чим вони відрізняються від параметрів
Якщо параметри — це те, що написано в оголошенні або визначенні функції, то аргументи — це те, що ви реально передаєте під час виклику. Здавалося б, дрібниця в термінах, але на практиці плутанина між «параметром» і «аргументом» призводить до дивних пояснень: «я змінив аргумент усередині функції, але зовні нічого не змінилося». Насправді ви змінювали параметр, тобто локальну копію, — і тут усе логічно.
Зафіксуймо це на дуже короткому прикладі — і більше не плутаймо ці поняття.
#include <iostream>
int add(int a, int b) { // a і b — параметри
return a + b;
}
int main() {
std::cout << add(10, 20) << '\n'; // 10 і 20 — аргументи
}
Аргументом може бути вираз
Аргумент — це не обовʼязково «просто число». Це може бути змінна, вираз, результат іншої функції — будь-що, що обчислюється у значення відповідного типу.
#include <iostream>
int square(int x) {
return x * x;
}
int main() {
int a = 3;
std::cout << square(a + 1) << '\n'; // 16
}
Зверніть увагу: a + 1 — це вираз, але він обчислюється в int, тому підходить параметру int x.
4. Тип повернення й використання результату
Тип повернення — це частина сигнатури, яку новачки часто сприймають як «ну треба щось написати зліва». Насправді це ключова частина контракту: тип повернення визначає, чи можна використовувати функцію як значення, чи можна покласти її результат у змінну, чи можна поставити її в умову if і взагалі що саме ми отримуємо «на виході».
Водночас сьогодні ми не заглиблюємося в тонкощі return і в правило «усі шляхи мають повертати значення» — це буде окрема тема. Зараз нам важливо навчитися вибирати тип результату й розуміти наслідки.
Функція повертає значення: int, double, bool, std::string
Наприклад, функція-перевірка зазвичай повертає bool, бо її зручно читати в if:
#include <string>
bool is_short(std::string text) {
return text.size() < 5;
}
Тепер це читається природно:
if (is_short(name)) { /* ... */ }
А функція-обчислення часто повертає число:
double average(int a, int b) {
return (a + b) / 2.0;
}
Функція нічого не повертає: void
Іноді функція просто виконує дію: друкує, малює рамку, показує меню. У такому разі повертати значення не потрібно — використовуємо void.
#include <iostream>
void print_separator() {
std::cout << "----------\n"; // ----------
}
І ось тут є важливий момент: void-функцію не можна «підставити як значення». Тобто так писати не варто:
std::cout << print_separator(); // помилка: print_separator нічого не повертає
Виклик функції як частина виразу
Одна з найприємніших властивостей функцій, що повертають значення, — їх можна використовувати як будівельні блоки у виразах. Ви вже вмієте додавати числа й порівнювати їх. Виклик функції, яка повертає int, поводиться майже як int: його можна додати, порівняти, вивести або присвоїти.
Це схоже на модель «викликав функцію — отримав значення», і для новачків вона дуже корисна. Особливо якщо ви хочете, щоб main() був схожий на сценарій: «отримати дані → обчислити → вивести».
#include <iostream>
int double_value(int x) {
return x * 2;
}
int main() {
std::cout << double_value(21) << '\n'; // 42
}
#include <iostream>
int triple(int x) {
return x * 3;
}
int main() {
int v = triple(7);
std::cout << v << '\n'; // 21
}
5. Правила оголошення: що компілятор має знати про функцію
Зараз буде момент, який у реальному житті спричиняє приблизно 30 % усіх «чому воно не компілюється?!». Компілятор читає файл згори вниз. Це означає, що коли він натрапляє на виклик foo(...), то вже має розуміти, що таке foo, скільки в неї параметрів і якого вони типу. Тобто функція має бути оголошена до місця використання.
Оголошення — не обовʼязково окремий рядок-прототип. Ми розберемо це в наступній лекції. На цьому етапі є простий і надійний спосіб жити без болю: пишіть функції вище main(). Тоді на момент, коли main() почне їх викликати, компілятор уже все бачитиме.
«Оголошена» й «реалізована» на побутовому рівні
Поки що спростімо термінологію: коли компілятор бачить рядок виду int sum(int a, int b), він уже знає сигнатуру. Якщо після неї йде { ... }, то ми одразу даємо і оголошення, і реалізацію в одному місці.
А от прототипи, тобто оголошення без тіла, і порядок коду у файлі — це наступний крок. Саме він дає змогу тримати main() нагорі й не перетворювати початок файла на простирадло з функцій. Але поки що ми туди не забігаємо.
Мінісхема: як компілятор «бачить» файл
flowchart TD
A[Компілятор читає файл згори вниз] --> B{Натрапляє на виклик функції}
B -->|Функцію вже оголошено вище| C[Перевіряє сигнатуру й компілює виклик]
B -->|Функцію ще не оголошено| D[Помилка: функцію не оголошено]
6. Практичний мініпроєкт: «TextLab» — консольний аналізатор рядків
Зараз зберемо маленький застосунок, який ще деякий час буде з нами: меню, вибір дії, обробка рядка. Наша мета не в тому, щоб написати «ідеальний продукт», а в тому, щоб на прикладі побачити, як сигнатури роблять код читабельним. Ми додаватимемо функції так, щоб main() ставав сценарієм, а деталі жили в окремих блоках.
Поки що зробимо три функції: друк меню (void), підрахунок цифр (int) і перевірку «чи є пробіл» (bool). І все це акуратно підʼєднаємо в main().
Друк меню: void, без параметрів
#include <iostream>
void print_menu() {
std::cout << "1) Порахувати цифри\n";
std::cout << "2) Є пробіл?\n";
std::cout << "0) Вихід\n";
}
Зауважте: параметрів немає, бо меню завжди однакове. Тип повернення void, тому що функція нічого «не віддає» — вона просто друкує.
Підрахунок цифр: int, один параметр
#include <string>
int count_digits(std::string text) {
int cnt = 0;
for (char c : text) {
if (c >= '0' && c <= '9') cnt += 1;
}
return cnt;
}
Сигнатура тут говорить сама за себе: «дайте мені рядок — я поверну кількість цифр».
Перевірка умови: bool, один параметр
#include <string>
bool contains_space(std::string text) {
for (char c : text) {
if (c == ' ') return true;
}
return false;
}
Знову ж таки, функція повертає bool, тому її зручно використовувати в if.
Збираємо main() як сценарій
#include <iostream>
#include <string>
void print_menu() {
std::cout << "1) Порахувати цифри\n";
std::cout << "2) Є пробіл?\n";
std::cout << "0) Вихід\n";
}
int count_digits(std::string text) {
int cnt = 0;
for (char c : text) {
if (c >= '0' && c <= '9') cnt += 1;
}
return cnt;
}
bool contains_space(std::string text) {
for (char c : text) {
if (c == ' ') return true;
}
return false;
}
int main() {
std::string text;
std::getline(std::cin, text);
print_menu();
int choice = 0;
std::cin >> choice;
if (choice == 1) {
std::cout << count_digits(text) << '\n'; // наприклад: 3
} else if (choice == 2) {
std::cout << contains_space(text) << '\n'; // 0 або 1
}
}
Так, цей застосунок поки що «сируватий»: ми ще не повторюємо меню в циклі, не обробляємо всі варіанти й до того ж змішали getline з >>. Але для сьогоднішньої теми він ідеальний: на ньому добре видно, як сигнатури визначають, що саме можна зробити з результатом функції.
7. Типові помилки
Помилка №1: плутанина між параметрами й аргументами.
Часто програміст-початківець каже: «я змінив аргумент усередині функції». Насправді всередині функції ви змінюєте параметр — локальну змінну, створену під час виклику. Аргумент існує в місці виклику й може бути хоч a + 1, його не можна «помацати» зсередини функції напряму.
Помилка №2: неправильний вибір типу повернення, особливо int замість bool або навпаки.
Іноді пишуть функцію is_valid(...), але повертають int і починають кодувати зміст числами: 0 — погано, 1 — добре. Формально це працює, але читається гірше й провокує помилки. Якщо зміст результату — «так/ні», то bool робить код чеснішим.
Помилка №3: спроба використовувати void-функцію як значення.
Конструкція виду std::cout << print_menu(); виглядає логічно, якщо думати, що «вивести можна все». Але void означає, що значення немає, тому такий код не компілюється. Якщо потрібно щось вивести, нехай функція повертає рядок або друкує сама, але тоді її виклик має бути окремою інструкцією.
Помилка №4: виклик функції до того, як компілятор побачив її сигнатуру.
Новачки часто пишуть main() нагорі, а функції — нижче, і отримують помилку «функцію не оголошено». На поточному етапі є два способи це виправити: або підняти визначення функцій вище main(), або, трохи пізніше, використовувати прототипи. Сьогодні ми дотримуємося першого, найпрямішого варіанта.
Помилка №5: «параметри без змісту» — імена на кшталт a, b, x1, x2 там, де важлива читабельність.
У математичній задачі a і b ще терпимі, але в прикладному коді краще писати width, height, text, count. Сигнатура — це частина документації до вашої функції. Що зрозуміліше вона читається, то менше вам доведеться «дебажити очима» власний код за тиждень.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ