Home page

PC2A home page

Výuka home page




Faktoriál

(2016)



Princip volání funkce, podprogramu

Funkce slouží k dělení programu na logické části, čímž zlepšuje čitelnost programu. Umožňuje použití stejného kódu v různých místech (za pomoci volání, odskoku do podprogramu). Realizace funkce (tj. instrukce, které ji reprezentují), je v paměti pouze jednou.

Funkce se mohou volat navzájem – rozlišujeme potom funkci volající a funkci volanou. Volající funkce „zavolá“ funkci volanou pomocí instrukce skoku do podprogramu „call“ (na rozdíl od instrukce „prostého“ skoku umístí „call“ na zásobník adresu následující instrukce, protože po ukončení volané funkce se musí pokračovat z místa programu, ze kterého se „odskočilo“). Volaná funkce potom provede svou činnost a je ukončena returnem „return“ zajistí skok na adresu v programu, kterou na zásobník uložil „call“.

Volající a volaná funkce (v jazyce C/C++) sdílí data přes parametry/argumenty a návratovou hodnotu. Parametry jsou uvedeny svým typem a názvem v hlavičce funkce a jsou to lokální proměnné funkce (jejich život končí s instrukcí návratu z funkce). Návratová hodnota je uvedena pouze svým typem a nemá jméno - „zůstane“ po vykonání funkce jako její výsledek a dá se využít ve výrazech jako proměnná daného typu (typ dán návratovou hodnotou). Poté co je použita v daném výrazu (ve kterém je volána funkce) návratová hodnota „mizí“ a již není k dispozici.

Paměťové místo pro parametry funkce i pro návratovou hodnotu je vyhrazeno na zásobníku. Volající i volaná funkce si však musí rozumnět v typu a počtu parametrů. Proto je nutné aby před voláním funkce znal překladač její prototyp (jméno a typy argumentů a návratové hodnoty. Na základě této informace překladač nejen vyhradí správné místo na zásobníku, ale pokud je to nutné, tak provede i konverzi parametru z typu proměnné použité ve volání na typ očekávaný (je-li ve volání funkce, která očekává typ int proměnná typu double, je vyhrazeno místo na int a proměnná typu double je převedena na typ int a uložena do připraveného místa). Pro informování překladače o prototypech funkce slouží uvedení prototypů do hlavičkového souboru, který tímto oznamuje překladači jak má realizovat volání (konverze na typ očekávaný volanou funkcí).

S voláním funkce je spojená režie, spočívající ve „spotřebě“ paměti a času. Paměť je nutná k vytvoření a uložení předávaných proměnných, někdy je ještě před voláním funkce nutné „uklidit“ registry procesoru (uložit je na zásobník), protože volaná funkce je může použít pro své výpočty a tím přemazat v nich uložená data. Po ukončení funkce jsou tato data vrácena zpět do příslušných registrů. Dále je nutné uložit návratovou adresu. Tato manipulace s pamětí a časová režie pro „skoky“ na jiné adresy trvá nějaký čas – časová režie volání funkce.





Rekurzivní funkce

Jsou funkce, které volají samy sebe (nejčastěji přímo, ale může k tomu dojít i nepřímo (zprostředkovaně)). Používá se při řešení složitějších úloh (například neuronové sítě, expertní systémy), při hledání řešení, kdy se upraví parametry a zavolá se tatáž funkce.

Například výpočet faktoriálu lze vyjádřit funkcí, kdy se ve výpočtu opět vystupuje faktoriál: X! = X (X-1)!, kde výraz (X-1)! se počítá opět podle tohoto vzorce. Tento vzorec se použije pro X > 0, s čímž souvisí ukončovací podmínka (aby se z toho nestal nekonečný algoritmus končící pádem počítače na nedostatek paměti zásobníku). Faktoriál je tak vlastně definován pomocí faktoriálu.

Výhodou rekurze také je, že volání funkce nemusí být provedeno na konci algoritmu ale i na jeho začátku či uprostřed. Část algoritmu se tedy provádí při přímé a část až při zpětné cestě rekurzí. Jako příklad může sloužit tisk hodnot faktoriálu od nuly do daného čísla – u rekurzivního algoritmu faktoriálu se to dá vyřešit tiskem hodnoty po návratu z volání funkce s menším parametrem.
Dalším příkladem je způsob určení poloviny intervalu neznáme-li předem jeho délku (použito v programovacím jazyce Karel). Potom se dá použít rekurzivní algoritmus. Jeho základem je pravidlo, že doprostřed intervalu se dostaneme tak, že na každé dva kroky vpřed uděláme krok vzad. Rekurentní pravidlo společně s podmínkou ukončení lze napsat: Udělej dva kroky a pokud nejsi u zdi, použij tento návod, potom udělej krok zpět. (Průběh algoritmu je na následujícím obrázku, každý obdélník značí jednotlivé volání téže funkce. Pozn. po volání funkce se vracíme na následující část programu).

Nevýhodou rekurzivního volání funkcí je velká paměťová náročnost – při každém volání se vytváří nové lokální parametry, volané a návratové hodnoty. Jelikož se jedná o nestandardně velké požadavky na zásobník, může dojít k jeho přetečení a chybám programu. Překladače proto mají volbu na nastavení velikosti zásobníku.

Většina rekurzivních funkcí se dá přepsat pomocí cyklů – jejich použití je tedy otázkou zhodnocení jejich výhodnosti oproti tomuto řešení. Rekurzi si ukážeme na výpočtu faktoriálu (i když je často zmiňován jako nevhodný příklad použití, jedná se o jednoduchý a známý algoritmus (a z tohoto hlediska je tedy vhodný jako první ukázka této programovací techniky)).



Úkol:

V souboru mainfaktorial.c vypočtěte faktoriál (třemi „způsoby“). Dodržujte přesně požadovaný formát tisku.

0) Načtěte (funkcí scanf) dvě celá čísla oddělená čárkou. První celé číslo je hodnota jejíž faktoriál se bude počítat. Druhé číslo bude nula nebo jedna (udává, zda se provede bod číslo 4 zadání). Pro výpočty využívejte typ long int. Pokud se nenačtou dva parametry, nebo druhé číslo nebude jedna či nula, vypište text „chybne zadani“ a program ukončete s chybovým kódem 1.
1) Napište kód realizující výpočet faktoriálu (nerekurzivní) přímo ve funkci main. Vytiskněte ve funkci main faktoriál zadaného čísla (z bodu 0). Vytiskněte pouze výsledek (číslo) na začátek řádku a odřádkujte. Výpočet a tisk výsledku proveďte pouze je-li parametr pro výpočet správně zadán.
2) Napište funkci (se jménem faktnerek) počítající faktoriál (nerekurzivní). Funkce vrací faktoriál čísla zadaného jako parametr. Pokud parametr nabývá nevhodné hodnoty pro výpočet, vraťte hodnotu nula. Vypočtěte pro zadané číslo faktoriál. Vytiskněte výsledek jako v předchozím bodě.
3) Napište funkci (se jménem faktrek), počítající faktoriál, výpočet realizovaný rekurzivně. Funkce vrací faktoriál čísla zadaného jako parametr. Pokud parametr nabývá nevhodné hodnoty pro výpočet, vraťte hodnotu nula.

