[Přednáška 3] [Obsah] [Přednáška 5]

Přednáška 4


Témata


Funkce a procedury v jazyce C

Procedury a funkce označujeme společným pojmem podprogramy. Podprogram je část kódu (programu), samostatně definovaná, kterou je možné volat opakovaně z různých míst kódu. Podprogram může mít tzv. parametry, což jsou data, které se podprogramu předávají ke zpracování. Výhoda podprogramů spočívá v tom, že určitý algoritmus, který potřebujeme používat opakovaně, napíšeme pouze jednou (při definici) a na místech, kde potřebujeme tento algoritmus provést, jej pouze zavoláme. Navíc, řešíme-li nějakou složitější úlohu, rozdělíme si složitý problém na jednodušší dílčí podproblémy, jejichž algoritmus řešení napíšeme právě jako podprogramy.

Funkce je podprogram, který vrací („vyhazuje“) nějakou návratovou hodnotu (vypočtené číslo) a lze jej tedy použít ve výrazu, procedura je podprogram, který nevrací žádnou hodnotu, pouze provede nějakou akci (např. tiskne). Neznamená to ovšem, že procedura nemůže vypočítat nějakou hodnotu, ale ta musí uložit třeba do globální proměnné; v textu si ukážeme i jiný způsob.

Definice funkcí

Definice funkce zahrnuje hlavičku funkce (název a seznam formálních parametrů) a její tělo. Tělo funkce (kód) je uzavřeno mezi složené závorky „{“ a „}“, stejně jako hlavní program. Způsob definice funkce se liší v původní verzi K&R jazyka C a ve verzi podle ANSI normy. Definice podle ANSI je bližší jiným jazykům, např. Pascalu. Přesto má jazyk C v oblasti definice funkcí některé zvláštnosti, na které je potřeba upozornit.

Definice začíná specifikací návratové hodnoty, pak následuje identifikátor funkce (jméno funkce) se seznamem formálních parametrů v kulatých závorkách. Je-li parametrů více, oddělují se čárkou. Právě zde je první odlišnost mezi verzí K&R a ANSI. Rozdíl demonstrujeme na funkci, která počítá obsah obdélníka:

Definice podle K&R Definice podle ANSI
double obsah_obd(a,b)
  double a,b;
{
  return a*b;
}
double obsah_obd(double a, double b)
{
  return a*b;
}

Definovali jsme funkci obsah_obd, která vrací hodnotu (číslo) typu double a má dva formální parametry a, b.

Druhý způsob definice formálních parametrů (podle ANSI) je výhodnější, protože umožňuje překladači lépe kontrolovat skutečné parametry při volání funkce (ve verzi K&R není nutné typy na dalším řádku uvádět, překladač pak předpokládá typ int). Návratová (výstupní) hodnota se vrací pomocí příkazu return vyraz;, resp. return (vyraz);.

Uvnitř těla funkce je možné deklarovat lokální proměnné, které jsou platné pouze uvnitř těla. Deklarace se provádí běžným způsobem, např.:

double obsah_obd(double a, double b)
{
  double obsah;
  obsah = a*b;
  return obsah;
}
Lokální proměnné jsou alokovány automaticky při vstupu do funkce (při volání) na zásobníku (označují se také jako proměnné ve třídě auto).

Pokud není v definici uveden typ návratové hodnoty nebo typ parametru, překladač předpokládá implicitně typ int. Následujícím zápisem definujeme funkci

obsah_obd(double a, double b)
{
  return a*b;
}
vracející hodnotu typu int (nikoliv proceduru, jak by se mohlo na první pohled zdát). Při návratu se provede automatická konverze výsledku a*b typu double na typ int. Funkce vrátí celou část obsahu obdélníku.

Je vhodné podotknout, že moderní překladače, které mají nastaven překlad podle pravidel jazyka C++, vyžadují vždy uvést typ a ohlásí v tomto případě chybu. Z toho také plyne, že je nutné uvádět typ parametru u každého z nich, nelze tedy psát double obsah_obd(double a, b).

Volání funkce se provádí zápisem identifikátoru a uvedením skutečných parametrů v kulatých závorkách:

