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 | czytane 44783×
RELATED ARTICLES
KOMENTARZE
Funkce jako typ a proměnná
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;
}
Modifikátory proměnných
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
můžeme tuto proměnnou používat z jiného souboru, pokud v něm deklarujeme
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
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
tj. čekej, dokud signál nebo jiné vlákno nezmění hodnotu i, smí překladač
"vyoptimalizovat" na
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.
Pokračování příště
V příštím dílu se naučíme psát hlavičkové soubory.