Distanční opora předmětu: Programování v jazyce C Tématický blok č. 4: Pole a ukazatele Autor: RNDr. Jan Lánský, Ph.D. Obsah kapitoly 1 Pole 1.1 Přístup k prvkům pole 1.2 Vícerozměrné pole 1.3 Inicializace pole 2 Ukazatel 2.1 Neinicializovaný a nulový ukazatel 2.2 Ukazatel na ukazatel 2.3 Příklady: ukazatele 2.4 Ukazatel na prvek pole 2.5 Aritmetika ukazatelů 2.6 Příklady: pole a ukazatele Studijní cíle Cíle nutné k zahájení studia dalšího tématického bloku Znát klíčové pojmy tohoto tématického bloku (alespoň pasivní znalost je podmínkou pro studium dalších bloků). Podle slovního zadání umět napsat a odladit jednoduchý program pracující se vstupem předem známé velikosti (s použitím polí). Schopnost přistupovat k prvkům pole nejen přes operátor hranatých závorek, ale i s použitím operátoru dereference. Důsledné používání nulového ukazatele. Další cíle Znalost vícerozměrných polí. Znalost inicializace pole.
Čas potřebný ke studiu 2 - 4 hodiny na prostudování výukových textů + zodpovězení otázek k rekapitulaci 3 - 8 hodiny na vypracování modelových úloh na PC 1 - 3 hodiny na praktické zopakování učiva na PC ( v jiný den) 30 min - 1 hodina na (znovu)zodpovězení otázek k rekapitulaci (v jiný den) 1 hodina na vypracování úlohy POT 2, která je formou testu Časy jsou hodně individuální a jsou závislé na míře znalostí z předmětu Úvod do programování a Programování a případných programátorských zkušenostech z jiných jazyků.
Úvod V tomto bloku probereme následující témata. Naučíme se deklarovat pole, přistupovat k jeho prvkům. Naučíme se deklarovat pole inicializované počátečními hodnotami. Zmíníme se i o vícerozměrných polích. Seznámí se s ukazateli, vysvětlíme si jejich vzájemný vztah s polem. Ukážeme si jaké chyby mohou nastat při nesprávném používání ukazatelů a jak lze těmto chybám alespoň částečně předcházet používáním nulového ukazatele. Učivo si ukážeme na velkém množství příkladů. Ukážeme si, čím lze nahradit operátor hranatých závorek. Výkladová část Vysvětlivky Červený text – Porušením nebo opomenutím takto označených pravidel vznikají těžko odladitelné chyby (zejména pro začínající programátory). Modrý text – Doporučení jak programovat v praxi. Často prevence závažných chyb. 1 Pole Pole je datová struktura, která uchovává předem daný počet prvků stejného datového typu. Každý z prvků v poli má danou svoji pozici, kterou nazýváme index prvku. K jednotlivým prvkům pole můžeme přistupovat pomocí indexu v konstantním čase. Proměnná typu pole se deklaruje uvedením datového typu (tohoto typu budou prvky obsažené v poli) následovaného jménem proměnné a hranatými závorkami, v nichž je uvedena velikost pole. Například zápis int x[5] deklaruje pole celých čísel, které se jmenuje x, a které obsahuje 5 prvků. První prvek v poli má index 0, poslední prvek má index o jedna menší, než je velikost pole. V případě pole o velikosti 5 prvků, toto pole obsahuje prvky s indexy 0 až 4. K prvkům pole se přistupuje pomocí operátoru hranatých závorech, v nichž uvedeme index prvku pole, se kterým chceme pracovat. Například zápisem x[4] přistoupíme k prvku s indexem 4, což je pátý prvek pole (v případě našeho pole zároveň i prvek poslední). Velikost pole se zadává při jeho deklaraci, a nejde nikdy v budoucnu změnit. Tuto velikost si musíme jako programátoři pamatovat, neexistuje žádná funkce, která by nám ji znovu řekla. Velikost pole musí být známá už v průběhu kompilace, nejčastěji velikostí bývá přímo konstanta, ale může to být i libovolný konstantní výraz. Kompilátor musí vědět kolik paměti má poli přidělit. V žádném případě nemůže být velikost pole ovlivněna hodnotou, kterou uživatel programu zadá za jeho běhu. V poli po jeho deklaraci jsou jeho jednotlivé prvky neinicializované, obsahují v sobě relativně náhodná data, stejná jaká obsahují ostatní lokální proměnné. Deklarací pole jsme získali
nějaký kus paměti, ale obsah této paměti se samotným aktem deklarace nemění, zůstávají zde staré hodnoty, z našeho pohledu náhodná data. 1.1 Přístup k prvkům pole K prvkům pole se přistupuje pomocí operátoru hranatých závorech, v nichž uvedeme index prvku pole, se kterým chceme pracovat. Při přístupu k prvku pole, se nikdy nekontroluje správnost indexu prvku. Kompilátor si pamatuje pouze adresu začátku pole, a podle indexu prvku, který uvedeme, vypočítává adresu paměti, na které by se prvek pole měl nacházet. Můžeme se pokusit přistoupit k prvku s naprosto libovolným indexem, včetně indexů záporných a vždy bude takto vypočítané místo v paměti (adresa začátku pole + index) prohlášeno, za místo, kde se nachází námi hledaný prvek. Předpokládejme, že jsme se v indexu spletli jen o malou hodnotu, například zkusíme pracovat s prvkem s indexem -1 nebo prvkem těsně následujícím za koncem pole. V případě čtení hodnoty prvku pole získáme pouze nesmyslnou hodnotu, což obvykle stačí k tomu, aby náš program nepracoval správně. V případě zápisu do tohoto prvku pole máme šanci způsobit rozsáhlejší škody, například přepsat hodnotu jiné lokální proměnné. Ladit takový program je velmi těžké, protože neočekávané změny libovolné proměnné si je těžké všimnout. Neobvyklé chování programuje se začne projevovat až po vykonání velkého množství dalších příkazů, občas dojde i k ukončení programu operačním systémem. Pokud se spleteme v indexu pole hodně, například budeme chtít přistupovat k prvku s indexem 108, adresa tohoto prvku bude mimo adresný prostor našeho programu. Při pokusu z takové adresy číst nebo na ni zapisovat bude náš program násilně ukončen operačním systémem s vypsáním hlášky: Program provedl neplatnou operaci a proto bude ukončen. Na slajdu č. 50 nahoře vidíme příklad na červeném podkladu, ve kterém pomocí cyklu jednotlivým prvkům pole přiřazujeme nějakou hodnotu. V příkladu chybně předpokládáme, že pole má prvky s indexy 1 až 5, místo 0 až 4. Napravo vidíme graficky znázorněný výsledek našeho příkladu. První prvek pole (s indexem 0) v sobě uchovává neznámou náhodnou hodnotu, v průběhu cyklu nebyl modifikován. Mnohem větší problém působí zápis do prvku s indexem 5. Poslední platný index prvku v tomto poli je 4. Zápis do prvku s indexem 5 znamená přepsání paměti, které poli nepatří a může budoucnu způsobit chybný výsledek běhu programu, nebo dokonce jeho násilné ukončení operačním systémem. 1.2 Vícerozměrné pole Zatím jsme pracovali s jednorozměrným (= jednodimenzionálním) polem. Pole se deklaruje uvedením datového typu, názvu proměnné a poté pro každou dimenzi se uvede v hranatých závorkách její velikost. Například zápisem int x[8][8] jsme vytvořili dvojdimenzionální pole (matici) s velikosti dimenzí 8 a 8. Z hlediska syntaxe (tvorby dalších dimenzí) se dimenze se číslují směrem zprava doleva, ale pro praktické příklady s maticemi nedělá problémy si je číslovat zleva doprava. Teoreticky lze tímto postupem vytvořit pole libovolné dimenze, ale málokdy je pro řešení praktických příkladů potřeba větší dimenze než 2. K prvkům pole přistupujeme uvedením názvu proměnné a poté pro každou dimenzi pole uvedeme hranatou závorku a v ní hodnotu příslušné souřadnici (index), podobným způsobem
jako se deklarovala velikost jednotlivých dimenzí. Například zápisem x[0][5] přistoupíme k prvku pole s indexem 0 v druhé dimenzi a indexem 5 v první dimenzi. Při přístupu k prvkům vícedimenzionálního pole nejde použít zápis s jednou hranatou závorkou, kde souřadnice by byly odděleny čárkami (ani jinými znaménky). Zápis x[0,5] se vyhodnotí jako x[5] tedy přístup k prvku s indexem 5 v jednodimenzionálním poli, protože zápis 0,5 se vyhodnotí jako použití operátoru sekvence (tématický blok č. 3, kapitola 2.8), tedy hodnotou 5. Na slajdu č. 50 dole vidíme příklad, ve kterém jednotlivým prvkům pole přiřadíme hodnotu rovnou součinu jejich souřadnic. Všimněme si podmínek v obou cyklech (řídící proměnná je menší než 8), které zaručují, že indexem prvků nepřekročíme hranice pole (0 až 7). 1.3 Inicializace pole Podobně jako ostatní proměnné lze i pole při deklaraci inicializovat. Inicializace pole při deklaraci se provádí přidáním znaménka = a ve složených závorkách uzavřeným výčtem prvků pole, navzájem oddělených čárkou. Je velmi důležité nezaměňovat tuto inicializaci při deklaraci s obyčejným přiřazením. Inicializaci jde provést pouze při deklaraci proměnné, tedy pouze jednou na začátku práce s proměnnou a nikoliv, kdykoliv v průběhu práce s proměnnou jako přiřazení. Při deklarace inicializovaného pole nemusíme na rozdíl od normální deklarace uvádět jeho velikost, kompilátor si ji sám dopočítá. Hranaté závorky mohou být tedy prázdné. U vícerozměrných polí se dopočítá pouze rozměr poslední dimenze (uvedené při deklaraci nejvíce nalevo). Pokud se však rozhodneme velikost přesto uvést, může nastat situace, že námi uvedená velikost je menší než skutečný počet prvků, to je syntaktická chyba. Pokud námi uvedená velikost je větší než počet skutečný počet prvků, vytvoří se pole této větší velikosti, obsah chybějících prvků je náhodný. Na slajdu č. 51 vidíme několik příkladů. V prvním z nich vytváříme jednorozměrné pole celých čísel, v druhém z nich jednorozměrné pole textových řetězců a ve třetím případě vytváříme dvojrozměrné pole celých čísel. U dvojrozměrného pole musíme určit velikost první vnitřní dimenze (při deklaraci té vpravo), dopočítá se pouze rozměr druhé vnější dimenze (při deklaraci té vlevo). Dvojrozměrné pole se chová jako pole polí, tedy jeho inicializace obsahuje jako jednotlivé prvky celá jednodimenzionální pole. 2 Ukazatel Každá proměnná má své místo v paměti, kde je uložena její hodnota. Každé místo v paměti má svoji adresu. Ukazatel (= pointer) je taková proměnná, ve které je uložena adresa nějakého jiného místa v paměti. Na slajdu č. 52 je tato problematika graficky znázorněna. Je zde proměnná s hodnotou 1 a poté ukazatel (menší čtvereček, ze kterého vede šipka), který v sobě obsahuje adresu této proměnné. Pro jazyk C je práce s ukazateli typická. Jsou potřeba i k tak základní činnosti, jako je práce s řetězci. Umožňují také funkcím, aby předávaly své parametry odkazem. Nezbytné jsou pro programování dynamických datových struktur (spojový seznam, strom, graf,…)
Obyčejná proměnná se deklaruje uvedením datového typu a názvu proměnné. Například zápis int x deklaruje proměnnou x typu int. Ukazatel má vždy svůj typ, může v sobě uchovávat pouze adresy proměnných (obecně míst v paměti) jednoho datového typu. Ukazatel se deklaruje uvedením názvu typu následovaného symbolem * (hvězdička) a názvem proměnné. Například zápis int * p deklaruje proměnnou p typu ukazatel na int, v této proměnné můžeme mít uloženy adresy proměnných typu int. Pokud chceme deklarovat více ukazatelů na stejný typ v jedné deklaraci, musíme před každým názvem proměnné typu ukazatel znova uvést symbol *. Zápis int *p, *q deklaruje dva ukazatele na typ int, zatímco zápis int *p, q deklaruje ukazatel p na typ int a obyčejnou proměnnou q typu int. Adresu proměnné získáme pomocí prefixního operátoru reference & (ampersand). Například zápis p = &x znamená, že do proměnné p (typu ukazatel na int) uložím adresu proměnné x (typu int). Tato operace je znázorněna graficky na slajdu č. 53 na obrázku vpravo. Místo formulace, že ukazatel p má v sobě uloženou adresu proměnné x, říkáme, že ukazatel p ukazuje na proměnnou x. Operátor reference lze použít pouze na výrazy, které mají svoji adresu, nelze ho použít na konstantní výrazy, třeba p = &3. Prefixní operátor dereference * (hvězdička) slouží k získání hodnoty proměnné, na kterou ukazatel ukazuje. Například zápis y = *p znamená, že do proměnné y (typu int) uložím hodnotu proměnné, na kterou ukazuje p (ukazatel na typ int), v našem případě hodnotu proměnné x (typu int). Dereference je inverzní operací k referenci. 2.1 Neinicializovaný a nulový ukazatel Po deklaraci je ukazatel neinicializovaný, obsahuje neznámou náhodnou hodnotu, stejně jako jakákoliv jiná lokální proměnná. Je v něm tedy náhodná adresa v paměti. Zatímco v případě neinicializované celočíselného proměnné získáme jejím zpracováním pouze nesmyslný výsledek, v případě ukazatele dojde k přístupu k náhodné adrese paměti. Obvykle se tato náhodná adresa nachází mimo adresný prostor našeho programu a dojde tedy k násilnému ukončení programu ze strany operačního systému. Můžeme mít ovšem smůlu a náhodná adresa může patřit našemu programu, poté data na této adrese zápisem poškodíme. Z hlediska bezpečného programování je výhodné každému nově deklarovanému ukazateli, přiřadit speciální hodnotu, která bude znamenat, že v ukazateli není platná adresa. Za tímto účelem existuje konstanta NULL, která ukazuje na adresu s číslem 0. Můžeme také používat přímo číslo 0, a dokonce tento zápis je mezi programátory rozšířenější. Například zápisem int * p=0 vytvoříme proměnnou p typu ukazatel na int, která ukazuje na adresu 0. Pokud získáme ukazatel například jako parametr funkce, nevíme, zda obsahuje platnou adresu. Pokud důsledně dodržujeme pravidlo s inicializací nově deklarovaných ukazatelů na hodnotu 0, stačí nám nyní otestovat, zda ukazatel neobsahuje hodnotu 0 pomocí zápisu if(!p) nebo if(p!=NULL). Pokud test projde, ukazatel obsahuje platnou adresu a můžeme s tímto ukazatelem pracovat. Tato ochrana funguje pouze v případě, že ukazatel před prvním použitím testujeme na hodnotu 0. Pokud test neprovedeme, a pokud se pokusíme číst nebo zapisovat data z adresy 0, dojde k běhové chybě programu a jeho ukončení. 2.2 Ukazatel na ukazatel Proměnnou p typu ukazatel (například na int) můžeme považovat za obyčejnou proměnnou, má svoje místo v paměti, kde je uložena její hodnota. Toto místo v paměti má svoji adresu,
kterou můžeme pomocí operátoru reference získat a uložit ho do proměnné pp typu ukazatel na ukazatel na int. Někdy bývá taky označován jako dvouhvězdičkový ukazatel. Deklarace ukazatele na ukazatel není složitá, oproti deklaraci obyčejného ukazatele pouze přidáme jednu hvězdičku navíc. V našem případě bude deklarace vypadat takto int ** pp. Do proměnné pp nyní můžeme uložit adresu proměnné p pomocí operátoru reference pp = &p. Pokud chceme získat nazpátek obyčejný ukazatel, použijeme operátor dereference *pp. Pokud se chceme dostat až k hodnotě samotného čísla musíme použít operátor dereference dvakrát **pp. Ukazatel na ukazatel se využívá například při práci s polem textových řetězců, které se používá při zpracování parametrů programu zadaných z příkazové řádky (tématický blok č. 5). Rovněž ze využívá v některých situacích při práci s dynamicky alokovanými datovými strukturami. Lze vytvořit i ukazatel na ukazatel na ukazatel (= tříhvězdičkový ukazatel), pokud při deklaraci použijeme tři hvězdičky int *** ppp, ale takovéto zápisy se vidí v běžných programech velmi vzácně. Počet použitých hvězdiček není limitován, lze tedy vytvořit i ukazatele s 4, 5 nebo i 10 hvězdičkami. 2.3 Příklady: ukazatele Na slajdech č. 54 – 57 vidíme devět řádek zdrojového kódu aktivně využívajících práci s ukazateli. Na každém ze slajdů je několik řádků označeno tučně. Hodnoty proměnných (a vztahy mezi nimi) po vykonání těchto tučně zvýrazněných řádků jsou pak graficky znázorněny na obrázku napravo. Na slajdu č. 54 na prvním řádku vidíme deklaraci dvou proměnných x a y typu int. Proměnným jsou přiřazeny inicializační hodnoty, x = 1 a y = 3. Na druhém řádku je deklarace dvou ukazatelů na typ int, pojmenovaných px a py. Zajímavý je třetí řádek, kde se pokoušíme zapsat hodnotu 5 na adresu v paměti, kam ukazuje ukazatel px. Ukazatel px je dosud nezinicializovaný, takže v sobě obsahuje neznámou náhodnou adresu, do které se pokusíme provést zápis. Tato programátorská chyba se projeví za běhu programu pravděpodobně jeho ukončením od operačního sytému. Na obrázku napravo je celá situace znázorněna graficky. Abychom mohli pokračovat ve vykonávání zdrojového kódu, musíme si představit, že řádek 3 se nevykonal. Na slajdu č. 55 jsou tučně označeny řádky č. 4 a 5. Na čtvrtém řádku do ukazatele py přiřadíme hodnotu NULL. Následně na tom samém řádku v druhém příkazu zkusíme na adresu, kam ukazuje py zapsat hodnotu 7. Protože py ukazuje na adresu 0 (NULL), dojde k běhové chybě programu a jeho ukončení. Opět předpokládejme, že jsme tento příkaz nevykonali. Na řádku č. 5 otestujeme pomocí podmíněného příkazu, jestli ukazatel py obsahuje nenulovou adresu a pokud ano, provedeme uživatelskou funkci etwas. Protože podmínka splněna nebyla (ukazatel py obsahuje nulovou adresu), funkce se neprovede. Pokud bychom zkoušeli tento zdrojový kód přeložit kompilátorem, je nutné napsat alespoň prázdné tělo funkce etwas, nejedná se o žádnou knihovní funkci. Na obrázku napravo je opět celá situace znázorněna graficky.
Na slajdu č. 56 jsou tučně označeny řádky č. 6 a 7. Na šestém řádku přiřadíme do ukazatele px adresu proměnné x, do ukazatele py adresu proměnné y. Na sedmém řádku pomocí operátoru dereference přistoupíme k hodnotě paměti, na kterou ukazuje ukazatel py a tuto hodnotu pomocí postfixního operátoru ++ zvětšíme o jedna. Ukazatel py ukazuje na proměnnou y, která má hodnotu 3, její hodnota se tedy zvýší na 4. Na obrázku napravo dole je opět celá situace znázorněna graficky. U zápisu na sedmém řádku je důležité použití závorek určujících prioritu vykonávání operátorů., aby se jako první vykonal operátor dereference. Bez použití závorek by se vykonal jako první operátor ++, protože jako postfixní operátor má vyšší prioritu, než prefixní operátor dereference. V případě zápisu bez použití závorek by se zvýšila adresa uložená v py o jedna, tedy py by ukazoval do paměti následující za proměnnou y. V této paměti bude pravděpodobně uložena jiná lokální proměnná. Na slajdu č. 56 jsou tučně označeny řádky č. 8 a 9. V prvním příkazu na osmém řádku přiřadíme do ukazatele px adresu uloženou v ukazateli py. Tedy px bude ukazovat na to samé místo v paměti, kam ukazuje py, tedy na proměnnou y. V druhém příkazu na osmém řádku do proměnné y přiřadím hodnotu proměnné x, tedy 1. Tuto situaci znázorňuje horní obrázek napravo slajdu. Na devátém řádku do ukazatele py přiřadíme adresu proměnné x, a na místo kam nově ukazatel ukazuje zapíšeme hodnotu 9. Tedy hodnotu 1 proměnné x přepíšeme na hodnotu 9. Tuto situaci znázorňuje spodní obrázek napravo slajdu. 2.4 Ukazatel na prvek pole Ukazatel může ukazovat nejen na obyčejnou proměnnou, ale například také na určitý prvek pole. Zápisem p=&a[2] do ukazatele p uložíme adresu třetího prvku (prvku s indexem 2) pole a. Pokud se chceme ukazatelem v poli posunout na další prvek, stačí zvětšit jeho hodnotu (uloženou adresu) o jedna například zápisem p++. Tento posun je možné provést, protože pole je souvislý kus paměti a prvky v poli jsou fyzicky uloženy ve stejném pořadí, v jakém k ním můžeme přistoupit pomocí indexů. Na slajdu č. 58 vidíme příklad s polem a o velikosti pět prvků a ukazatelem p. Na třetím řádku zdrojového kódu do prvku s indexem 2 v poli a uložíme hodnotu 20. Na čtvrtém řádku bude ukazatel p nově ukazovat právě na tento prvek s indexem 2. Situace je graficky znázorněna na obrázku vpravo nahoře. Na pátém řádku do prvku pole a s indexem 0 uložím hodnotu uloženou na adrese určené ukazatelem p (hodnota prvku pole a s indexem 2, tedy 20) zmenšenou o 15, tedy číslo 5. Na šestém řádku ukazatel p posunu na další prvek pole (prvek s indexem 3). Na posledním řásku do místa v paměti, kam ukazuje ukazatel p, zapíšu hodnotu 20. Situace je graficky znázorněna na obrázku vpravo dole. 2.5 Aritmetika ukazatelů Existuje automatická konverze pole na ukazatel na první prvek (prvek s indexem 0) tohoto pole. Zápis a je ekvivalentní zápisu &a[0] znamenajícímu adresa prvku s indexem 0 v poli a. Tato konverze se využívá v situacích, kdy na daném místě ve zdrojovém kódu nemůže být
použito pole, ale ukazatel být použitý může. Například pole nelze přiřazovat, ani předávat hodnotou. Pokud ukazatel ukazuje na prvek pole, přičtení čísla n k ukazateli znamená posun tohoto ukazatele na prvek pole vzdálený o n pozic napravo od prvku, na který ukazatel právě ukazuje. Odečtení znamená posun nalevo. Na slajdu č. 59 na prvním řádku ukazatel p nastavíme na prvek pole a s indexem 1. Na druhém řádku do prvku vzdáleného od prvku, na který ukazuje ukazatel p, o 3 prvky směrem doprava (prvek s indexem 4) zapíšeme hodnotu 40. Situace je znázorněna na obrázku napravo slajdu. Zápis p[i] znamená hodnota prvku s indexem i v poli p. Tento zápis je ekvivalentní zápisu *(p+i), znamenajícího hodnota místa v paměti, které má adresu získanou přičtením indexu i k adrese pole p. K prvkům pole můžeme přistupovat pomocí operátoru dereference, s polem můžeme pracovat jako s ukazatelem. Operátor hranatých závorek pro práci s polem nemusíme používat. Pokud ukazatel ukazuje na prvek pole, tak naopak s ním můžeme jako s polem pracovat a operátor hranatých závorek na něm používat. 2.6 Příklady: pole a ukazatele Na slajdu č. 60 máme deklarované pole a obsahující 5 prvků typu int a ukazatel p ukazující na typ int. Předpokládejme, že pole je inicializováno počátečními hodnotami 0, 10, 20, 30 a 40, jak ukazuje obrázek napravo slajdu. Nyní se budeme zabývat devíti řádky zdrojového kódu, který je nalevo na slajdu na zeleném podkladu. Na prvním řádku ukazatel p nastavíme tak, aby ukazoval na první prvek (prvek s indexem 0) pole a. Na druhém řádku provedeme přesně to samé, pouze použijeme automatické konverze pole na svůj první prvek. Na třetím řádku do místa v paměti, kam ukazuje ukazatel p, zapíšeme hodnotu prvku pole a s indexem 1. Tedy do prvku pole a s indexem 0 se zapíše hodnota 10. Výsledné pole a má hodnoty 10, 10, 20, 30 a 40. Na čtvrtém řádku je složitější příkaz, rozebereme si samostatně výraz nalevo před operátorem přiřazení a výraz napravo. Výraz nalevo určuje místo v paměti, kam se bude zapisovat hodnota výrazu spočteného napravo. Na pravé straně od prvku pole a s indexem 2 odečteme hodnotu 1, tedy 20-1=19. Na levé straně se ukazatel p posune o 2 prvky doprava a do místa v paměti, kam ukazuje (prvek pole a s indexem 2), zapíšeme hodnotu pravé strany příkazu, tedy 19. Tento složitý příkaz by šel nahradit jednoduchým příkazem a[2]--. Po skončení příkazu ukazatel p stále ukazuje na prvek pole a s indexem 0. Výsledné pole a má hodnoty 10, 10, 19, 30 a 40. Na pátém řádku nastavíme ukazatel p tak, aby ukazoval na prvek pole a s indexem 2. Ekvivalentní zápis by byl p=&a[2]. Hodnoty prvků pole a se nemění, pouze se posouvá ukazatel p. Na šestém řádku je opět složitější příkaz, rozebereme si samostatně výraz nalevo před operátorem přiřazení a výraz napravo. Výraz nalevo p[1] pracuje s ukazatelem p jako s polem. Tento zápis si můžeme dovolit, protože ukazatel p skutečně ukazuje na prvek nějakého pole. Představme si, že prvek pole p s indexem 0 odpovídá prvku pole a s indexem 2. Výraz p[1] znamená, že hodnotu získanou z pravé strany celého příkazu budeme ukládat
do prvku pole p s indexem 1, tedy do prvku pole a s indexem 3. Na pravé straně příkazu je výraz *(a+1), který naopak s polem a pracuje jako s ukazatelem, je to ekvivalent výrazu a[1], má hodnotu 10. Celý příkaz do prvku pole a s indexem 3 zapíše hodnotu 10. Výsledné pole a má hodnoty 10, 10, 19, 10 a 40. Na sedmém řádku do prvku pole a s indexem 3 zapíšeme hodnotu prvku pole p s indexem 2, neboli prvku pole a s indexem 4, tedy hodnotu 40. Výsledné pole a má hodnoty 10, 10, 19, 40 a 40. Na osmém řádku nás může překvapit zápis p[-1], který je na pravé straně od operátoru přiřazení. Nejedná se ovšem o chybu. Pole p začíná uprostřed pole a, má tedy platné indexy svých prvků z intervalu <-2,2>. Zápis p[-1] v našem případě odpovídá zápisu a[1], hodnotě 10. Celý příkaz znamená, že do prvku pole a s indexem 2, zapíšeme hodnotu 10. Výsledné pole a má hodnoty 10, 10, 10, 40 a 40. Na devátém řádku vidíme zápis 3[a], který na první pohled připomíná syntaktickou chybu, ale o žádnou chybu se nejedná. V zápisu můžeme rozložit operátor hranatých závorek, získáme *(3+a), využijeme komutativnosti sčítání a získáme *(a+3), tento výraz odpovídá a[3]. Analogicky 2[p] odpovídá p[2]. Přestože tyto zápisy (3[a], 2[p]) jsou syntakticky správně, není vhodné je používat, zdrojový kód se stává nepřehledným. Na devátém řádku do prvku pole prvku pole a s indexem 3 zapíšeme hodnotu p[2], neboli a[4], číslo 40. Výsledné pole a má shodou okolností stejné hodnoty, jaké mělo po vykonání minulého řádku: 10, 10, 10, 40 a 40. Klíčové pojmy pole (prvek, velikost, index, dimenze) ukazatel (nezinicializovaný, nulový) operátory reference a dereference ukazatel na ukazatel automatická konverze pole na ukazatel Otázky k rekapitulaci Upozornění: odpovědi na některé zde uvedené otázky nelze najít ve studijním textu tohoto tématického bloku. Lze je získat vlastním experimentováním se zdrojovými kódy nebo studiem doporučené literatury. Jak se deklaruje jednorozměrné a vícerozměrné pole ? Jakým způsobem jsou jednotlivé prvky pole označeny, jak k ním přistoupíme? Jaké hodnoty mají prvky pole po jeho vytvoření? Co se stane, pokud použijeme neplatný index pro přístup k prvku pole? Můžeme měnit velikost pole za běhu programu? Musíme vždy zadat velikost pole? Jaké jsou kladeny podmínky na hodnotu, kterou zadáme jako velikost pole? Jak se deklaruje pole s inicializací jeho prvky? Vysvětlete pojem ukazatel, jak se ukazatel deklaruje?
Co dělají operátory reference a dereference? Co znamená ukazatel na ukazatel, jak se deklaruje a k čemu se používá? Od jakých výrazů můžeme získat jejich adresu? Co je to neinicializovaný ukazatel? Co je to nulový ukazatel, k čemu se používá? Co se stane při přístupu k paměti pomocí neinicializovaného nebo nulového ukazatele, je zde nějaký rozdíl? Jaký je rozdíl mezi zápisy p++, *p++, (*p)++ a *(p++), pokud p je ukazatel? Jaký je vztah mezi ukazatelem a polem a k čemu se tento vztah využívá? Můžeme v poli přistoupit k prvku se záporným indexem? Jaký je vztah mezi operátorem hranatých závorek a operátorem dereference. Které ze zápisů jsou syntakticky správně p[i], [p]i, i[p], [i]p, za předpokladu, že p je pole a i je celé číslo? Co tyto zápisy znamenají? Své odpovědi zdůvodněte. Můžete přidat i syntaktické zápisy tam, kde je to vhodné.
Doporučené příklady k naprogramování Pro zadání vstupních dat použijte inicializované pole, je to rychlejší, než při každém testovacím pokusu, zadávat znova data z klávesnice. V programu používejte funkce, kterým předáte jako parametry ukazatel na první prvek pole a velikost pole. 1. Napište program, který setřídí pole. Vyzkoušejte všechny následující třídící algoritmy: minimem, bubblesort, mergesort, quicksort, heapsort v poli. 2. Napište program, který v poli najde třetí největší prvek a spočítá aritmetický průměr prvků pole. Původní pole musí zůstat nezměněno 3. Napište program, který v poli najde medián (když se prvky setřídí, tak je to prostřední prvek, v případě sudého počtu prvků je to aritmetický průměr prostředních prvků) a modus (nejčastěji se vyskytující prvek). Původní pole musí zůstat nezměněno. 4. Definujte si výčtový typ a pak náhodně s jeho prvky naplňte pole. Napište program, který vypíše počty výskytů jednotlivých prvků pole setříděné sestupně. 5. Dobrovolný domácí úkol č. 1: Pomocí Gaussovy eliminační metody vyřešte soustavu n rovnic o n neznámých, kde n je pevně dané. Zdrojový kód tohoto domácího úkolu lze používat u zkoušky. Studijní literatura Výklad často odkazuje na slajdy (ve formátu .ppt), které je vhodné si vytisknout. Je vhodné si pořídit nějakou knihu o programování v C nebo C++. Uvedené příklady knih berte pouze jako inspirativní. Miroslav Virius: Programování v C++ (ČVUT, 2. vydání 2004) Jesse Liberty, Bradley L. Jones: Naučte se C++ za 21 dní (Computer Press, 2. vydání, 2007)
Knihu je dobré číst postupně a vlastním tempem, můžete mít i mírné zpoždění oproti našemu výkladu. Pořadí kapitol v knize neodpovídá úplně přesně pořadí, v jakém učivo probíráme. Tento tématický blok se zaměřte na pole a ukazatele Miroslav Virius: Pasti a propasti jazyka C++ (Brno, 2. vydání 2005) Kapitola 3 Pole a ukazatele