4) Využijte funkci z bodu 3 (vytvořte kopii se jménem faktrektisk) tak, aby při jediném jejím volání vytiskla tato funkce tabulku faktoriálů pro čísla od 0 (nuly) do zvoleného čísla. Tuto funkci zavolejte pouze, mělo-li druhé zadané číslo hodnotu 1. Formát tisku je: od začátku řádku je pole desíti znaků do kterých je vytištěna hodnota čísla ze kterého se aktuálně počítá faktoriál (zarovnáno na pravý kraj tohoto pole), potom následuje pole třiceti znaků, do kterého se vytiskne faktoriál aktuální hodnoty (zarovnáno na pravý kraj tohoto pole). Každá dvojice (hodnota + výsledek) má vlastní řádek.

Pokud některá z funkcí ohlásila chybu (vrátila nulu), funkce main vrací hodnotu 2, jinak hodnotu 0.



Testovací soubory.



Jaký je vhodný datový typ pro realizaci proměnných pro výpočet faktoriálu? Jaké jsou rozdíly (výhody/nevýhody použití různých datových typů).

Jaké je maximální číslo (pro zvolený datový typ) jehož faktoriál se dá vypočítat? Navrhněte jak toto číslo zjistit.

Představte si, jaký je rozdíl v použití paměťi (volání funkce, uložení proměnných …) při realizace funkce podle bodu 2 a 3.



Poznámky:

Faktoriál je označován „!“ (faktoriál pěti je: 5!=5*4*3*2*1*1= 120 ,10! = 3628800, 1000! = 4,02387260077093773543702433923e+2567). Definice funkce faktoriál je:
i ! je pro kladná i roven součinu (i * (i-1) * (i-2) * … 4 * 3 * 2 * 1).
Pro i=0 je i! = 1.

Zároveň je možné faktoriál definovat i jako rekurentní vztah (v definici se objeví tatáž funkce)
i ! je pro kladná i roven (i * (i-1)! )
pro i = 0 je i! = 1.



Rozšíření úkolu (na cvičeních pro rychlejší studenty)

Na základě příkladu (example) na adrese http://msdn.microsoft.com/en-us/vstudio/4e2ess30(v=vs.105).aspx (msdn time.h clock) zkuste zjistit kolik času jednotlivé funkce zaberou pro výpočet čísla MAX. (V případě, že je časový interval malý, je vhodné zavolat funkci několikrát a vypočítat průměrný čas).

Upravený příklad z uvedené adresy
http://msdn.microsoft.com/en-us/vstudio/4e2ess30(v=vs.105).aspx












// crt_clock.c
// This example displays the elapsed time for given program period.
//

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main( void )
{
   long    i = 6000000L;
   clock_t start, finish;
   double  duration;

   // Measure the duration of an event.
   printf( "Time to do %ld empty loops is ", i );
   start = clock();
   while( i-- ) 
      ;
   finish = clock();
   duration = (double)(finish - start) / CLOCKS_PER_SEC;
   printf( "%2.1f seconds\n", duration );
}







Poslední změna 2016-02-10