JavaRush /Курси /C++ SELF /Знайомство з функціями

Знайомство з функціями

C++ SELF
Рівень 14 , Лекція 0
Відкрита

1. Вступ

Спершу давайте чесно зізнаємося: писати все в main() — цілком нормальний етап. Так роблять усі. Навіть досвідчені розробники… коли поспішають, втомилися й думають: «тимчасово, потім виправлю» (спойлер: не виправлять). Проблема починається тоді, коли main() стає надто довгим і без прокручування та внутрішнього діалогу із собою ви вже не розумієте, що там відбувається.

Уявіть, що main() робить усе підряд: зчитує дані, перевіряє введення, рахує результат, друкує, показує меню, знову зчитує, знову перевіряє… У підсумку будь-які зміни стають небезпечними: ви виправили одну дрібницю, а зламалося щось на іншому кінці файла. Не тому, що ви «поганий програміст», а тому, що код не поділено на смислові частини, у яких є назви й межі відповідальності.

Декомпозиція — це спосіб перетворити код із «простирадла» на набір деталей конструктора: кожна деталь відповідає за свою роботу. А ще це значно спрощує налагодження: якщо помилка в друку меню, ви йдете у функцію друку меню, а не шукаєте серед 300 рядків, де там був cout.

Функція як «іменована дія»

Функція — це іменований блок коду, який можна викликати. Але важливіше інше: це спосіб дати імʼя дії. Коли ви даєте імʼя, то перестаєте щоразу думати про деталі. Це як із кнопкою на мікрохвильовці: вам не потрібно щоразу заново вигадувати, «як розігріти їжу», — ви просто натискаєте «Розігрів».

З погляду програми функція допомагає у двох речах: по-перше, ховає деталі — цикл, перевірки, формат виведення — за зрозумілою назвою; по-друге, дає змогу повторно використовувати код без копіпасти. І так, копіпаста — це не «швидко». Це «швидко зараз, дорого потім».

Міні-ідея, яку варто тримати в голові з першого дня знайомства з функціями: хороший main() — це сценарій. Він має читатися приблизно як інструкція: «прочитати команду → обробити → показати результат → повторити». А деталі нехай живуть у функціях.

2. «Тонкий main» як сценарій

Що означає «тонкий main»

Термін «тонкий main» означає, що в main() майже немає деталей. Він не займається тим, «як саме друкувати меню» або «як саме додавати елемент до списку». Він лише каже: зараз покажемо меню, зараз прочитаємо команду, зараз обробимо команду.

Це схоже на роль режисера: режисер не тримає камеру, не пише музику й не клеїть декорації. Він каже: «сцена 1», «сцена 2», «сцена 3». У коді «режисер» — це main(), а «сцени» — функції.

Щоб цей образ закріпився, ось невелика схема. Не сприймайте її як сувору архітектуру: це радше «образ у голові».

flowchart TD
    A[main()] --> B[показати меню]
    A --> C[прочитати команду]
    A --> D[обробити команду]
    D --> E[додати елемент]
    D --> F[показати елементи]
    D --> G[очистити елементи]

Головна думка: main() не зобовʼязаний знати, як улаштовані add_item або print_items. Він просто викликає їх як «інструменти з назвами».

Коли справді час виділяти функцію

Початківці часто запитують: «А як зрозуміти, що цей шматок треба винести?» Це хороше запитання, бо тут легко впасти у дві крайнощі. Перша крайність — усе в main(), друга — 50 функцій по одному рядку, і ви вже не розумієте, де саме перебуваєте.

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

Невелика таблиця — як підказка для мозку:

Спостереження в коді Що це зазвичай означає Що зробити
Один і той самий фрагмент скопійовано два чи більше разів Майбутній біль під час змін Винести у функцію
У середині main() починається «мікроалгоритм» (цикл + умови + виведення) Це окрема дія Дати імʼя: винести у функцію
main() складно переказати одним реченням Змішані відповідальності Розділити на кроки-функції
Функція виростає до 40–60 рядків, і ви не можете коротко сказати, «що вона робить» Вона робить забагато Розділити на 2–3 функції

Важливо: на перших порах не женіться за «ідеальністю». Наша мета — зробити код зрозумілішим насамперед для вас самих. Якщо після винесення функції читати стало легше, отже, ви все зробили правильно.

3. Практичний приклад: міні-застосунок «Список справ»

Зараз ми почнемо збирати маленький консольний застосунок, який супроводжуватиме нас упродовж усього курсу й поступово вдосконалюватиметься. Сьогоднішня версія буде простою: список рядків (справ), команди додати/показати/очистити/вийти. Жодної магії — лише вже знайомі std::string, std::vector, цикли та введення/виведення.

Спочатку ми спеціально зробимо «погану» версію, де все в main(). Потім почнемо виносити частини у функції — і побачимо, як main() стає тонким і зручним.

Поганий старт: усе в main()

