Obsah
Nejprve trocha teorie. Program se skládá z instrukcí, také říkáme kódu, a dat. Instrukce musí procesor vykonávat, k datům pak bude přistupovat tehdy, když mu to instrukce nařídí. Teď musíme pouze určit, jak procesor pozná, kterou další instrukci má vykonat, kde najde potřebnou proměnnou, přesněji její hodnotu? Pozná to podle adresy. Ukazatel je název proměnné, která obsahuje adresu. Prostřednictvím této adresy ukazuje na data.
Adresa je klíčem pro procesor, kde má brát instrukce nebo data. Překladač vyššího programovacího jazyka, například jazyka C, za nás vykoná všechnu potřebnou práci, když se jedná o adresy instrukcí.
Potřebné znalosti | |
K zvládnutí obsahu této kapitoly je nutné mít znalosti z kapitoly 2 až 8 - rozumět dříve probírané problematice jazyka C, chápat činnosti preprocesoru, provádění podmíněného předkladu a psaní maker. |
O ukazatelích jsme se už zmínili v kapitole 3 konkrétně v části 3.10. Nyní se k problematice ukazatelů ještě vrátíme. Pro začátek trocha opakování.
A jak tedy změníme hodnotu ukazatele? K tomu vystačíme již se známou "hvězdičkou". Jí rozšířený ukazatel ovšem musí být vlevo od přiřazení.
Pár příkladů pro procvičení.
Příklad: | |
int x, y, *px, *p2x; /* *px, *p2x jsou pointery (ukazatele) na int */ px = &x; /* px nyní ukazuje na x */ *px = 5; /* totéž co x = 5; */ y = *px + 1; /* y = x + 1; */ *px += 1; /* x += 1; */ (*px)++; /* x++; závorky jsou nutné */ p2x = px; /* p2x ukazuje na totéž, co px, tj. na x */ *p2x = *p2x + y; /* x = x + y; */ |
Při deklaraci ukazatele můžeme použít i modifikátor const. Pak se jedná o konstantní ukazatel, ukazatel na konstantu, nebo obojí, tedy konstantní ukazatel na konstantu. Tyto konstrukce se zejména používají při tvorbě nebo použití knihovních funkcí, kdy je například zdůrazněno (a překladačem kontrolováno), že argument funkce nebude v žádném případě modifikován. Opět pár příkladů.
Příklad: | |
int i; int *pi; /* pi je neinicializovaný ukazatel na typ int */ int * const cp = &i; /* konstantní ukazatel na int */ const int ci = 7; /* celočíselná konstanta */ const int *pci; /* neinicializovaný ukaz. na celoč. konst. */ const int * const cpc = &ci; /* konst. uk. na konstantu */ |
V jazyku C je jednorozměrné pole seznamem proměnných, které jsou všechny stejného typu a přistupuje se k ním přes společné jméno. Jednotlivá proměnná v poli se nazývá prvek pole. Pole umožňuje pohodlně zpracovávat skupinu souvisejících dat.
Pro deklaraci pole se používá obecný formát:
kde:
Klasický příklad deklarace celočíselného pole o 20 znaků.
Příklad: | |
#define ROZSAH 20 int pole[ROZSAH]; |
Prvek pole se získává indexováním pole pomocí čísla prvku. V jazyku C začínají všechna pole nulovým indexem. To znamená, že chceme-li pracovat s prvním prvkem pole, zadáme jako index nulu. Pro indexování pole zadáme index požadovaného prvku do hranatých závorek.
Příklad: | |
pole[0], pole[1], ... , pole[19] |
Paměťový prostor (velikost paměti) přidělený poli pole můžeme zjistit výpočtem, v němž počet prvků pole ROZSAH násobíme velikostí jednoho prvku, kterou zjistíme pomocí operátoru sizeof, jehož argumentem je požadovaný datový typ:
počet obsazených byte = ROZSAH * sizeof(int)
Nebo naopak, počet prvků pole:
pole = sizeof(pole) / sizeof(int)
Jazyk C ukládá pole do souvislé oblasti paměti. První prvek pole má nejnižší adresu. Po provedení této části programu:
Příklad: | |
int i[5]; int j; for(j=0; j<5; j++) i[j]=j; |
Bude pole i vypadat takto:
i[0] hodnota 0 i[1] hodnota 1 i[2] hodnota 2 i[3] hodnota 3 i[4] hodnota 4
Jazyk C netestuje indexy polí, ale umožňuje současně s definicí pole provést i jeho inicializaci, tedy nastavení počátečních hodnot:
Příklad: | |
double cisla[5] = {1.22, 0.4, -1.2, 12.3, 4.0}; |
Princip je jednoduchý. Před středník, jímž by definice končila, přidáme "rovnítko" a do složených závorek vložíme seznam hodnot, jimiž chceme inicializovat prvky pole. Hodnoty pochopitelně musí být odpovídajícího typu. Navíc jich nemusí být stejný počet jako je dimenze pole. Může jich být méně. Přiřazení totiž probíhá následovně: první hodnota ze seznamu je umístěna do prvního prvku pole, druhá hodnota do druhého prvku pole, ... Pokud je seznam vyčerpán dříve, než je přiřazena hodnota poslednímu prvku pole, zůstávají odpovídající (zbývající) prvky pole neinicializovány. Výjimkou jsou globální a statické proměnné, které jsou inicializovány nulou.
Při inicializaci popsané výše jsme museli uvádět dimenzi pole. Tuto práci ovšem můžeme přenechat překladači. Ten poli vymezí tolik místa, kolik odpovídá inicializaci.
Příklad: | |
int pole[] = {21, 14, 55}; |
Překladač nekontroluje rozsah použitého indexu. Pokud se o tuto skutečnost nepostaráme sami, můžeme číst nesmyslné hodnoty při odkazu na neexistující prvky pole. |
Po probrané teorii si práci s poli vyzkoušíme na několika příkladech.
Naším prvním úkolem bude naplnit pole sqr druhými mocninami čísel 1 až 10 a pak je vypsat.
Naším dalším úkolem bude napsat program, který si bude v poli uchovávat zadávané teploty. Nakonec vypíše nejteplejší den (den s nejvyšší teplotou), nejstudenější den (den s nejnižší teplotou) a průměrnou měsíční teplotu.
Jak jsme si již uváděli, ukazatel ukazuje na hodnotu nějakého typu. Nese s sebou informaci o tomto typu. Tato informace pochopitelně představuje počet bytů nutný pro uchování hodnoty typu. A při aritmetice ukazatelů smysluplně používáme obě části zmíněné informace, adresu i velikost položky.
Sčítání i odčítání jsou binární operace. Protože se zabýváme aritmetikou ukazatelů, bude jedním z operandů vždy ukazatel. Druhým operandem pak bude buď opět ukazatel (Týká se pouze operace odčítání dvou ukazatelů), nebo jím může být celé číslo. Pokud jsou oba operandy ukazatele, je výsledkem jejich rozdílu počet položek, které se mezi adresami, na něž ukazatele ukazují, nacházejí. Pokud k ukazateli přičítáme, respektive odčítáme celé číslo, je výsledkem ukazatel ukazující o příslušný počet prvků výše, respektive níže, představíme-li si prvky s vyšším indexem nad prvky s nižším indexem. Tak je ale pole v C definováno.
Příklad: | |
int i, *pi, a[N]; |
adresa prvku a[0] je &a[0], což je totéž jako a + 0. Tento výraz již představuje součet ukazatele s celočíselnou hodnotou. V našem případě je celočíselná hodnota rovna 0. Z toho vyplývá že platí i:
Příklad: | |
a + i <=> &a[i] *(a+i) <=> a[i] |
Pozn: | |
znak <=> znamená ekvivalenci, což čteme jako "což je totéž". |
Výrazy uvedené níže jsou po tomto přiřazení zaměnitelné (mají stejný význam, představují hodnotu prvku pole a s indexem i).
Příklad: | |
a[i] <=> *(a+i) <=> pi[i] <=> *(pi+i) |
Chceme-li se například dostat na prostřední prvek u pole majícího 20 znaků, použijeme tento zápis s pomocí ukazatele pi:
Příklad: | |
<screen> pi = a + 9; </screen> |
S tímto ukazatelem se pak můžeme posunout na následující prvek pole třeba pomocí inkrementace, neboť i u ní překladač ví, o kolik bajtů má posunout (zvětšit) adresu, aby ukazoval na následující prvek: ++pi nebo pi++, ale ne ++a ani a++, neboť a je konstantní ukazatel (je pevně spojen se začátkem pole).
A na závěr si opět ukážeme příklad. Funkce printf() umožňuje zobrazit adresu paměti obsaženou v ukazateli pomoci specifikátoru %p. Tuto schopnost můžeme použít pro předvedení ukazatelové aritmetiky. Ukážeme si, jak ukazatelová aritmetika závisí na základním typu ukazatele.
Hodnoty obsažené v jednotlivých proměnných se mohou lišit v závislosti na překladači, ale přesto uvidíme, že adresa ukazující na c bude zvýšena o jeden byte. Ostatní budou inkrementovány o počet bytů jejich základních typů. V 16 bitovém prostředí to bude typicky 2 pro int, 4 pro float a 8 pro double.
Příklad definice pole:
Příklad: | |
char retezec[SIZE]; |
Na identifikátor retezec můžeme nahlížet takto:
Řetězcové konstanty píšeme mezi dvojici uvozovek, uvozovky v řetězcové konstantě musíme uvodit speciálním znakem \, nazývaným "zpětné lomítko".
Příklad: | |
|
Platí tedy, že následující zápisy jsou ekvivalentí:
Příklad: | |
char pozdrav[] = "ahoj"; char pozdrav[] = {'a','h','o','j',0}; |
Mějme nyní tuto definici:
Příklad: | |
char *ps = "retezec"; |
Jde o velice podobnou definici, ale přesto se podstatně liší. ps je ukazatel na znak, jemuž je přiřazena adresa řetězcové konstanty retezec. Tato konstanta je tedy umístěna v paměti, ale proměnná ps na ni ukazuje jen do okamžiku, než její hodnotu třeba změníme. Pak ovšem ztratíme adresu konstantního řetězce "retezec".
Následující příklad demonstruje práci s řetězci.
world world world world hello world llo world
Hodnota řetězcové konstanty je stejná jako hodnota jakéhokoliv ukazatele na řetězec (a nejen na řetězec) - ukazatel na první položku.
Při práci s řetězci nesmíme zapomínat na skutečnost, že jsou reprezentovány ukazateli. Pouhou změnou či přiřazením ukazatele se samotný řetězec nezmění.
Řetězce zřejmě budeme v našich programech používat velmi často. Naštěstí jsou již funkce pro práci s řetězci napsány a nemusíme si je psát sami. Jsou součástí standardní knihovny funkcí a jejich prototypy jsou obsaženy v hlavičkovém souboru string.h.
Syntaxe:
Jazyk C umožňuje deklarovat pole pouze jednorozměrné. Jeho prvky ovšem mohou být libovolného typu. Mohou tedy být opět (například) jednorozměrnými poli. To však již dostáváme vektor vektorů, tedy matici. Budeme-li uvedeným způsobem postupovat dále, vytvoříme datovou strukturu prakticky libovolné dimenze. Pak již je třeba mít jen dostatek paměti. A operační systém který nám ji umí poskytnout.
Příklad definici matice:
Příklad: | |
type jmeno[4][5]; |
Kde:
Grafické zobrazení takového dvourozměrného pole včetně indexů jednotlivých prvků je následující:
S vícerozměrným polem můžeme pracovat stejně jako s jednorozměrným. To se přirozeně týká i možnosti inicializace. Jen musíme jednotlivé "prvky" vektory, uzavírat do složených závorek. Tento princip je ovšem stejný jako pro vektor.
Nyní si na příkladu ukážeme jak vypsat dvou rozměrné pole 4x5, které bude vyplněno součiny indexů.
Jedná se často o ukazatele nebo o pole ukazatelů. Funkce ovšem nejen vracejí hodnotu jistého typu, ale mohou mít i různý počet argumentů různého typu.
Definujme si například ukazatel na funkci, která nemá argumenty a vrací hodnotu zvoleného datového typu:
Příklad: | |
typ (*jmeno)(); |
Závorky okolo identifikátoru jmeno a úvodní hvězdičky jsou nutné, neboť zápis
představuje funkci vracející ukazatel na typ.
Příkladem použití ukazatelů na funkce může být knihovní funkce qsort().
Syntaxe:
void qsort(void *base, size_t nelem, size_t width, int (*fcmp)(const void *, const void *));
Kde:
Pozn: | |
Prototyp funkce qsort() je umístěn v hlavičkovém souboru
stdlib.h.
|
Funkce qsort() je napsána obecně a tak nemá žádné konkrétní informace o typech hodnot, které třídí. Tuto část řeší naše uživatelská funkce, která správně porovnává hodnoty.
Možný výstup:
Trideni qsort trvalo 0.26s pole je setrideno
Vzhledem k tomu, že ukazatel je proměnná jako každá jiná, můžeme na ni ukazovat nějakým jiným (dalším) ukazatelem. Ve vícerozměrném poli provádíme v podstatě totéž, jen jednotlivé vektory nejsou pojmenované. Přistupujeme k nim pomocí bázové adresy základního pole a indexu. Z tohoto pohledu jsou opravdu nepojmenované. Lze říci, že pole ukazatelů je v jazyce C nejen velmi oblíbená, ale i velmi často používaná konstrukce.
Nejlépe si ukazatele procvičíme na příkladu. Tentokrát půjde o program, který lexikograficky setřídí jména měsíců v roce. Pro vlastní třídění jsme použili funkci qsort().
Výstup:
setrideno: 0. brezen 1. cerven 2. cervenec 3. chyba 4. duben 5. kveten 6. leden 7. listopad 8. prosinec 9. rijen 10. srpen 11. unor 12. zari
Následující program používá dvojrozměrné pole ukazatelů pro vytvoření tabulky řetězců, která sdružuje odrůdy jablek s jejich barvami. Pro použití programu zadejte název odrůdy a program vypíše její barvu.
Prohlédněte si pozorně podmínku řídící cyklus for. Výraz *p[i][0] nabývá hodnoty prvního bytu i-tého řetězce. Jelikož je seznam ukončen nulovým řetězcem, bude tato hodnota při dosazení konce tabulky nulová (nepravdivá). Ve všech ostatních případech bude nenulová a cyklus se bude opakovat.
Mnoho programů umožňuje zadávat při jejich spuštění argumenty na příkazovém řádku. Argument příkazového řádku je informace, která následuje za jménem programu na příkazovém řádku operačního systému.
Samozřejmě i programy napsané v jazyce C mohou využívat argumenty příkazového řádku. Ty se předávají do programu pomocí dvou argumentů funkce main(). Tyto argumenty se nazývají argc a argv a jak jste již mohli odhadnout jsou tyto argumenty nepovinné.
Syntaxe:
Kde:
Pozn: | |
Je dobrým zvykem popsané argumenty funkce main() pojmenovat
argc a argv, kde
arg znamená argument, přípona c
znamená count a přípona v znamená
value.
|
Jazyk C nespecifikuje, co tvoří argument příkazového řádku, protože jednotlivé operační systémy se v tomto bodě od sebe značně liší. Nejčastější konvence zní: Každý argument příkazového řádku musí být oddělen mezerou nebo tabulátorem. Čárky, středníky a podobně nejsou považovány za oddělovač.
Toto je test
Je tvořen třemi řetězci, ale
Toto,je,test
je jeden řetězec.
Potřebujeme-li předat argument příkazového řádku který obsahuje mezery, musíme jej vložit do uvozovek, jak ukazuje následující příklad.
"Toto je test"
Nyní si napišme příklad, který vypíše své argumenty příkazového řádku.