int main(int argc, char *argv[])
{
  double a=5.6, b=4;
  double x=1.2, y=3.5;
  double obsah;
  // volani funkce se skutečnými parametry konstantami 3,4
  obsah = obsah_obd(3,4);
  printf("Obsah obdelniku je %lf.\n",obsah);
  // volani funkce se skutečnými parametry - proměnnými a,b
  printf("Obsah druheho obdelniku je %lf.",obsah_obd(a,b));
  // volani funkce se skutečnými parametry - proměnnými x,y
  printf("Obsah tretiho obdelniku je %lf.",obsah_obd(x,y));
}  
Při deklaraci funkce je možné u formálního parametru uvést klíčové slovo const. Deklarujeme tím, že parametr je konstatní a není možné uvnitř těla funkce jeho hodnotu modifikovat; při pokusu o změnu překladač ohlásí chybu. Konstatní parametr má význam zejména při předávání polí, protože zabrání programátorovi např. v nechtěné změně prvků pole. Důležité je uvedení klíčového slova const u knihovních funkcí pracujících s řetězci, tj. s poli typu char, kde není znám kód funkce, pouze její prototyp. Programátor, který takto deklarovanou funkci ve svém programu použije, má jistotu, že obsah řetězce (pole) není ve funkci změněn.

Mezi další vlastnosti definic funkcí v jazyce C platí:

Definice procedur

Formálně v jazyce C procedury neexistují. Definují se jako funkce, které nevracejí žádnou hodnotu. Využívá se klíčového slova void:
void tisk_cisel(int a, int b)
{
  printf("%d\t%d",a,b);
}
Příkaz return není nutné na konci procedur uvádět, pouze v případě předčasného (nuceného) ukončení.

Nemá-li funkce (procedura) žádný parametr, píše se klíčové slovo void do závorek místo seznamu formálních parametrů. Pokud klíčové slovo void chybí, znamená to v „klasickém“ C, že o parametrech není nic známo. V C++ se tyto dva případy nerozlišují, neuvedení klíčového slova znamená taktéž, že funkce parametry nemá.

Procedura nemá žádný parametr O parametrech není nic známo
void tisk_hlasky(void)
{
  printf("CHYBA");
}
void tisk_hlasky()
{
  printf("CHYBA");
}

Při volání funkce (procedury), která nemá parametry, je nutné psát prázdnou dvojici závorek, jinak bude překladač považovat zapsaný identifikátor za indentifikátor proměnné! Volání výše definované procedury musíme provést takto:

int main(void)
{
  tisk_hlasky();
}  

Prototypy funkcí (hlavičky)

Prototypem funkce (hlavičkou) rozumíme uvedení návratové hodnoty, identifikátoru a seznamu parametrů bez těla funkce. Prototypy funkcí jsou např. uvedeny v hlavičkových souborech. Prototyp funkce je třeba uvést, není-li funkce definována před tím, než je poprvé volána. Překladač musí při volání totiž vědět, jakou má funkce návratovou hodnotu a parametry, aby mohl provést kontrolu. Někdy definice kódu funkce ve zdrojové podobě není vůbec k dispozici (některé knihovny existují pouze v přeloženém tvaru .obj, .lib).

Uvedení prototypu funkce funkce se také někdy označuje termínem deklarace, na rozdíl od její definice. Uvedení prototypu je ukončeno středníkem, ale při definici se za identifikátorem středník neuvádí!

Příklad:

Definice funkce před místem volání Definice funkce za místem volání, uvedení prototypu před voláním
double obsah_obd(double a, double b)
{
  return a*b;
}

void main(void)
{
   double S;
   S = obsah_obd(3,4);
}
double obsah_obd(double a, double b);

void main(void)
{
   double S;
   S = obsah_obd(3,4);
}

double obsah_obd(double a, double b)
{
  return a*b;
}

(Poznámka: Ve verzi K&R deklarace funkce spočívala pouze v uvedení návratové hodnoty a identifikátoru funkce bez uvedení parametrů, tedy double obsah_obd(double, double).)


Předávání parametrů funkcím

Skutečné parametry funkcí a procedur jsou v jazyce C předávány pouze hodnotou, tj. hodnota skutečných parametrů je zkopírována na zásobník (parametry se kopírují od posledního). Veškeré operace prováděné ve funkci (proceduře) s parametry se dějí s touto kopií, skutečný parametr zůstane nedotčen. Parametry předávané odkazem neexistují, jejich absence se obchází pomocí ukazatelů (viz dále).