Цей шматок коду не «жахливий», він просто типовий. І це саме те, із чого зручно починати рефакторинг.

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> tasks;
    while (true) {
        std::cout << "1) додати  2) список  3) очистити  0) вихід\n> ";
        std::string cmd;
        std::getline(std::cin, cmd);

        if (cmd == "1") {
            std::cout << "Справа: ";
            std::string text;
            std::getline(std::cin, text);
            tasks.push_back(text);
        } else if (cmd == "2") {
            for (int i = 0; i < static_cast<int>(tasks.size()); ++i)
                std::cout << (i + 1) << ") " << tasks[i] << '\n';
        } else if (cmd == "3") {
            tasks.clear();
        } else if (cmd == "0") {
            break;
        }
    }
}

Зауважте: тут уже є все — меню, введення команди, обробка, робота з даними, друк. Поки програма маленька, це ще терпимо. Але варто додати «видалити одну справу», «пошук за підрядком», «зберегти/завантажити» — і main() швидко перетвориться на кашу.

Крок 1: виносимо «показати меню»

Зараз ми акуратно зробимо найпростіше: дамо імʼя друку меню. Це майже завжди хороший перший крок, бо меню зазвичай друкується в кількох місцях або друкуватиметься пізніше, і воно взагалі не зобовʼязане жити всередині main().

Головна перевага тут така: main() починає читатися як сценарій, а не як драйвер принтера. Ми поки не обговорюємо тонкощі параметрів і значень, які повертаються, — нам важливо відчути саму ідею «іменованої дії».

#include <iostream>

void print_menu() {
    std::cout << "1) додати  2) список  3) очистити  0) вихід\n> ";
}

І тепер main() викликає цю дію:

#include <iostream>
#include <string>
#include <vector>

void print_menu() {
    std::cout << "1) додати  2) список  3) очистити  0) вихід\n> ";
}

int main() {
    std::vector<std::string> tasks;

    while (true) {
        print_menu();
        std::string cmd;
        std::getline(std::cin, cmd);
        // обробка команди поки тут
    }
}

Смішно, але це вже перемога: ми прибрали одну «деталь» із main() і дали їй імʼя.

Крок 2: виносимо читання рядка з підказкою

Дуже часто програми роблять одне й те саме: друкують підказку й читають рядок. Це ідеальний кандидат на функцію, бо така дія повторюється і має зрозумілий зміст: «прочитати рядок».

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

#include <iostream>
#include <string>

std::string read_line(const std::string& prompt) {
    std::cout << prompt;
    std::string s;
    std::getline(std::cin, s);
    return s;
}

Тепер main() стає ще більш «режисерським»:

#include <iostream>
#include <string>
#include <vector>

std::string read_line(const std::string& prompt) {
    std::cout << prompt;
    std::string s;
    std::getline(std::cin, s);
    return s;
}

int main() {
    std::vector<std::string> tasks;

    while (true) {
        std::string cmd = read_line("1) додати  2) список  3) очистити  0) вихід\n> ");
        // обробка команди поки тут
    }
}

Так, ми поки що друкуємо меню прямо з read_line() — це нормально для поточного кроку. Декомпозицію часто роблять «драбинкою», а не одним стрибком.

Крок 3: операції зі списком справ

Тепер винесемо обробку списку справ: друк, додавання, очищення. Тут важливо тримати в голові один нюанс: ми ще не проходили передавання за посиланням, тому заради простоти повертатимемо новий std::vector<std::string> із функцій, які його змінюють. Для маленьких навчальних прикладів це нормально: мета — зрозуміти декомпозицію, а не займатися оптимізацією.

Друк списку справ

Функція друку не змінює список, вона просто його показує.

#include <iostream>
#include <string>
#include <vector>

void print_tasks(const std::vector<std::string>& tasks) {
    for (int i = 0; i < static_cast<int>(tasks.size()); ++i) {
        std::cout << (i + 1) << ") " << tasks[i] << '\n';
    }
}

Якщо вас збентежив const std::vector<std::string>&: це «посилання на константний вектор», тобто ми обіцяємо не змінювати tasks. Докладніше ми розберемо це на наступному занятті, але саму ідею можна зрозуміти вже зараз: «я хочу передати список у функцію, але не копіювати його повністю».

Якщо поки хочеться зовсім обійтися без посилань, можна зробити версію «за значенням» — вона теж працюватиме, просто створюватиме копію. Для навчання декомпозиції це допустимо.

Додавання справи

Додавання змінює список, тому в нашому «поки що без посилань» стилі повернемо новий вектор.

#include <string>
#include <vector>

std::vector<std::string> add_task(std::vector<std::string> tasks, std::string text) {
    tasks.push_back(text);
    return tasks;
}

Очищення списку

Очищення теж змінює список.

#include <string>
#include <vector>

std::vector<std::string> clear_tasks(std::vector<std::string> tasks) {
    tasks.clear();
    return tasks;
}

Збираємо «тонкий main»: сценарій замість простирадла

