| ← práce s typy dat | C/C++ | operátory a výrazy → |
O datových polích jsem se již stručně zmínil v souvislosti s řetězci. K řetězcům snad není nutné říkat nic jiného, než že jsou to vlastně pole typu char zakončené nulovým znakem '\0'.
Datové pole je nějaké spojité místo vyhrazené kdesi v paměti pro několik objektů stejného datového typu. K tomu, abychom mohli k těmto objektům přistupovat, slouží ukazatel na onen datový typ.
Pole může mít různou velikost. Kde začíná je dáno právě ukazatelem, jeho konec si však musíme hlídat. U řetězců je konec řetězce dán znakem '\0', což není nic jiného než nulový bajt. Je tak ukončen řetězec, nikoliv pole. Konec pole nijak označen není, proto se často stává ta chyba, že se program pokouší číst mimo pole a to vede ke zhroucení programu.
Rozdíl mezi polem typu char a mezi řetězcem je pouze v tom, že řetezec má na svém konci uložen nulový znak.
Pole se deklaruje následovným způsobem:
datovy_typ nazev [rozsah];
Například:
char znaky[4]; /* pole pro 4 znaky */ int cisla[1]; /* pole pro jedno cislo */
Proměnné znaky a cisla jsou konstantní ukazatele, které ukazují na první prvek z pole. Jednotlivé položky pole se adresují od nuly pomocí hranatých závorek. Například pole cisla obsahuje jen jedno místo pro číslo typu int a s tím lze pracovat následovně (připomeňme, že v jazyku C se indexuje od nuly, nikoliv od jedničky, jako např. v Pascalu):
cisla[0] = 5; x = cisla[0];
Chybné by bylo přiřazení cisla=5;, protože tím by jste přiřazovali číslo 5 do proměnné cisla, čili by jste se pokoušeli změnit konstantní ukazatel, který obsahuje adresu začátku pole.
Číslo v hranatých závorkách při definici pole určuje jeho velikost. V jazyce C
to musí být skutečné číslo, nelze použít proměnnou, ani konstantu (číslo musí
překladač znát už v době překladu, aby pro pole dokázal vyhradit dostatek paměti).
V jazyce C++ lze použít i konstantu (její hodnota je překladači známa, protože
se nemění).
Číslo v hranatých závorkách za ukazatelem při použití v kódu se nazývá index pole.
Indexem pole se odkazujeme na n-tou položku pole.
Používání indexu pole si osvětlíme na následujícím příkladu. Definujeme si pole znaky.
char znaky[4];
V tabulce je ukázáno, jak se uloží pole znaky do paměti.
| skutečná adresa | ... | 1535 | 1536 | 1537 | 1538 | ... |
|---|---|---|---|---|---|---|
| pozice | ... | 0 | 1 | 2 | 3 | ... |
| bity | ??? | ???????? | ???????? | ???????? | ???????? | ??? |
Momentálně pole znaky obsahuje náhodné bity. Ukazatel znaky ukazuje na adresu 1535 (to číslo je samozřejmě smyšlené). Pokud chceme vložit do pole znaky a vytvořit tak řetězec, nesmíme zapomenout na zarážku (nulový bite).
znaky[0] = 'j'; znaky[1] = 'e'; znaky[2] = '\0';
V tabulce můžete vidět, jak se pole přiřazením hodnot vyplnilo.
| skutečná adresa | ... | 1535 | 1536 | 1537 | 1538 | ... |
|---|---|---|---|---|---|---|
| pozice | ... | 0 | 1 | 2 | 3 | ... |
| bity | ??? | 01101010 | 01100101 | 00000000 | ???????? | ??? |
Pokud pole rovnou inicializujeme, nemusíme uvádět jeho délku. Délka pole bude taková, kolik prvků předáme při inicializaci.
int cisla[] = { 5, -1, 3, 8}; /* pole pro 4 cisla */
char pole1[] = {'a', 'h', 'o', 'j', '\0'}; /* pole pro 5 znaku */
char pole2[] = "ahoj"; /* pole pro 5 znaku */
Inicializace pole1 a pole2 je ekvivalentní. Nyní můžeme přiřadit: pole2[4] = 'x';. Tím se však připravíme o zarážku a již nemůžeme s polem pracovat jako s řetězcem (například jej předat funkci printf(), protože by se po vytištění ahojx pokusila funkce printf() tisknout další byty z paměti za koncem pole).
Když se podíváte, jakým způsobem je v paměti pole uloženo, pochopíte, že v paměti můžou být jen jednorozměrná pole (prvky pole můžou být řazeny v paměti počítače pouze za sebou). Nic nám však nebrání vytvořit pole, jehož prvky budou jiná pole a tak vytvářet pole libovolných dimenzí. Podívejme se na definici dvourozměrného pole a jeho uložení v paměti.
char pole[4][2] = {{0,0},{1,1},{3,4},{0xff,0xff}};
Jedná se o čtyřprvkové pole, jehož prvky jsou dvouprvkové pole.
| skutečná adresa | ... | 1535 | 1536 | 1537 | 1538 | 1539 | 1540 | 1541 | 1542 | ... |
|---|---|---|---|---|---|---|---|---|---|---|
| pozice | ... | [0][0] | [0][1] | [1][0] | [1][1] | [2][0] | [2][1] | [3][0] | [3][1] | ... |
| hodnota | ??? | 0 | 0 | 1 | 1 | 3 | 4 | 255 | 255 | ??? |
Všimněte si, že při putování pamětí se mění vždy nejdříve poslední index pole a pak ty předešlé.
Můžete tak vytvářet pole libovolné dimenze a velikosti, ale dejte pozor na to, kolik takové pole zabírá místa v paměti. Např. pole long double nazev[50][7][2][8] je velké 10*50*7*2*8 = 56000 bytů (long double je veliký 10 bytů)!
Vícerozměrná pole se využívají poměrně často. Například kalendar[12][31], sachovnice[8][8] atp. Práce s takovými poli je daleko snazší a přehlednější než s mnoha jednorozměrnými poli, přestože např. v kalendáři nevyužijeme všechny prvky pole (ne každý měsíc má 31 dní, že).
Když si zopakujete kapitolu o ukazatelích, pak spolu s předcházejícími odstavci o polích by vám mělo být již vše jasné. Přesto si můžeme ještě ukázat zajímavá „kouzla“, která lze s ukazateli provádět. Začneme s řetězci. Víte například to, jaký je rozdíl v následujících definicích?
char *retezec1 = "Ahoj"; char retezec2[] = "Ahoj";
Rozdíl je v tom, že retezec2 je konstantní ukazatel (pole), kdežto retezec1 je ukazatel inicializovaný hodnotou adresy řetězce "Ahoj". Při obou definicích jsme vytvořili řetězec "Ahoj", což je pole znaků (se zarážkou '\0' na konci). V následujícím příkladě se podívejte, jakým způsobem lze adresovat jednorozměrné pole. Když si představíte, jakým způsobem je pole v paměti uloženo a uvědomíte si, že ukazatel obsahuje číselnou hodnotu která je adresou, neměl by být pro vás problém příklad pochopit.
1: /*------------------------------------------------*/ 2: /* pole1.c */ 4: #include <stdio.h> 6: int main(void) 7: { 9: float pole[] = { 5.0, 6.0, 7.0, 8.0, 9.0 }; 10: float *ukazatel; 12: ukazatel = pole; 14: /* prvni cast */ 15: printf("%.1f ", pole[1]); 16: printf("%.1f ", pole[1] + 10.0); 17: printf("%.1f, ", (pole + 1)[2]); 19: /* druha cast */ 20: printf("%.1f ", *ukazatel); 21: printf("%.1f ", *ukazatel + 10); 22: printf("%.1f\n", *(ukazatel + 1)); 24: return 0; 25: } 27: /*------------------------------------------------*/
Výstup z programu:
6.0 16.0 8.0, 5.0 15.0 6.0
Všimněte si, jakou roli hrají závorky! Také si všimněte, jak chytře pracuje jazyk C s aritmetikou ukazatelů. Výraz (ukazatel + 1) ukazuje na další prvek pole, přestože typ float je dlouhý hned 4 bajty a ne jeden. To je jeden z důvodů, proč se při deklaraci ukazatele určuje, jakého je typu. Nemusíte si tak lámat hlavu, o kolik bytů by jste měli zvýšit jeho hodnotu, aby se ukazoval na další prvek příslušného datového typu. Překladač pak zvýší hodnotu ukazatele o správný počet bytů tak, aby ukazoval na další prvek v poli.
Následující výrazy jsou ekvivalentní:
&pole[n] je totéž co
(pole + n) (tj adresa n-tého prvku v paměti)
a
pole[n] je totéž co *(pole
+ n) (tj hodnota n-tého prvku v paměti)
V jazyku C může vytvořit ukazatel na libovolný datový typ (třeba i na vlastní strukturu – o těch později). Ukazatel tedy může ukazovat i na ukazatel. Například můžete vytvořit ukazatel, který může ukazovat na „ukazatel na typ char“:
char **ukazatel;
Můžete taktéž vytvořit pole ukazatelů (včetně vícerozměrných polí ukazatelů...).
char * ukazatel[10];
Příklad:
1: /*------------------------------------------------*/ 2: /* unau1.c */ 3: #include <stdio.h> 5: int main(void) 6: { 7: int x; 8: int *ukazatel_na_int; 9: int **ukazatel_na_ukazatel; 11: int pole[] = { 5, 7, 9, 11, 13 }; 13: /* inicializace promennych */ 14: x = 25; 15: ukazatel_na_int = &x; 16: ukazatel_na_ukazatel = &ukazatel_na_int; 18: /* pristup k promenne x */ 19: printf("%2d = %2d = %2d\n", x, *ukazatel_na_int, 20: *(*ukazatel_na_ukazatel)); 22: /* inicializace */ 23: ukazatel_na_int = pole; 24: /* ukazatel_na_ukazatel = &ukazatel_na_int; ... toto prirazeni je o 25: * nekolik radek vyse a hodnota ukazatel_na_ukazatel ukazuje stale na 26: * ukazatel_na_int */ 28: /* pristup k poli */ 29: printf("%2d = %2d = %2d\n", pole[0], *ukazatel_na_int, 30: **ukazatel_na_ukazatel); 31: printf("%2d = %2d = %2d\n", pole[1], *(ukazatel_na_int + 1), 32: *((*ukazatel_na_ukazatel) + 1)); 33: return 0; 34: } 36: /*------------------------------------------------*/
Výstup z programu:
25 = 25 = 25 5 = 5 = 5 7 = 7 = 7
Podívejme se na vyhodnocení výrazu *(*ukazatel_na_ukazatel)
Závorky v tomto výrazu nejsou podstatné a jsou zde jen pro
ilustraci. První, co se provede, je vyhodnocení výrazu
*ukazatel_na_ukazatel. Hvězdička je dereferencí adresy, tedy
hodnota proměnné, na kterou ukazuje proměnná
ukazatel_na_ukazatel. V našem případě to znamená, že
*ukazatel_na_ukazatel je hodnota proměnné
ukazatel_na_int (to je v příkladě adresa proměnné
x).
Dejme tomu, že adresa x je 123456.
Potom *(*ukazatel_na_ukazatel) je
*(123456) a to je hodnota,
na kterou 123456 ukazuje, tedy hodnota proměnné
x.
Teď si už jen v rychlosti probereme výraz
*((*ukazatel_na_ukazatel) + 1).
Dejme tomu, že pole
začíná na adrese 12340. Potom
ukazatel_na_int je po druhé
inicializaci 12340, (*ukazatel_na_ukazatel) je tedy také 12340.
A teď pozor. Výraz
(*ukazatel_na_ukazatel + 1) je
(12340+1*n) a výraz
*((*ukazatel_na_ukazatel) + 1) je
*(12340+1*n).
Číslo n je počet bajtů, které zabírá typ int, protože jedničku
přičítáme k ukazateli na int. Tak tomu se říká aritmetika ukazatelů.
Uff.
Ovšem pozor. Ukazatele a pole nelze libovolně zaměňovat. Tak například **pole1 a pole5[][5] a pole6[][6] jsou tři různé typy. S vědomostmi které máte o aritmetice ukazatelů vás jistě napadne, jaké chyby by se mohli stát, kdyby jste deklarace těchto proměnných libovolně zaměňovali. (Mám na mysli hlavně aplikaci pole1 na některé z dvouromzěrných polí. Správně by se mělo použít např.: int pole5[N][5]; int (*upole5)[5]; upole5 = pole5; atp.)
Pokud jste se ve zdraví prokousali až sem, pak vám gratuluji. Váš mozek právě začal mutovat v „mozek programátora jazyka C“. Tento proces je bohužel nevratný.
| ← práce s typy dat | C/C++ | operátory a výrazy → |
