[Cvičení 5] [Obsah] [Cvičení 7]

Cvičení 6


Témata


Dokončení úloh z předchozího cvičení

Dokončete úlohy 5.3 a 5.4 z předchozího cvičení.

Ukázka výpisu adres:

Napíšeme program, který vypíše na obrazovku adresy paměti, kde jsou umístěny jednotlivé položky jednak statického, jednak dynamického pole.

Řešení:

Dev C++:adresy.dev, adresy.c
CodeBlocks:adresy.cbp, adresy.c

Funkce a procedury v jazyce C

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 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));
}  
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ů.)


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). Může jít např. o nastavení chybové proměnné, viz následující ukázka
double obsah_obd(double a, double b, int *chyba)
{

  if (a<0 || b<0) { *chyba = 1; return 0; }
  *chyba = 0;
  return a*b;
}

int main()
{
  int ch;
  double o;
  o = obsah_obd(3,-5,&ch);
  if (ch==1) printf("Zaporne strany!");
}  

(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.)

Úloha 6.1

Napište dvě funkce pro převod úhlu zadaného ve stupních na radiány a naopak. Přepište je také jako procedury s výstupními proměnnými.
Funkce a procedury napište nejprve do jednoho souboru se zdrojovým kódem, potom si vyzkoušejte tvorbu vlastní knihovny a hlavičkového souboru (stačí pouze pro funkce).

Postup, jak vytvořit v překladači Dev C++ a připojit k projektu vlastní knihovnu a hlavičkový soubor, který bude obsahovat zmíňované funkce, si popíšeme.

  1. V hlavní nabídce zvolíme Soubor - Nový - Zdrojový kód. Vytvoří se nový soubor Beze jména 1.
  2. Uložíme si soubor pod názvem uhly.c.
  3. V hlavní nabídce opět zvolíme Soubor - Nový - Zdrojový kód.
  4. Uložíme si soubor pod názvem uhly.h (hlavičkový soubor) .
  5. Do hlavičkového souboru uhly.h napíšeme prototypy funkcí (nezapomeneme na středník):
    float deg2rad(float u);
    float rad2deg(float u);
    
  6. Implementaci napíšeme do souboru uhly.c, resp. zkopírujeme z předchozího programu.
  7. Vytvoříme nový projekt: Soubor - Nový - Projekt a uložíme jej např. pod názvem prevody.
  8. Na začátek hlavního programu vložíme pomocí direktivy hlavičkový soubor: #include "uhly.h"
  9. Do projektu vložíme naši knihovnu: stiskneme pravé tlačítko myši na názvu projektu a zvolíme položku Připojit k projektu.
  10. Přeložíme knihovnu (Zkompilovat tento soubor). Všimněte si, že v adresáři je přeložená knihovna - uhly.o.
  11. Doplníme hlavní program a vyzkoušíme knihovnu.
Pokud si soubory uhly.c a uhly.h, resp. uhly.o uložíme do archivu, můžeme obě funkce použít např. za půl roku v jiném programu, aniž bychom je znovu programovali. V tom je výhoda deklarace procedur a funkcí v knihovnách.

Chceme-li někomu poskytnout naši knihovnu a přitom utajit implementaci (zdrojový kód), poskytneme uživateli pouzé soubory uhly.h a uhly.o. Pak musíme nastavit parametry překladače tak, aby sestavovací program používal při sestavení knihovnu uhly.o. V prostředí Dev C++ to provedeme následovně: Zobrazíme vlastnosti projektu (stiskem pravého tlačítka myši na názvu projektu) a zvolíme Vlastnosti projektu. Zobrazíme kartu Parametry a do posledního editačního okénka Linker přidáme pomocí tlačítka Připojit knihovnu uhly.o

Řešení:

Kompletní kód v jediném souboruDev C++:stupne.dev, stupne.c
 CodeBlocks:stupne.cbp, stupne.c
 
Funkce pro převod v samostatné knihovněKnihovna:uhly.c, uhly.h
 Dev C++:stupne2.dev, stupne2.c
 CodeBlocks:stupne2.cbp, stupne2.c

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
}

Úloha 6.2

Napište funkci, která zjistí, zda je zadaný prvek v neseřazeném poli. Funkce má tři parametry: pole, počet prvků pole a hledaný prvek. Pokud je prvek nalezen, funkce vrátí jeho index, jinak vrátí hodnotu -1. Dále naprogramujte dvě procedury, které seřadí prvky pole. První procedura seřadí pole vzestupně bublinkovým řazením, druhá procedura seřadí pole sestupně výběrem maximálního prvku.

Prezentace s popsanými algoritmy řazení a vyhledávání: algoritmy_vyhledavani_razeni.ppt

Polotovar:
Dev C++:hledani_vzor.dev, hledani_vzor.c
CodeBlocks:hledani_vzor.cbp, hledani_vzor.c
Řešení:
Dev C++:hledani.dev, hledani.c
CodeBlocks:hledani.cbp, hledani.c


[Cvičení 5] [Obsah] [Cvičení 7]