Funkcje w C/C++ mogą wywoływać same siebie — takie funkcje nazywamy funkcjami rekurencyjnymi. Przy definiowaniu takich funkcji trzeba oczywiście zadbać, aby ciąg wywołań skończył się. Zatem przed wywołaniem samej siebie funkcja zwykle sprawdza pewien warunek i jeśli nie jest on spełniony, nie dokonuje już samo-wywołania.
Jako przykład rozpatrzmy funkcję
gcd
z poniższego programu.
Oblicza ona i zwraca największy wspólny dzielnik
(ang. greatest common divisor) swoich dwóch
argumentów, które są liczbami naturalnymi (całkowitymi
dodatnimi). Użyty algorytm to znana wszystkim modyfikacja
algorytmu Euklidesa
opisanego w Elementach jako Twierdzenie VII.2
(i będącego wypadkiem szczególnym wcześniejszego i bardziej ogólnego
algorytmu, przypisywanemu Teajtetosowi):
1. #include <iostream> 2. using namespace std; 3. 4. int gcd(int a, int b) { 5. return b == 0 ? a : gcd(b, a % b); 6. } 7. 8. int main() { 9. int x, y; 10. 11. x = 732591; y = 1272585; 12. cout << "gcd(" << x << "," << y << ")\t= " 13. << gcd(x,y) << endl; 14. 15. x = 732591; y = 1270; 16. cout << "gcd(" << x << "," << y << ")\t= " 17. << gcd(x,y) << endl; 18. }
Sama funkcja realizująca algorytm składa się w zasadzie z jednej instrukcji return. Dopóki zmienna lokalna b nie stanie się równa zeru, funkcja wywołuje się rekurencyjnie. Z analizy teoretycznej wynika, że dla prawidłowych danych (a więc liczb naturalnych, czyli, w szczególności, dodatnich) po skończonej liczbie kroków wartość b musi stać się równa zeru. Tak więc warunek zakończenia (tzw. warunek stopu) jest zapewniony i funkcja działa prawidłowo:
gcd(732591,1272585) = 129 gcd(732591,1270) = 1„Samowywołanie” się funkcji jest w zasadzie normalnym wywołaniem: wszystkie lokalne zmienne są tworzone oddzielnie w każdym wywołaniu, mechanizm przekazywania argumentów jest taki sam jak dla funkcji nierekursywnych. Na przykład funkcja gcd kreuje zmienne lokalne a i b a następnie, obliczając wyrażenie za dwukropkiem, wywołuje tę samą funkcję gcd i czeka na zwrócenie wyniku. To następne „wcielenie” funkcji też tworzy swoje zmienne lokalne a i b, woła gcd i czeka na wynik, aby go zwrócić, itd. Kiedy w końcu b stanie się zero, funkcja zwróci a, które zostanie zwrócone przez poprzednie „wcielenie”, które zostanie zwrócone przez poprzednie „wcielenie”, itd.
Funkcje rekurencyjne trzeba stosować z umiarem i umiejętnie. Nieprzemyślane zastosowanie rekurencji może bowiem prowadzić do kombinatorycznej eksplozji liczby wywołań i rozmiaru stosu potrzebnego do realizacji rekurencji. Tak na przykład bywa, gdy w treści funkcji wywołanie samej siebie występuje dwukrotnie, jak w klasycznym przykładzie nieprawidłowego obliczania wartości wyrazów ciągu Fibonacciego. Definicja tego ciągu jest następująca:
Naturalnym sposobem zaprogramowania takiej funkcji jest postać rekurencyjna
jak w następującym programie (w linii ➊, po dwukropku, mamy tu
dwukrotne wywołanie przez funkcję samej siebie):
1. #include <iostream> 2. #include <iomanip> 3. using namespace std; 4. 5. int licznik; 6. 7. int fib(int n) { 8. licznik++; 9. return (n < 2) ? n : fib(n-1) + fib(n-2); ➊ 10. } 11. 12. int main() { 13. cout << "\n i Fib(i) # wywolan\n" 14. "---------------------------" << endl; 15. for (int i = 10; i <= 43; i += 3) { 16. licznik = 0; 17. int w = fib(i); 18. cout << setw(3) << i << setw(12) << w ➋ 19. << setw(12) << licznik << endl; 20. } 21. }
W programie wprowadziliśmy zmienną globalną licznik zliczającą liczbę wywołań funkcji fib podczas znajdowania elementów ciągu Fibonacciego — zmienna ta jest zerowana przed przystąpieniem do wyliczania każdego kolejnego elementu ciągu. W ten sposób możemy wypisać nie tylko wartości kolejnych liczb Fibonacciego, ale również liczbę wywołań funkcji, jak były potrzebne do ich wyliczenia. Obliczenia zabrały kilka sekund; większość tego czasu zużyta została na obliczenie ostatniej wartości, czyli F43. Poniższy wydruk z tego programu pokazuje, że do obliczenia F43 potrzebne było ponad miliard wywołań!
i Fib(i) # wywolan --------------------------- 10 55 177 13 233 753 16 987 3193 19 4181 13529 22 17711 57313 25 75025 242785 28 317811 1028457 31 1346269 4356617 34 5702887 18454929 37 24157817 78176337 40 102334155 331160281 43 433494437 1402817465Oczywiście wykonanie wersji „normalnej”, a więc tzw. iteracyjnej, tej funkcji zabiera ułamek sekundy i w ogóle nie zużywa miejsca na stosie. [Użyte w tym programie tzw. manipulatory (setw w linii ➋) służą jedynie do przejrzystego sformatowania tabelki wyników — będą one omówione w rozdziale o operacjach we/wy ].
T.R. Werner, 21 lutego 2016; 20:17