Operační systém UNIX a jazyk C

Jan Brodský
Luděk Skočovský

SNTL Nakladatelství technické literatury Praha 1989, ISBN 80-03-00049-1

5. Programovací jazyk C


5.6 Funkce a jejich volání

Funkce jsou tak důležitým mechanismem jazyka C, že si zaslouží speciální pozornost. Definici funkcí jsme probrali již v odst. 5.4.6, nyní se soustředíme na mechanismus volání funkce a vnitřní strukturu těla funkce.

5.6.1 Mechanismus volání a návratu

Volání funkce je primitivní výraz (odst. 5.3.2) sestávající z výrazu typu funkce vracející objekt, popř. neprázdného seznamu skutečných argumentů oddělených čárkami, např.

empty();
je voláním funkce empty bez argumentu; po provedení příkazu
z = f(x, 3.141592);
bude mít z hodnotu, kterou vrátí funkce f pro skutečné argumenty x a 3.141592.

Volání funkce zajistí předání argumentů hodnotou a předání řízení do těla funkce, které tak obdrží lokální kopie hodnot argumentů. Předání argumentů může být chápáno tak, že v těle funkce tvoří formální parametry automatické proměnné, které jsou lokální v těle funkce a jsou inicializovány hodnotou skutečných argumentů.

Tak volání

max(1, 2)

funkce definované takto:	      se provede jako blok:

int max(a,b)
int a,b;			{ int a=1, b=2;
{				if(a>b)
if(a>b)					return(a);
	return(a);		else
else					return(b);
	return(b);		}
}

Návrat z funkce zajišťuje příkaz return, popř. s uvedenou hodnotou. Tato hodnota se po případné konverzi na požadovaný typ vrátí jako hodnota volané funkce.

Není-li typ funkce v definici uveden, je implicitně považován za int. Pro vyjádření faktu, že funkce nevrací hodnotu (tedy že jde o proceduru), slouží klíčové slovo void, použité v definici funkce místo typu vrácené hodnoty. Například procedura nuluj pro nulování pole celých čísel je definována tak, že

void nuluj(a,n)
int a[], n;
{ register i;
	if(n <= 0)
		return;
	else
		for(i = 0; i < n; i++)
			a[i]=0;
}
nevrací hodnotu. Příkaz return použitý v jejím těle nemůže tedy hodnotu vrátit. Návrat z funkce se rovněž provede přechodem přes uzavírající závorku bloku těla funkce. Hodnota funkce není v tomto případě definována.

Je chybou použít volání procedury jako podvýraz, např.

k = nuluj(b,10);

Funkce vracející hodnotu by tedy měla opustit tělo pouze příkazem return s uvedenou hodnotou. Těchto příkazů může být v těle funkce více.

5.6.2 Skutečné argumenty

Skutečnými argumenty jsou výrazy a podléhají automaticky prováděným konverzím: char a short se převedou na int, float na double. Jiné automatické konverze se neprovádějí. Identifikátor pole se chápe jako ukazatel na první objekt pole. To mj. znamená, že se pole jako celek nedá předat (hodnotou), předává se (hodnotou) adresa prvního prvku. V předchozím příkladu tedy tělo procedury nuluj nemůže změnit skutečný argument - ukazatel na prvek a[0], ale může změnit celé pole, jehož prvky jsou prostřednictvím ukazatele přístupné.

Struktury a unie se předávají (hodnotou) jako celek. To skýtá možnost předat pole jako celek (hodnotou), je-li jedinou složkou struktury.

Funkce nemohou být předávány jako skutečné argumenty, ukazatel na funkci však ano. Obvyklý postup bude patrný z příkladu volání funkce h, která pro své účely využívá dvě další funkce f a g:

double x, f(), g();
  ...
if(x > 0.0)
  h(f,x);
else
  h(g,x);
  ...

Funkce f a g musí zde být předdeklarovány, protože ve volání funkce h se vyskytují pouze jejich identifikátory bez okrouhlých závorek, které by kompilátoru naznačily, že jde o identifikátor funkce (vracející objekt typu double).

Definice funkce h může vypadat takto:

h(pf,d)
double(*pf)(), d;
{
  ...
  return((*pf)(d));
}
pf je ukazatel na funkci vracející double a funkce, na niž ukazuje, je v těle h volána s argumentem d. Tělo funkce h je pro x > 0 provedeno jako blok:
{double(*pf)() = f, d = x;
...
return((*pf)(d));
}

Korespondence formálních parametrů a skutečných argumentů je dána pořadím, v němž jsou uvedeny v definici a ve volání.

Nejčastější chybou je, že nesouhlasí typ formálního parametru a skutečného argumentu. Pro separátně kompilované funkce kompilátor ani nemůže provést potřebnou kontrolu (viz však čl. 5.9). Příčin konfliktu však může být víc (nesouhlas vlastnosti objektu, nesouhlas signed nebo unsigned apod.)

V C rovněž není mechanismus na zvládnutí proměnného počtu skutečných argumentů. Avšak např. funkce printf (čl. 5.7) využívá první argument (formát výstupu) k určení počtu a typu dalších použitých argumentů. Nesouhlasí-li formát s dalšími skutečnými argumenty, nelze o tvaru výstupu předem nic tvrdit.

Pořadí vyhodnocování skutečných argumentů není definováno. Způsob, který používá (zleva doprava, zprava doleva) konkrétní kompilátor, se dá zjistit např. takto:

void p(z)
char z;
{
  printf("%c",z);	/* vystup jednoho znaku */
}

void test(a,b,c)
  char a,b,c;
{			/* prazdne telo */
}

main()
{
  test(p('a'), p('b'), p('c'));
  p('\n');
}
Vypíše se buď text abc, nebo cba (popř. jiné pořadí).

5.6.3 Rekurzivita funkcí

Každému volání funkce přísluší jeho aktivační záznam, tj. čerstvá kopie skutečných argumentů a objektů lokálních v bloku, který je tělem funkce. Ve funkci tak může být volána táž funkce buď přímo, nebo zprostředkovaně přes další funkce. Klasickým případem je funkce flib vracející n-tý člen Fibonacciho posloupnosti:

unsigned long fib(n)
unsigned long n;
{
	if(n>1)
		return(fib(n-1) + fib(n-2));
	else
		return(n);
}
Předpokládáme přitom, že fib(0) = 0, fib(1) = 1; n nemůže být záporné, protože je specifikováno jako unsigned.

5.6.4 Příkaz goto, návěští

Jazyk C se až do tohoto okamžiku mohl jevit jako dokonalý nástroj pro strukturované programování. Tento dojem je pokažen příkazem goto, ale je mírně napraven tím, že cíl skoku, tj. místo, kam se předá řízení, musí být v těle téže funkce jako příkaz goto. Cíl skoku musí být vyznačen návěštím, které je tvořeno identifikátorem, za nímž následuje dvojtečka:

 ...
goto nav3;
 ...
nav3: i++; /* cíl skoku */
 ...

Používání příkazu goto je problematické už proto, že umožňuje předat řízení, "skočit" dovnitř bloku:

main()
{ int i=5;
  goto nav;
  { int i=10;
  nav: printf("i=%d\n", i);
  }
}
Oproti očekávání se nevytiskne 5 ani 10, protože ve volání funkce printf se předává hodnota proměnné i deklarované (implicitně jako automatická) ve vnitřním bloku. Skokem na návěští nav se le "přeskočí" její inicializace. Hodnota vnější proměnné i, jejíž inicializace se provedla, není ve vnitřním bloku známa. Vyzkoušejte! (Vytiskne se i=0).