Зараз найприємніше: ми поєднуємо все в main() так, щоб він читався як сценарій. Зверніть увагу: main() більше не знає, як саме друкуються справи або як саме додається нова справа. Він просто каже: «якщо команда така — робимо ось це».

Це і є «тонкий main»: мінімум деталей, максимум змісту.

#include <iostream>
#include <string>
#include <vector>

std::string read_line(const std::string& prompt) {
    std::cout << prompt;
    std::string s;
    std::getline(std::cin, s);
    return s;
}

void print_tasks(const std::vector<std::string>& tasks) {
    for (int i = 0; i < static_cast<int>(tasks.size()); ++i) {
        std::cout << (i + 1) << ") " << tasks[i] << '\n';
    }
}

std::vector<std::string> add_task(std::vector<std::string> tasks, std::string text) {
    tasks.push_back(text);
    return tasks;
}

std::vector<std::string> clear_tasks(std::vector<std::string> tasks) {
    tasks.clear();
    return tasks;
}

int main() {
    std::vector<std::string> tasks;

    while (true) {
        std::string cmd = read_line("1) додати  2) список  3) очистити  0) вихід\n> ");

        if (cmd == "1") {
            std::string text = read_line("Справа: ");
            tasks = add_task(tasks, text);
        } else if (cmd == "2") {
            print_tasks(tasks);
        } else if (cmd == "3") {
            tasks = clear_tasks(tasks);
            std::cout << "Очищено\n";
        } else if (cmd == "0") {
            break;
        } else {
            std::cout << "Невідома команда\n";
        }
    }
}

Так, main() усе ще містить розгалуження за командами. Але тепер усередині гілок майже немає деталей. І це вже величезний крок: програма стала розширюваною. Хочете додати команду "4) видалити"? Ви просто додасте гілку й функцію, а не почнете вбудовувати нову операцію в гігантський main().

4. Як називати функції, щоб вас розуміли

Імена функцій — це не прикраса, а частина інтерфейсу програми. Початківці часто соромляться довших назв, але краще чесне print_tasks(), ніж загадкове doStuff2() (яке за тиждень означатиме «роби щось, але що саме — не знаю навіть я»).

Хороша назва функції зазвичай починається з дієслова й відповідає на запитання «що робить?»: print_menu, read_line, add_task, clear_tasks. Погана назва — це або «як робить»: loop_and_print_vector (надто детально), або така, що нічого не каже: f1.

Усередині команди розробників назви — це ще й інструмент домовленості. У навчальному проєкті це особливо важливо, бо ви самі для себе теж команда, просто поки що з однієї людини. І так, іноді ви сперечаєтеся з керівником команди — і цим керівником теж є ви.

5. Типові помилки

Помилка № 1: функція «робить усе одразу» (введення + обчислення + виведення) без потреби.
Початківцям часто здається, що зручно написати void add_task() так, щоб вона сама запитувала текст, сама додавала його й сама друкувала «успішно». У маленьких прикладах це працює, але дуже швидко перетворює код на заплутаний клубок. Намагайтеся, щоб функція виконувала одну зрозумілу дію. Якщо потрібно і запитати, і додати, — нехай main() організує сценарій: спочатку read_line(), потім add_task().

Помилка № 2: винести у функцію «шматок коду», але не винести зміст.
Буває так: ви створили функцію process() і перенесли туди половину main(). Формально декомпозиція є, але фактично змісту не побільшало. Простий тест — спробуйте прочитати main() як текст: якщо він усе ще незрозумілий без переходу всередину функцій, отже, функції названо надто загально або виділено не за смисловими межами.

Помилка № 3: надто дрібні функції «заради функцій».
Іноді хтось зі студентів виносить у функцію рядок на кшталт std::cout << '\n'; і гордо каже: «у мене все у функціях». Формально так, але читати стало гірше. Якщо функція не додає змісту, тобто не дає хорошої назви дії, і не зменшує повторення, то вона радше заважає, ніж допомагає.

Помилка № 4: копіпаста замість функції — «потім зроблю красиво».
Найпідступніший сценарій: ви один раз скопіювали 6 рядків. Потім другий. Потім третій. А далі змінюєте одну деталь і забуваєте змінити її у двох копіях. У підсумку зʼявляються «примарні баги», коли однакові шматки поводяться по-різному. Щойно ви помітили другий копіпаст того самого фрагмента, це майже гарантований кандидат на функцію.

Помилка № 5: намагатися зробити main() «ідеально порожнім» і втратити керованість.
Іноді після слів «тонкий main» хочеться довести ідею до абсурду: сховати взагалі все, зокрема цикл і вибір команд. У підсумку main() стає незрозумілим набором магічних викликів, а логіка керування виявляється розпорошеною. Правильний тонкий main() — це сценарій, а сценарій зазвичай містить цикл і розвилки. Тонкість тут в іншому: усередині сценарію не повинно бути брудних деталей.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