Ukazatele na funkce
V této kapitole zakončím výklad syntaxe jazyka C (no fakt, už je koneec! :D). Vysvětlím vám ukazatele na funkce a pak si zopakujete 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, jejichž znalost je pro programování v C zásadní. V dalším výkladu se s nimi budete seznamovat.
Ukazatele na funkce
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 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 typu typ. V závorce mohou být uvedené typy očekávaných argumentů funkce.
Kulaté závorky kolem jména funkce a hvězdičky jsou nutné, jinak by to vypadalo jen jako
deklarace funkce, která má jako návratový typ typ *
.
V příkladu níže deklaruji ukazatel uknf, který ukazuje na funkci bez argumentů s návratovou hodnotou „ukazatel na typ char“. Pro srovnání máte hned za touto deklarací deklaraci oné funkce. Všimněte si, že závorky při deklaraci ukazatele na funkci jsou vždy nevyhnutelné. Kdybych je v tomto příkladu nepoužil, vytvořil bych deklaraci funkce s návratovým typem „ukazatel na ukazatel na typ char“.
V příkladu použiji 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í dočtete více.
/* c17/ukafce.c */
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#ifdef _MSC_VER
#define SZU "lu"
#else
#define SZU "zu"
#endif
/* nejdrive vytvorim strukturu, ktera bude obsahovat pojmenovani
* funkce pro uzivatele programu, cislo funkce, bude zaznamenavat
* pocet spusteni funkce a bude obsahovat ukazatel na funkci */
typedef struct {
unsigned int cislo;
char nazev[50];
unsigned int spusteno;
void (*ukfce) (unsigned int *, char *);
} robot;
/* nyni definuji funkce programu, ktere budou uzivateli nabizeny
* za pomoci predchozi struktury */
/**
* zobrazi pouze velka pismena ASCII tabulky
*/
void velka_pismena(unsigned int *spusteno, char *v)
{
size_t i = 0;
(*spusteno)++;
do {
if ((v[i] >= 'A') && (v[i] <= 'Z')) {
printf("%c", v[i]);
}
} while (v[i++]);
printf("\n");
}
/**
* zobrazi pouze mala pismena ASCII tabulky
*/
void mala_pismena(unsigned int *spusteno, char *v)
{
size_t i = 0;
(*spusteno)++;
do {
if ((v[i] >= 'a') && (v[i] <= 'z')) {
printf("%c", v[i]);
}
} while (v[i++]);
printf("\n");
}
/**
* zobrazi vse, co neni pismeno
*/
void nepismena(unsigned int *spusteno, char *v)
{
size_t i = 0;
(*spusteno)++;
do {
if (!(((v[i] >= 'a') && (v[i] <= 'z')) ||
((v[i] >= 'A') && (v[i] <= 'Z')))) {
printf("%c", v[i]);
}
} while (v[i++]);
printf("\n");
}
/**
* zobrazi text pozpatku
*/
void obrat_text(unsigned int *spusteno, char *v)
{
size_t i = 0;
if (*spusteno >= 2) {
printf("Pro dalsi pouziti teto funkce se musite registrovat!\n");
return;
}
(*spusteno)++;
while (v[++i]); /* hledam konec retezce */
for (; i > 0; i--)
printf("%c", v[i-1]);
printf("\n");
}
/**
* vlozi do textu mezery
*/
void roztahni(unsigned int *spusteno, char *v)
{
size_t i = 0;
(*spusteno)++;
while (v[i]) {
printf("%c ", v[i++]);
};
printf("\n");
}
/**
* Funkce exit() ma jine parametry, nez ktere ocekava nas ukazatel,
* proto jsem vytvoril funkci konec, ktera simuluje potrebu
* deklarovanych argumentu
*/
void konec(unsigned int *spusteno, char *v)
{
/* ukonci program s navratovou hodnotou 0, ktera znaci uspech */
exit(0);
}
/**
* funkce vlozim do pole struktury robot
* spolu s nazvem pro uzivatele atd.
*/
robot *inicializuj(void)
{
/* pokud by promenna "r" nebyla staticka, pak by po skonceni
volani funkce prestala existovat! */
static robot r[] = {
{1, "Velke pismena", 0, velka_pismena},
{1, "Mala pismena", 0, mala_pismena},
{2, "Co neni pismeno", 0, nepismena},
{3, "Obraceni textu", 0, obrat_text},
{4, "Roztazeni textu", 0, roztahni},
{5, "Ukonceni", 0, konec},
{0, "", 0, NULL} /* "zarazka", podle ktere poznam,
ze jsem na konci pole */
};
return r;
}
int main(void)
{
size_t i; int navrat;
/** ukazatel na pole struktury robot */
robot *rb;
/**
* funkce jsou ve strukture ocislovany,
* dle tohoto cisla budou vybirany */
size_t cislo;
/* maximalne 50 znaku + '\0' */
char retezec[51];
/* funkce inicializuj() vraci ukazatel na svou
statickou promennou. At ji zavolate kolikrat
chcete, bude ukazovat stale na to same pole.
O tom, jak vytvaret nove promenne za behu programu
bude rec v dalsi kaptile pozdeji. */
rb = inicializuj();
do {
i = 0;
do {
/* zobrazeni menu - snadne a prehledne */
printf("%2i)\t%s\t(%2i)\n", rb[i].cislo, rb[i].nazev, rb[i].spusteno);
} while (rb[++i].ukfce != NULL);
printf("Zadejte cislo z menu a retezec: ");
/* vysledek prirazeni muzeme pouzit jako hodnotu
(tak ho rovnou porovname s EOF) */
if ((navrat = scanf("%" SZU " %50s", &cislo, retezec)) == EOF)
break;
else if (navrat != 2) { /* chybne nacteni polozek */
printf("\n Chyba vstupu (%i)!\n", navrat);
/* Zrejme nebylo zadano jako prvni cislo, ale nejaky retezec.
* Ten se musi nyni nacist, jinak by se jej predchozi funkce
* scanf pokousela v cyklu neustale nacitat jako cislo a tim
* by cyklus nikdy neskoncil.
* Promenna retezec muze obsahnout maximalne 50 znaku, proto
* funkci scanf v prvnim argumentu urcime max. delku
* nacitaneho retezce. */
scanf("%50s", retezec); // nacteme "smeti"
continue;
}
i = 0;
/* hleda se sruktura uzivatelem zadaneho cisla */
while (rb[i].ukfce != NULL) {
if (rb[i].cislo == cislo) {
/* VOLANI FUNKCE PRES UKAZATEL */
rb[i].ukfce(&rb[i].spusteno, retezec);
/* prochazi se cele pole, takze se spusti vsechny polozky v
* poli, ktere maji rb[].cislo == cislo */
/* kdyby zde byl prikaz break, provedla by se prvni nalezena
* polozka a dale by se jiz nic neprohledavalo */
}
i++;
}
} while (1); /* nekonecny cyklus, ukonci se jen pomoci return nebo
break (nebo exit ukonci program) */
printf("\nAstalavista baby!\n");
return 0;
}
/*------------------------------------------------*/
Makro _CRT_SECURE_NO_WARNINGS
je tu kvůli funkci scanf()
,
viz scanf().
Důvod definování SZU
viz datový typ pro ukazatel.
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()
ji přidat do pole
struktury robot. Nic víc se na programu
měnit nemusí.
Ukazatel na funkci se 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. (Vytváření knihoven se budu věnovat až v části o programování v Linuxu.)
Přehled známých deklarací
Zde se můžete v přehledu podívat na deklarace, kterým byste měli rozumět. (Zopakujte si alespoň Ukazatele a pole.)
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 ukazatel na daný typ |
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:
Toto 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. Sranda, ne? :D
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:
To už je určitě daleko čitelnější. A myslím, že vám to pomůže i lépe pochopit předchozí zápis.
Pokud si nebudete někdy nějakým zápisem jistí, můžete využít program cdecl:
$ cdecl Type `help' or `?' for help cdecl> explain unsigned long *(*jmeno[5][4])(char *); declare jmeno as array 5 of array 4 of pointer to function (pointer to char) returning pointer to unsigned long
Opakování – příklad
Je na čase si zopakovat probranou látku. Program, který je zde popsán obsahuje většinu probrané látky. Měli byste 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žiji
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ím 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čnu třemi hlavičkovými soubory: "dos1.h", "windows1.h" a "unix1.h". Tyto soubory jsem již použil v kapitole o uživatelských knihovnách. Jsou v nich deklarovány funkce implementačně závislé.
V souboru "define1.h" definuji 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 zajistit, aby se nenačítal obsah tohoto soboru vícekrát
(pomocí makra _DEFINE1_H
, viz zdrojový kód).
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 asi výhodnější položku stop
použít.
V tomto příkladě se však hodnota stop
nepoužije vůbec, takže je opravdu zbytečná.
- /*------------------------------------------------*/
- /* c17/define1.h */
- #ifndef _DEFINE1_H
- #define _DEFINE1_H 1
- #define DELKACHUZE 40
- dopredu, dozadu, stat
- } smer;
- int pozice;
- } pohyb;
- #endif
- /*------------------------------------------------*/
V souboru "zobraz1.h"1) 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, která funkce se bude
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.
- /*------------------------------------------------*/
- /* c17/zobraz1.h */
- #include <stdio.h>
- #include <stdlib.h>
- #include "define1.h"
- #define TISKNIOPILCER(ZNK) printf("\r%3i%% (%*s%*c %3i%% (%3i)",p->vzad,\
- p->pozice, ZNK, DELKACHUZE - p->pozice,')',p->vpred,pocet);
- #define TISKNIOPILCEN(ZNK) printf("\n%3i%% (%*s%*c %3i%% (%3i)",p->vzad,\
- p->pozice, ZNK, DELKACHUZE - p->pozice,')',p->vpred,pocet);
- zobraz_opilce1, zobraz_opilce2, zobraz_opilce3,
- zobraz_opilce1, zobraz_opilce3
- };
- {
- case dopredu:
- p->pozice++;
- TISKNIOPILCER("->")
- case dozadu:
- p->pozice--;
- TISKNIOPILCER("<-")
- case stat:
- TISKNIOPILCER("<>")
- };
- }
- {
- case dopredu:
- p->pozice++;
- TISKNIOPILCEN("->")
- case dozadu:
- p->pozice--;
- TISKNIOPILCEN("<-")
- TISKNIOPILCEN("<>")
- };
- }
- {
- case dopredu:
- p->pozice++;
- TISKNIOPILCER(">>")
- case dozadu:
- p->pozice--;
- TISKNIOPILCER("<<")
- TISKNIOPILCER("><")
- };
- }
- {
- case dopredu:
- p->pozice++;
- case dozadu:
- p->pozice--;
- };
- }
- /*------------------------------------------------*/
A konečně k srdci programu, souboru opakov1.c.
- /*------------------------------------------------*/
- /* c17/opakov1.c */
- #include <stdio.h>
- #include <stdlib.h>
- #ifdef unix
- #include "unix1.h"
- #define VERZE "UNIX"
- #elif defined __MSDOS__
- #define VERZE "MSDOS"
- #include "dos1.h"
- #elif defined __WINDOWS__ || defined __WIN16__ || defined __WIN32__ || defined __WIN64__ || defined _MSC_VER
- #include "windows1.h"
- #define VERZE "WINDOWS"
- #endif
- #define CEKEJ 100
- #include "define1.h"
- #include "zobraz1.h"
- /* Pokusim se nacist cisla z prikazove radky a pote zkontroluji,
- * zda maji rozumne hodnoty. Pokud ne, program ukoncim */
- {
- pohyb p = { 0, 0, 0, DELKACHUZE / 2 };
- "Zadejte pravdepodobnost chuze vpred a vzad.\n");
- }
- p.stop = 100 - p.vpred - p.vzad;
- }
- }
- }
- return p;
- }
- /**
- * nahodne vybere smer pohybu opilce
- */
- {
- int x;
- return dopredu;
- return dozadu;
- return stat;
- }
- {
- pohyb opilec;
- opilec = nacti_procenta(argc, argv);
- /* Diky tomu, ze nahodne cisla inicializuji "nenahodne", pak
- * pri stejne zadanych procentech se bude program chovat stejne.
- * Lepe je funkcisrand inicializovat treba na zaklade aktualniho
- * casu */
- /* NULL je ukazatel "do nikam". Jako takovy ma stejnou velikost
- * jako kterykoliv ukazatel, vcetne ukazatele na funkci.
- * Tj. sizeof(NULL) = sizeof(char *) atd. */
- pocet = 0;
- do {
- pocet++;
- /* vybiram nahodne funkci na zobrazeni */
- zobraz[fce] (&opilec, vyber_smer(&opilec), pocet);
- /* vyprazdneni standardniho vystupu pred pozastavenim */
- cekej(CEKEJ);
- }
- /*------------------------------------------------*/
Možný průběh 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)
1) const
je zkratka pro const int
, viz zobraz1.h