V dnešním díle probereme podrobněji proměnné. Ukážeme si, jak se definují ukazatele na funkce a na co jsou dobré. Dojde na modifikátory proměnných a to i v souvislosti s projekty z více souborů.
23.2.2005 15:00 | Jan Němec | přečteno 44464×
Funkci již umíme definovat a volat, umíme také deklarovat jejich hlavičky. Céčko navíc umožňuje definovat ukazatel na funkci. Jedná se skutečně o ukazatel do paměti, označuje místo v kódu, kde začíná přeložená funkce. S ukazatelem na funkci lze pracovat jako s běžnou proměnnou, můžeme do něj přiřadit nějakou hodnotu nebo jej předat jako parametr funkce, lze také vypsat jeho hodnotu. Pokud architektura umožňuje číst programu vlastní kód (běžně ano), můžeme ukazatel na funkci přetypovat na ukazatel na unsigned char a naučit se strojový kód. Pamětníci z dob MS-DOSu zavzpomínají i na sebemodifikaci spuštěného kódu, ale v chráněném módu na Linuxu podobné pokusy skončí špatně. Především však lze funkci, na niž ukazatel míří zavolat.
/* p je ukazatel na funkci s parametrem typu const char * a vracející int */ int (* p)(const char *); /* Identifikátor funkce bez kulatých závorek znamená ukazatel na funkci */ p = puts; printf("Funkce puts je v paměti na adrese %p.\n", (void *) p); /* Ukazatel na puts lze normálně zavolat */ p("Ahoj světe!");
Nejsložitější na celém příkladu je určitě konstrukce typu ukazatele. Nejdřív se píše typ návratové hodnoty funkce, potom v kulatých závorkách hvězdička a identifikátor - jméno definovaného ukazatele a nakonec následují opět v kulatých závorkách typy parametrů funkce. Všimněte si, že název funkce je v C kódu pouze konstantní ukazatel na tuto funkci. Volání funkce přes ukazatel, definovaný jako proměnná, je už stejné, jako běžné volání funkce.
Ukazatele na funkce se nejčastěji používají v souvislosti s knihovnami, kdy je třeba nějakým dostatečně obecným způsobem parametrizovat algoritmus z knihovny. Pomocí ukazatele na funkci lze říci nějaké GUI knihovně, co se má stát při stisknutí tlačítka (zavolat uživatelskou funkci předanou jako parametr). Časově náročné funkce z knihoven občas umožní zadat jako parametr funkci, která se bude jednou za čas během výpočtu volat. Například programátor obecně užitečné knihovny na násobení velkých matic neví, jakým způsobem (a zda vůbec) informovat uživatele během výpočtu. To ví až aplikační programátor. Proto programátor knihovny přidá jako poslední parametr funkce na násobení matic ukazatel na funkci (tu už píše aplikační programátor), která se zavolá po vypočtení každého prvku výsledné matice. V grafické aplikaci se pak může pohybovat teploměr od 0% ke 100%, v interaktivním konzolovém programu se bude vypisovat na standardní výstup atd., ačkoli oba programy používají stejnou knihovnu.
#include <stdio.h> /* Tohle je jako časově náročná funkce z knihovny */ void NapisStoTecek(void (* callback)(int)) { int i; for (i = 0; i < 100; i++) { /* Jednou za deset teček zavolej parametr */ if (!(i % 10)) callback(i); /* Vypisuj tečky */ putchar('.'); } } /* Funkce volaná z knihovny, ale definovaná v aplikačním kódu */ void parametr(int procent) { printf("\nHotovo na %i%%\n", procent); } /* Hlavní program */ int main(void) { NapisStoTecek(parametr); return 0; }
Když nepočítáme parametry funkcí, zná Céčko dva druhy proměnných. Lokální, definované uvnitř funkce, a globální definované mimo funkci. Lokální proměnná má vyhrazené místo na zásobníku, a je tedy svázána nejen s konkrétní funkcí, ale přímo s její instancí. V případě rekurze tak pro každé volání funkce vzniknou i instance jejích lokálních proměnných a přiřazení do lokální proměnné proto ovlivní hodnotu pouze příslušné instance. Toto chování lze změnit klíčovým slovem static. Lokální proměnná definovaná jako static má vždy jen jednu instanci uloženou stejně jako globální proměnné, navíc její případná inicializace v definici proběhne jen jednou.
void funkce(void) { static int i = 0; printf("Celkový počet volání funkce = %i\n", ++i); }
U globálních proměnných má static poněkud jiný význam. Jedná se o omezení platnosti proměnné na příslušný modul. Z ostatních souborů je pak tato proměnná nedostupná, jde tedy o jakousi ochranu identifikátorů podobně jako v případě static funkcí.
Globální proměnná z jiného zdrojového souboru se zpřístupní pomocí extern. Pokud v jednom souboru definujeme
int pocet;
můžeme tuto proměnnou používat z jiného souboru, pokud v něm deklarujeme
extern int pocet;
Jde tedy jen (podobně jako v případě hlaviček funkcí) o jakousi informaci pro překladač, že pocet je známý identifikátor, proměnná typu int, která je definovaná někde jinde. Pokud v celém projektu není nikde definovaná bez extern, ale je používaná, projde vlastní překlad jednotlivých souborů, ale při linkování dojde k chybě.
Pokud nám záleží na efektivitě programu, můžeme označit lokální proměnnou jako register.
void funkce(void) {
register int i;
/* ... */
}
Je to doporučení pro překladač, aby umístil tuto proměnnou do registru procesoru a nikoli na zásobník. Práce s registry je pochopitelně podstatně rychlejší. Překladač může toto doporučení ignorovat (u dnešních překladačů běžné) a může také proměnnou dát do registru sám od sebe (i to je obvyklé). Označení register má tedy smysl pro dobrovolně spolupracující překladače se špatnou optimalizací, například staré dosovské verze Borland C++.
Překladač se obvykle pokouší kód zjednodušit tak, aby na cílovém procesoru běžel co nejrychleji. V tom mu podstatně pomáhá znalost hodnot proměnných, například po
i = 5;
může předpokládat, že v proměnné i bude skutečně hodnota 5 a to až do nějakého příkazu, který to může změnit. Problém nastane u programů s obsluhou asynchronních událostí (signály) nebo více vlákny. Cyklus
int i = 0;
while (!i);
tj. čekej, dokud signál nebo jiné vlákno nezmění hodnotu i, smí překladač "vyoptimalizovat" na
while (1);
tj. čekej do nekonečna. Z tohoto důvodu existuje modifikátor volatile, který překladači zakazuje provádět jakékoli předpoklady o hodnotě proměnné.
volatile int i = 0;
while (!i);
Teď je příklad správný, pouze poněkud neefektivní, neboť aktivní čekání zbytečně vytíží procesor.
Modifikátor const již známe. Jde o označení proměnných, které nelze jednoduchým způsobem měnit. Nejedná se však o skutečné konstanty, neboť je nelze použít např. při definici mezí polí. Tento modifikátor je rozumné použít zejména u vstupních parametrů funkce předávaných pomocí ukazatelů. Je-li parametrem funkce const char *, je zřejmé, že funkce nemá v úmyslu řetězec měnit. Užití const tak zvyšuje čitelnost programu.
V příštím dílu se naučíme psát hlavičkové soubory.