Na následujícím obrázku je příklad jednoduché procedury tisk a odpovídající stav zásobníku těsně před voláním procedury, kdy jsou již na vrcholu zásobníku kopie hodnot skutečných parametrů. Dále je zobrazen stav zásobníku těsně před ukončením volané funkce. Hodnota parametru a na zásobníku je změněna, ale hodnota skutečného parametru x (proměnná v hlavním programu) se nemění. Po návratu z procedury jsou parametry ze zásobníku odstraněny.

Zdrojový kódZásobník před voláním procedury tisk Zásobník před ukončením procedury tisk
void tisk(int a, int b)
{
  a++;
  printf("%d\t%d",a,b);
}

void main(void
{
  int x = 5;
  tisk(x,3);
}

Obrázek 1: Předávání parametrů procedurám a funkcím

Pokud je potřeba změnit v proceduře či funkci hodnotu skutečného parametru (neboli deklarovat výstupní proměnnou procedury, která by v Pascalu byla předávána odkazem a označena jako var parametr), deklaruje se formální parametr typu ukazatel a jako skutečný parametr se předává adresa proměnné. Následující kód ukazuje výpočet obsahu obdélníka jednak pomocí funkce, jednak ve formě procedury s „výstupní“ proměnnou:
// obsah obdelniku jako funkce
double obsah_obd_fce(double a, double b)
{
  return a*b;
}

// obsah obdelniku jako procedura s vystupni promennou
void obsah_obd_proc(double a, double b, double *obs)
{
  *obs = a*b;
}

void main(void)
{
  double obsah;
  // vypocet pomoci funkce
  obsah = obsah_obd_fce(3,5);
  // vypocet pomoci procedury s vystupni promennou
  // musim predat adresu promenne, kam se ulozi vysledek
  obsah_obd_proc(3,7,&obsah);
  // nelze napsat obsah = obsah_obd_proc(3,7,&obsah); 
  // protoze procedura nevraci (nevyhazuje) zadnou hodnotu 
}
Někdy může mít výstupní proměnnou i funkce, tj. vracet de-facto dvě hodnoty. Nastavení hodnoty jiné proměnné mimo návratové hodnoty se často označuje termínem vedlejší, resp. stranový efekt funkce (side effect).

(Poznámka: V jazyce C++ byl přidán další způsob předávání parametrů funkcím, a sice pomocí reference, což odpovídá parametrům předávaným odkazem v jazyce Pascal.)


Pole jako parametr funkcí

Jednorozměrné pole

Jednorozměrné pole (statické i dynamické) je v jazyce C reprezentováno ukazatelem na počátek pole. Z toho vyplývá, že formální parametr, který představuje pole, můžeme deklarovat jako ukazatel na příslušný typ. V jazyce C je také možné deklarovat parametr typu pole jiným způsobem: int pole[]. Ze skutečného parametru pole není známa jeho velikost, proto je nutné předávát zároveň s polem i jeho velikost jako další parametr. Oba způsoby jsou naprosto rovnocené, druhému z nich se dává přednost, protože je z kódu na první pohled zřejmé, že skutečným parametrem má být pole a nikoliv pouze ukazatel na prvek. Možnosti deklarace jsou naznačeny dále v tabulce - zatím bez kódu, konkrétní funkce budou ukázány na příkladech algoritmů řazení.

void razeni(float *pole, int n)
{
  // kód
}
void razeni(float pole[], int n)
{
  // kód
}

Uvedení velikosti pole ve tvaru void razeni(float pole[10]) není možné, překladač hodnotu 10 ignoruje.

Dvourozměrné pole

V případě dvourozměrného pole je situace trochu složitější. Pokud deklarujeme parametr typu pomocí závorek, první dimenzi je možné vynechat, ale druhý rozměr musíme uvést. Překladač kontroluje druhý rozměr skutečného parametru - předáváného statického pole. Předávání dvourozměrných dynamických polí uvedeme později.
float max_prvek(float pole[][3], int n)
{
  // kód
  // jako skutečný parametr mohu předat
  // libovolné statické pole, které má první
  // dimenzi libovolnou a druhou pouze 3
}

[Přednáška 3] [Obsah] [Přednáška 5]