Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
+ООП_Навч_посібник.doc
Скачиваний:
8
Добавлен:
01.07.2025
Размер:
6.58 Mб
Скачать

7.4.7. Організація рекурсивних функцій

Рекурсивна функція – це функція, яка викликає сама себе.

Рекурсія – це остання тема, яку ми розглянемо у цьому розділі. Рекурсія, яку іноді називають циклічним визначенням, є процес визначення чого-небудь на власній основі. В області програмування під рекурсією розуміють процес виклику функцією самої себе. Функцію, яка викликає саму себе, називають рекурсивною.

Класичним прикладом рекурсії є обчислення факторіалу від числа за допомогою функції factr(). Факторіал числа N є добуток всіх цілих чисел від 1 до N. Наприклад, факторіал числа 3 дорівнює 123, або 6. Рекурсивний спосіб обчислення факторіалу від числа продемонстровано у наведеному нижче коді програми. Для порівняння сюди ж включений і його нерекурсивний (ітеративний) еквівалент.

Код програми 7.21. Демонстрація рекурсивного способу обчислення факторіалу

#include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

int factr(int n);

int fact(int n);

int main()

{

// Використання рекурсивної версії.

cout << "Факторіал числа 4 дорівнює " << factr(4) << "\n";

// Використання ітеративної версії.

cout << "Факторіал числа 4 дорівнює " << fact(4) << "\n";

getch(); return 0;

}

// Рекурсивна версія.

int factr(int n)

{

int rezult;

if(n == 1) return (1);

rezult = factr(n-1)*n;

return (rezult);

}

// Ітеративна версія.

int fact(int n)

{

int t, rezult;

rezult = 1;

for(t=1; t<=n; t++) rezult = rezult*(t);

return (rezult);

}

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

Рекурсивна функція factr() є дещо складнішою. Якщо вона викликається з аргументом, що дорівнює 1, то відразу повертає значення 1. В іншому випадку вона повертає добуток factr(n-l)*n. Для обчислення цього виразу викликається метод factr() з аргументом n-1. Цей процес повторюється доти, доки аргумент не стане таким, що дорівнює 1, після чого викликані раніше методи почнуть повертати значення. Наприклад, під час обчислення факторіалу від числа 2 перше звернення до методу factr() приведе до другого звернення до того ж методу, але з аргументом, що дорівнює 1. Другий виклик методу factr() поверне значення 1, яке буде помножене на 2 (початкове значення параметра n). Можливо, Вам буде цікаво вставити у функцію factr() настанову cout, щоб показати ієрархічний рівень кожного виклику і проміжні результати.

Коли функція викликає саму себе, в системному стеку виділяється пам'ять для нових локальних змінних і параметрів, і код функції із самого початку виконується з цими новими змінними. Рекурсивний виклик не створює нової копії функції. Новими є тільки аргументи. Під час повернення кожного рекурсивного виклику із стека витягуються старі локальні змінні та параметри, а виконання функції поновлюється з "внутрішньої" точки її виклику. Про рекурсивні функції можна сказати, що вони "висуваються" і "засуваються".

Варто знати! Здебільшого використання рекурсивних функцій не дає значного скорочення об'єму коду програми. Окрім того, рекурсивні версії багатьох процедур виконуються повільніше, ніж їх ітеративні еквіваленти, через додаткові витрати системних ресурсів, пов'язаних з багатократними викликами функцій. Дуже велика кількість рекурсивних звернень до функції може викликати переповнювання стека. Оскільки локальні змінні і параметри зберігаються в системному стеку і кожен новий виклик створює нову копію цих змінних, може настати момент, коли пам'ять стека буде вичерпана. У цьому випадку можуть бути зруйновані інші ("ні у чому не винні") дані. Але, якщо рекурсія побудована коректно, про це навряд чи варто хвилюватися.

Основна перевага рекурсії полягає у тому, що деякі типи алгоритмів рекурсивно реалізуються простіше, ніж їх ітеративні еквіваленти. Наприклад, алгоритм сортування Quicksort достатньо важко реалізувати ітеративним способом. Окрім того, деякі завдання (особливо ті, які пов'язані з штучним інтелектом) просто створені для рекурсивних вирішень. Нарешті, у деяких програмістів процес мислення організований так, що їм простіше думати рекурсивно, ніж ітеративно.

Під час написання рекурсивної функції необхідно включити в неї настанову перевірки умови (наприклад, if-настанову), яка б забезпечувала вихід з функції без виконання рекурсивного виклику. Якщо цього не зробити, то, викликавши одного разу таку функцію, з неї вже не можна буде повернутися. Під час роботи з рекурсією це найпоширеніший тип помилки. Тому у процесі розроблення програм з рекурсивними функціями не варто ігнорувати настанову cout, щоб бути в курсі того, що відбувається в конкретній функції, і мати можливість перервати її роботу у разі виявлення помилки.

Розглянемо ще один приклад рекурсивної функції. Функція reverse() використовує рекурсію для відображення свого рядкового аргументу в зворотному порядку.

Код програми 7.22. Демонстрація відображення рядка в зворотному порядку за допомогою рекурсії

#include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

void reverse(char *s);

int main()

{

char strMas[] = "Це тест";

reverse(strMas);

getch(); return 0;

}

// Виведення рядка в зворотному порядку.

void reverse(char *s)

{

if(*s) reverse(s+1);

else return;

cout << *s;

}

Функція reverse() перевіряє, чи не переданий їй як параметр покажчик на нуль, яким завершується рядок. Якщо ні, то функція reverse() викликає саму себе з покажчиком на наступний символ у рядку. Цей "закручений" процес повторюється доти, доки тієї ж самої функції не буде передано покажчик на нуль. Коли, нарешті, виявиться символ кінця рядка, розпочнеться процес "розкручування", тобто викликані раніше функції почнуть повертати значення, і кожне повернення супроводжуватиметься "довиконанням" методу, тобто відображенням символу s. Внаслідок цього початковий рядок посимвольно відобразиться в зворотному порядку.

Розроблення рекурсивних функцій часто викликає труднощі у програмістів-початківців. Але з приходом досвіду використання рекурсії стає для багатьох звичайною практикою.

Розділ 8. Використання засобів програмування для розширення можливостей С++-функцій

У цьому розділі продовжимо вивчення деяких особливостей застосування С++-функцій, а саме розглянемо три засоби програмування: посилання, перевантаження функцій і використання аргументів за замовчуванням. Вони значною мірою розширюють можливості розроблених функцій користувача. Як буде показано далі, посилання – це неявний покажчик. Перевантаження функцій є властивістю, яка дає змогу одну і ту саму функцію реалізувати декількома способами, причому у кожному випадку можливе виконання окремого завдання. Тому є всі підстави вважати перевантаження функцій одним із шляхів підтримки поліморфізму мови C++. Використовуючи можливість задавання аргументів за замовчуванням, можна визначити значення для параметра, яке буде автоматично застосоване у випадку, якщо відповідний аргумент не задано.

Оскільки до параметрів функцій часто застосовуються посилання (це основна причина їх існування), почнемо цей розділ з короткого перегляду способів передачі аргументів функціям.