| ← vytváření typů II | C/C++ | dynamická alokace paměti → |
V této kapitole zakončíme výklad syntaxe jazyka C. Podíváme se na ukazatele na funkce a pak si zopakujeme základní deklarace proměnných a funkcí. V jazyce C existují ještě další konstrukce, které jsem nevysvětloval (například jak použít assembler), ale troufám si tvrdit, že s většinou potřebných konstrukcí jsem vás již seznámil. Příklad na konci bude shrnovat většinu probrané látky. Ale ještě neodcházejte. Výklad jazyka C tím zdaleka nekončí. Jazyk C obsahuje řadu standardních knihoven a v dalším výkladu se budeme seznamovat právě s nimi a jejich funkcemi.
Ukazatele na funkce jsem si nechal schválně až na konec, protože snažit se pochopit jejich zápis je opravdu sado maso. Doufám že už dobře zvládáte samotné ukazatele a také ukazatele na ukazatele.
Protože i funkce programu leží kdesi v paměti, existuje adresa, která ukazuje na začátek této funkce. Nyní se konečně dozvíte, proč musí být za jménem funkce ve zdrojovém kódu kulaté závorky, i když nečeká funkce žádné argumenty. Je to proto, že samotné jméno funkce (bez kulatých závorek) je překladačem chápáno jako adresa funkce, tedy nějaké číslo! Takové číslo lze i vytisknout, ale to by asi k ničemu nebylo. Pomocí ukazatele na funkci lze takovou funkci zavolat (a to i s příslušnými argumenty). Můžete tak kupříkladu napsat funkci, která bude mít jako argument ukazatel na jinou funkci, kterou pak může spustit. Podívejte se, jak vypadá deklarace ukazatele na funkci. Jde o deklaraci ukazatele (jmeno) na funkci, která má návratovou hodnotu „typ“. V závorce mohou být uvedené typy očekávaných argumentů funkce.
typ (*jmeno)([typ_arg,..]);
V příkladu níže deklaruji ukazatel uknf, který ukazuje na funkci bez argumentů s návratovou hodnotou „ukazatel na typ char“. Pro porovnání je hned za touto deklarací deklarace oné funkce. Všimněte si, že závorky při deklaraci ukazatele na funkci jsou vždy nevyhnutelné. Kdybychom je v našem příkladu nepoužili, vytvořili bychom deklaraci funkce s návratovým typem „ukazatel na ukazatel na typ char“.
char *(*uknf) (void); /* deklarace ukazatele na funkci, která vrací ukazatel */ char *uknf (void); /* deklarace funkce, která vracejí ukazatel */
V příkladu použijeme novou funkci exit(), která ukončí program v jakémkoliv místě s návratovou hodnotou, která je argumentem funkce exit(). Tato funkce je definována v souboru <stdlib.h>. Tam se o ní také více dočtete.
1: /*------------------------------------------------*/ 2: /* ukafce.c */ 4: #include <stdio.h> 5: #include <stdlib.h> 7: /* nejdrive vytvorime strukturu, ktera bude obsahovat nejake pojmenovani 8: * funkce pro uzivatele programu, cislo funkce, bude zaznamenavat pocet 9: * spusteni funkce a ukazatel na funkci */ 10: typedef struct { 11: unsigned int cislo; 12: char nazev[50]; 13: unsigned int spusteno; 14: void (*ukfce) (unsigned int *, char *); 15: } robot; 17: /* nyni definujeme funkce programu, ktere budou uzivateli nabizeny pomoci 18: * predchozi struktury */ 19: /* zobrazi pouze velka pismena */ 20: void velka_pismena(unsigned int *spusteno, char *v) 21: { 22: int i = 0; 23: (*spusteno)++; 24: do { 25: if ((v[i] >= 'A') && (v[i] <= 'Z')) { 26: printf("%c", v[i]); 27: } 28: } while (v[i++]); 29: printf("\n"); 30: } 32: /* zobrazi pouze mala pismena */ 33: void mala_pismena(unsigned int *spusteno, char *v) 34: { 35: int i = 0; 36: (*spusteno)++; 37: do { 38: if ((v[i] >= 'a') && (v[i] <= 'z')) { 39: printf("%c", v[i]); 40: } 41: } while (v[i++]); 42: printf("\n"); 43: } 45: /* zobrazi vse, co neni pismeno */ 46: void nepismena(unsigned int *spusteno, char *v) 47: { 48: int i = 0; 49: (*spusteno)++; 50: do { 51: if (!(((v[i] >= 'a') && (v[i] <= 'z')) || 52: ((v[i] >= 'A') && (v[i] <= 'Z')))) { 53: printf("%c", v[i]); 54: } 55: } while (v[i++]); 56: printf("\n"); 57: } 59: /* zobrazi text pozpatku */ 60: void obrat_text(unsigned int *spusteno, char *v) 61: { 62: int i = 0; 63: if (*spusteno >= 2) { 64: printf("Pro dalsi pouziti teto funkce se musite registrovat!\n"); 65: return; 66: } 67: (*spusteno)++; 69: while (v[i++]); /* hledame konec retezce */ 71: for (i--; i >= 0; i--) 72: printf("%c", v[i]); 74: printf("\n"); 75: } 77: /* vlozi do textu mezery */ 78: void roztahni(unsigned int *spusteno, char *v) 79: { 80: int i = 0; 81: (*spusteno)++; 82: while (v[i]) { 83: printf("%c ", v[i++]); 84: }; 85: printf("\n"); 86: } 88: /* funkce exit() ma jine parametry, nez ktere ocekava nas ukazatel, proto 89: * jsem vytvoril funkci konec, ktera simuluje potrebu deklarovanych argumentu 90: * */ 91: void konec(unsigned int *spusteno, char *v) 92: { 93: exit(0); /* ukonci program s navratovou hodnotou 0, 94: ktera znaci uspech */ 95: } 98: /* funkce vlozime do pole struktury robot spolu s nazvem pro uzivatele atd. */ 99: robot *inicializuj(void) 100: { 101: /* pokud by promenna "r" nebyla staticka, pak by po skonceni 102: volani funkce prestala existovat! */ 103: static robot r[] = { {1, "Velke pismena", 0, velka_pismena}, 104: {1, "Mala pismena", 0, mala_pismena}, 105: {2, "Co neni pismeno", 0, nepismena}, 106: {3, "Obraceni textu", 0, obrat_text}, 107: {4, "Roztazeni textu", 0, roztahni}, 108: {5, "Ukonceni", 0, konec}, 109: {0, "", 0, NULL} /* "zarazka", podle ktere pozname, ze jsme na konci */ 110: }; 111: return r; 112: } 115: int main(void) 116: { 117: int i, navrat; 118: robot *rb; /* ukazatel na pole struktury robot */ 119: unsigned int cislo; /* funkce jsou ve strukture ocislovany, 120: dle tohoto cisla budou vybirany */ 121: char retezec[51]; /* maximalne 50 znaku + '\0' */ 123: rb = inicializuj(); /* funkce inicializuj() vraci ukazatel na svou 124: statickou promennou. At ji zavolate kolikrat 125: chcete, bude ukazovat stale na to same pole. 126: O tom, jak vytvaret nove promenne za behu programu 127: si povime pozdeji. */ 128: do { 129: i = 0; 130: do { 131: /* zobrazeni menu - snadne a prehledne */ 132: printf("%2i)\t%s\t(%2i)\n", rb[i].cislo, rb[i].nazev, 133: rb[i].spusteno); 134: } while (rb[++i].ukfce != NULL); 136: printf("Vyber polozku dle cisla a zadejte retezec: "); 138: /* vysledek prirazeni muzeme pouzit jako hodnotu 139: (tak ho rovnou porovname s EOF) */ 140: if ((navrat = scanf("%u %50s", &cislo, retezec)) == EOF) 141: break; 142: else if (navrat != 2) { /* chybne nacteni polozek */ 143: printf("\n Chyba vstupu (%i)!\n", navrat); 144: /* Zrejme nebylo zadano jako prvni cislo, ale retezec. 145: * Ten se musi nyni nacist, jinak by se jej predchozi funkce 146: * scanf pokousela v cyklu neustale nacitat jako cislo a tim 147: * by cyklus nikdy neskoncil 148: * Promenna retezec muze obsahnout maximalne 50 znaku, proto 149: * funkci scanf v prvnim argumentu urcime max. delku 150: * nacitaneho retezce. */ 151: scanf("%50s", retezec); 152: continue; 153: } 154: i = -1; 155: while (rb[++i].ukfce != NULL) { /* hledame spravnou strukturu, dokud 156: nenarazime na nami vytvorenou zarazku */ 157: if (rb[i].cislo == cislo) { 158: rb[i].ukfce(&rb[i].spusteno, retezec); 159: /* prochazi se cele pole, takze se spusti vsechny polozky v 160: * poli, ktere maji rb[].cislo == cislo */ 161: /* kdyby zde byl prikaz break, provedla by se prvni nalezena 162: * polozka a dale by se jiz nic neprohledavalo */ 163: } 164: } 165: } while (1); /* nekonecny cyklus, ukonci se jen pomoci return nebo 166: break (nebo exit ukonci program) */ 168: printf("\nAstalavista !\n"); 169: return 0; 170: } 172: /*------------------------------------------------*/
Možný výstup z programu:
1) Velke pismena ( 0) 1) Mala pismena ( 0) 2) Co neni pismeno ( 0) 3) Obraceni textu ( 0) 4) Roztazeni textu ( 0) 5) Ukonceni ( 0) Vyber polozku dle cisla a zadejte retezec: 1 VelkaAMalaPismena VAMP elkaalaismena 1) Velke pismena ( 1) 1) Mala pismena ( 1) 2) Co neni pismeno ( 0) 3) Obraceni textu ( 0) 4) Roztazeni textu ( 0) 5) Ukonceni ( 0) Vyber polozku dle cisla a zadejte retezec: 5 konec
Program má jeden drobný nedostatek, a to, že při volání funkce k ukončení programu (je pod číslem 5) musíte zadat za číslo 5 ještě nějaký zbytečný řetězec. Určitě vás napadne spousta možností, jak se tohoto nedostatku zbavit. Důležitější je, jak snadno se do takového programu přidá další funkce. Stačí jí jen napsat a ve funkci inicializuj() jí přidat do pole struktury robot. Nic víc se na programu měnit nemusí. Ukazatel na funkci se tak může s výhodou použít v programech, které načítají funkce z knihoven. Při vytvoření funkce stačí jen změnit nebo vytvořit novou knihovnu pro program (a ne hned překládat celý program). Program si jí načte do paměti a pak s ní může snadno pracovat pomocí ukazatele, jako by ji měl odedávna.
Zde se můžete v přehledu podívat na deklarace, kterým by jste měli rozumět.
| Deklarace | Význam |
|---|---|
| typ jmeno; | Proměnná určeného typu |
| typ *jmeno; | Ukazatel na určitý typ (respektive pole daného typu) |
| typ jmeno[]; | Konstantní ukazatel na pole daného typu (neznámo jak dlouhé) |
| typ jmeno[10]; | Konstantní ukazatel na pole daného typu o velikosti 10-ti proměnných daného typu. |
| typ **jmeno; | Ukazatel na pole ukazatelů daného typu |
| typ *jmeno[]; | Konstantní ukazatel na pole ukazatelů daného typu |
| typ jmeno[][]; | Konstantní ukazatel na pole konstantních ukazatelů na pole daného typu. |
| typ jmeno(); | Deklarace funkce vracející typ |
| typ *(jmeno()); | Funkce vracející ukazatel na typ. Zvýrazněné závorky jsou nadbytečné. |
| typ (*jmeno)() | Ukazatel na funkci bez parametrů vracející typ |
| typ *(*jmeno)() | Ukazatel na funkci bez parametrů vracející ukazatel na typ |
Lze vytvářet i daleko složitější deklarace. Například
unsigned long *(*jmeno[5][4])(char *);
je dvojrozměrné pole obsahující ukazatele na funkci, jejíž návratová hodnota je ukazatel na typ unsigned long a argumentem této funkce je ukazatel na typ char. Rozluštit takovéto zápisy není zrovna legrace. Proto doporučuji využívat typedef pro zjednodušení takovýchto konstrukcí. Předchozí proměnnou jmeno lze definovat takto:
typedef unsigned long *(*ukazatel_na_fci1)(char *); ukazatel_na_fci1 jmeno[5][4];
Je na čase si zopakovat probranou látku. Program, který je zde popsán obsahuje většinu probrané látky. Měli by jste být schopni nejenom takovýto zdrojový kód přečíst a pochopit, ale i sami napsat. Programování se naučíte nejlépe tím, že si budete vymýšlet vlastní příklady (stále těžší a těžší) a programovat je. Snad sem vás dokázal během výuky dostatečně inspirovat.
Naprogramujeme si chůzi opilce. Program si přečte z příkazové řádky pravděpodobnosti, s jakými půjde opilec vpřed a vzad. Pravděpodobnost, že zůstane stát si dopočte program sám (zbytek do 100%). Jelikož na příkazové řádce jsou i čísla chápána jako text, použijeme funkci atoi() ze standardní knihovny na převod řetězce na číslo typu int. Po spuštění programu bude mít navíc uživatel možnost vybrat si z několika zobrazení opilce. Jakým způsobem se bude opilec zobrazovat se pokusíme naprogramovat tak, jako by šlo o modul programu. Proto funkce pro zobrazování opilce budou v jiném souboru, než hlavní program a budou se volat pomocí ukazatele na funkce.
Začneme třemi hlavičkovými soubory: "dos1.h", "windows1.h" a "unix1.h". Tyto soubory jsme již použili v kapitole o uživatelských knihovnách. Jsou v nich deklarovány funkce implementačně závislé.
1: /*------------------------------------------------*/ 2: /* dos1.h */ 3: #include <dos.h> 5: void cekej(int cas) 6: { 7: delay(cas); 8: } 10: /*------------------------------------------------*/
1: /*------------------------------------------------*/ 2: /* windows1.h */ 3: #include <windows.h> 5: void cekej(int cas) 6: { 7: Sleep(cas); 8: } 10: /*------------------------------------------------*/
1: /*------------------------------------------------*/ 2: /* unix1.h */ 3: #include <unistd.h> 5: void cekej(unsigned long cas) 6: { 7: usleep(cas); 8: } 10: /*------------------------------------------------*/
V souboru "define1.h" definujeme potřebné datové typy a makra.
Tyto datové typy a makra se budou používat i v dalších knihovnách,
proto je třeba v hlavním programu načíst tuto knihovnu
(pomocí #include) jako jednu z prvních.
Výčtový typ smer
bude sloužit k určování směru chůze opilce, struktura
pohyb
bude obsahovat pravděpodobnosti, s jakou půjde opilec vpřed, vzad
nebo zůstane stát a také pozici, kde opilec právě stojí. Všimněte
si, že položka stop nebude ani využita.
Lze jí dopočítat do 100% pomocí položek vpred a vzad.
Je otázkou, zda je lepší vytvořit takovou položku, do které se hodnota
jednou uloží a pak už se jen používá, nebo kdykoliv je v programu potřeba,
tak se vypočte výrazem (100-vpred-vzad).
Vzhledem k tomu, že by se tento výraz musel v programu vícekrát počítat
(což program zpomaluje) a i tento výraz zabírá v programu místo (instrukce
pro výpočet), je výhodnější položku stop
použít.
V našem příkladě se však hodnota stop
nepoužije vůbec, takže je opravdu zbytečná.
1: /*------------------------------------------------*/ 2: /* define1.h */ 4: #define DELKACHUZE 40 6: typedef enum { 7: dopredu, dozadu, stat 8: } smer; 10: typedef struct { 11: int vpred, vzad, stop; 12: int pozice; 13: } pohyb; 14: /*------------------------------------------------*/
V souboru "zobraz1.h" jsou definovány všechny funkce, kterými
se zobrazuje pohyb opilce. Všimněte si, jak makra
TISKNIOPILCE
pokračují za zpětným lomítkem na druhé řádce.
Ukazatele na tyto funkce jsem uložil do pole
zobraz. To, kterou funkci budeme
volat, nechám v programu na náhodě. Některé funkce jsem tam tak
vložil záměrně vícekrát, aby byla větší pravděpodobnost, že budou
vybrány. Naproti tomu jsem tam jednu funkci nevložil vůbec.
1: /*------------------------------------------------*/ 2: /* zobraz1.h */ 4: #include <stdio.h> 5: #include <stdlib.h> 7: #define TISKNIOPILCER(ZNK) printf("\r%3i%% (%*s%*c %3i%% (%3i)",p->vzad,\ 8: p->pozice, ZNK, DELKACHUZE - p->pozice,')',p->vpred,pocet); 9: #define TISKNIOPILCEN(ZNK) printf("\n%3i%% (%*s%*c %3i%% (%3i)",p->vzad,\ 10: p->pozice, ZNK, DELKACHUZE - p->pozice,')',p->vpred,pocet); 12: void zobraz_opilce1(pohyb *, const smer1), const int); 13: void zobraz_opilce2(pohyb *, const smer, const int); 14: void zobraz_opilce3(pohyb *, const smer, const int); 15: void zobraz_opilce4(pohyb *, const smer, const int); 17: void (*zobraz[]) (pohyb *, const smer, const int) = { 18: zobraz_opilce1, 19: zobraz_opilce2, zobraz_opilce3, zobraz_opilce1, zobraz_opilce3}; 21: void zobraz_opilce1(pohyb * p, const smer s, const int pocet) 22: { 23: switch (s) { 24: case dopredu: 25: p->pozice++; 26: TISKNIOPILCER("->") 27: break; 28: case dozadu: 29: p->pozice--; 30: TISKNIOPILCER("<-") 31: break; 32: case stat: 33: TISKNIOPILCER("<>") 34: break; 35: }; 36: } 38: void zobraz_opilce2(pohyb * p, const smer s, const int pocet) 39: { 40: switch (s) { 41: case dopredu: 42: p->pozice++; 43: TISKNIOPILCEN("->") 44: break; 45: case dozadu: 46: p->pozice--; 47: TISKNIOPILCEN("<-") 48: break; 49: default: 50: TISKNIOPILCEN("<>") 51: break; 52: }; 53: } 55: void zobraz_opilce3(pohyb * p, const smer s, const int pocet) 56: { 57: switch (s) { 58: case dopredu: 59: p->pozice++; 60: TISKNIOPILCER(">>") 61: break; 62: case dozadu: 63: p->pozice--; 64: TISKNIOPILCER("<<") 65: break; 66: default: 67: TISKNIOPILCER("><") 68: break; 69: }; 70: } 72: void zobraz_opilce4(pohyb * p, const smer s, const int pocet) 73: { 74: switch (s) { 75: case dopredu: 76: p->pozice++; 77: printf("\rPOZICE: %2i%*c", p->pozice++, DELKACHUZE + 10, ' '); 78: break; 79: case dozadu: 80: p->pozice--; 81: printf("\rPOZICE: %2i%*c", p->pozice++, DELKACHUZE + 10, ' '); 82: break; 83: default: 84: printf("\rPOZICE: %2i%*c", p->pozice++, DELKACHUZE + 10, ' '); 85: break; 86: }; 87: } 89: /*------------------------------------------------*/
A konečně se dostáváme k srdci programu, souboru opakov1.c.
1: /*------------------------------------------------*/ 2: /* opakov1.c */ 4: #include <stdio.h> 5: #include <stdlib.h> 7: #ifdef unix 8: #include "unix1.h" 9: #define VERZE "UNIX" 10: #define CEKEJ 100000ul 11: #elif defined __MSDOS__ 12: #define VERZE "MSDOS" 13: #include "dos1.h" 14: #define CEKEJ 100 15: #elif defined __WINDOWS__ || defined __WIN16__ || defined __WIN32__ || defined __WIN64__ 16: #include "windows1.h" 17: #define VERZE "WINDOWS" 18: #define CEKEJ 100 19: #endif 22: /* V souboru define1.h jsou nadefinovany datove typy, ktere se pouzivaji i v 23: * souboru zobraz1.h. Proto musi byt #include "define.h" pred 24: * #include "zobraz1.h" */ 26: #include "define1.h" 27: #include "zobraz1.h" 29: /* Pokusime se nacist cisla z prikazove radky a pote zkontrolujeme, zda maji 30: * rozumne hodnoty. Pokud ne, program ukoncime */ 32: pohyb nacti_procenta(int argc, char *argv[]) 33: { 34: pohyb p = { 0, 0, 0, DELKACHUZE / 2 }; 35: if (argc != 3) { 36: printf("Spatny pocet argumentu\n" 37: "Zadejte pravdepodobnost chuze vpred a vzad.\n"); 38: exit(0); 39: } 40: p.vpred = atoi(argv[1]); 41: p.vzad = atoi(argv[2]); 42: p.stop = 100 - p.vpred - p.vzad; 44: if (p.stop < 0) { 45: printf("Chybne zadane pravdepodobnosti\n"); 46: printf("Jejich soucet nesmi prekrocit 100\n"); 47: exit(0); 48: } 50: if (!(p.vpred || p.vzad)) { 51: printf("Pravdepodobnost vpred nebo vzad musi byt nenulova\n"); 52: exit(0); 53: } 54: if ((p.vpred < 0) || (p.vzad < 0)) { 55: printf("Pravdepodobnosti musi byt kladne!\n"); 56: exit(0); 57: } 58: return p; 59: } 61: /* nahodne vybirame smer pohybu opilce */ 62: smer vyber_smer(const pohyb * p) 63: { 64: int x; 65: x = (rand() % 100); /* tj. 0 - 99 */ 66: if (x <= p->vpred) 67: return dopredu; 68: if (x <= p->vpred + p->vzad) 69: return dozadu; 70: return stat; 71: } 74: int main(int argc, char *argv[]) 75: { 76: pohyb opilec; 77: int pocet_funkci, fce, pocet; 78: opilec = nacti_procenta(argc, argv); 79: /* Diky tomu, ze nahodne cisla inicializujeme "nenahodne", pak pri stejne 80: * zadanych procentech se bude program chovat stejne. Lepe je funkci 81: * srand inicializovat treba na zaklade aktualniho casu */ 82: srand(opilec.vpred * opilec.vzad); 83: /* NULL je ukazatel "do nikam". Jako takovy ma stejnou velikost jako 84: * kterykoliv ukazatel, vcetne ukazatele na funkci. 85: * Tj. sizeof(NULL) = sizeof(char *) atd. */ 86: pocet_funkci = sizeof(zobraz) / sizeof(NULL); 88: pocet = 0; 89: do { 90: pocet++; 91: fce = rand() % pocet_funkci; /* vybirame nahodne funkci na zobrazeni */ 92: zobraz[fce] (&opilec, vyber_smer(&opilec), pocet); 93: fflush(stdout); /* vyprazdneni standardniho vystupu pred pozastavenim */ 94: cekej(CEKEJ); 95: } while ((opilec.pozice > 2) && (opilec.pozice < DELKACHUZE - 1)); 97: printf("\n"); 98: return 0; 99: } 101: /*------------------------------------------------*/
Možný výstup z programu:
opakov1 30 50 50% ( << ) 30% ( 8) 50% ( >> ) 30% ( 17) 50% ( <> ) 30% ( 24) 50% ( << ) 30% ( 27) 50% ( -> ) 30% ( 28) 50% ( << ) 30% ( 38) 50% ( <> ) 30% ( 43) 50% ( >> ) 30% ( 45) 50% ( <- ) 30% ( 49) 50% ( <- ) 30% ( 52) 50% ( << ) 30% ( 64) 50% ( << ) 30% ( 70) 50% (<- ) 30% ( 72)
| ← vytváření typů II | C/C++ | dynamická alokace paměti → |
