Pole a ukazatele
Datové pole a řetězce
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, abyste 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íte ohlí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 rozsah pole, což 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. Vlastně je to jen taková dohoda, na kterou spoléhají všechny funkce pracující s řetězci.
Pole se deklaruje následovným způsobem:
Například:
Proměnné znaky a
cisla jsou proměnné typu array
,
které se chovají jako 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řipomínám, že v jazyku C se indexuje od
nuly, nikoliv od jedničky, narozdíl od Pascalu):
Chybné by bylo přiřazení cisla=5;
,
protože tím byste přiřazovali číslo 5 do proměnné
cisla, čili byste se
pokoušeli změnit „konstantní ukazatel“,
který obsahuje adresu začátku pole.
Konstantní ukazatel na pole obsahuje adresu paměti, kde začíná pole. Tuto hodnotu změnit nelze, můžete měnit jen hodnoty v poli.
Čí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 odkazuje na n-tou položku pole.
Používání indexu pole osvětlím na následujícím příkladu. Definuji pole znaky.
V tabulce je ukázáno, jak se uloží pole znaky do paměti.
skutečná adresa | 1534 | 1535 | 1536 | 1537 | 1538 | 1539 |
---|---|---|---|---|---|---|
pozice (index) | ... | 0 | 1 | 2 | 3 | ... |
bity | ??? | ???????? | ???????? | ???????? | ???????? | ??? |
Momentálně pole znaky obsahuje náhodné byty. Ukazatel znaky ukazuje na adresu 1535 (to číslo je samozřejmě smyšlené). Pokud chcete vložit do pole znaky a vytvořit řetězec, nesmíte zapomenout na zarážku (nulový byte).
znaky[1] = 'e';
znaky[2] = '\0';
V tabulce můžete vidět, jak se pole přiřazením hodnot vyplnilo.
skutečná adresa | 1534 | 1535 | 1536 | 1537 | 1538 | 1539 |
---|---|---|---|---|---|---|
pozice (index) | ... | 0 | 1 | 2 | 3 | ... |
bity | ??? | 01101010 | 01100101 | 00000000 | ???????? | ??? |
Všiměte si, že poslední bajt pole je nevyužitý (znak[3] na adrese 1538). Taky si všiměte, že pokud byste se pokusili přiřadit hodnotu do znak[4], odstanete se na adresu 1539, která už zasahuje mimo rozsah pole. To může vést k chybě „neoprávněný přístup do paměti“ a následnému odstřelení programu operačním systémem. Ale taky nemusí. Můžete si přepsat nějakou jinou proměnnou, která v této části paměti sídlí a tím vytvořit nějakou jinou, nepředvídatelnou, těžko odhalitelnou chybu.
Operační systém nedovolí vašemu programu sahat do paměti, která mu nebyla vyhrazena. Je to z bezepčnostních důvodů, aby jeden program nemohl škodit druhému.
Pokud pole během jeho definice rovnou inicializujete, nemusíte uvádět jeho délku. Délka pole bude taková, kolik prvků předáte při inicializaci.
Inicializace pole1
a
pole2
je ekvivalentní. Všiměte si, že v druhém případě je přidán nulový znak na konec automaticky.
Nyní můžete přiřadit: pole2[4] = 'x';
.
Tím se však připravíte o zarážku (nulový znak) a již nemůžete s polem pracovat jako s
řetězcem (například jej předat funkci printf()
,
protože by po vytištění ahojx funkce printf()
tiskla další byty
z paměti, dokud by nenarazila na '\0' (nebo ji operační systém nesestřelil).
Podívejte se ještě na tento příklad:
Proměnné pole1 i pole2 jsou ukazatele na typ char.
Rozdíl v těchto definicích je ale v tom, že řetězec zdar je řetězcový
literál, který nemůžete měnit, zatímco ahoj se uloží do pole 5 znaků,
jehož hodnoty můžete libovolně měnit.
Na definici s pole1[]
prostě pohlížejte jako na char pole1[5] = {'a', 'h', 'o', 'j', '\0'};
a nebudete si to plést.
Další rozdíl je v tom, že proměnná pole1 je konstantní (já vím, konstantní proměnná zní divně :D), takže její hodnotu nemůžete měnit, ale hodnotu proměnné pole2 ano.
Řetězcový literál "zdar"
nelze měnit v žádném případě. Pokud byste se pokusili
o něco jako pole2[0] = 'Z';
, program se zhroutí.
Na druhou stranu, pro pole1 se alokuje paměť v místě,
kde ji lze měnit (a do tohoto místa se "ahoj"
nakopíruje).
Vícerozměrná pole
Když se pdí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 vám však nebrání vytvořit pole, jehož prvky budou jiná pole a tak vytvářet pole libovolných dimenzí (rozměrů). Podívejte se na definici dvourozměrného pole a jeho uložení v paměti.
Jedná se o čtyřprvkové pole, jehož prvky jsou dvouprvková pole typu char.
skutečná adresa | 1534 | 1535 | 1536 | 1537 | 1538 | 1539 | 1540 | 1541 | 1542 | 1543 |
---|---|---|---|---|---|---|---|---|---|---|
index | ... | [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 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žijete všechny prvky pole (ne každý
měsíc má 31 dní).
Ukazatele a pole
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čnu s řetězci. Víte například to, jaký je rozdíl v následujících definicích?
Rozdíl je v tom, že retezec2
je konstantní
ukazatel (pole), kdežto retezec1
je ukazatel
inicializovaný hodnotou adresy řetězcového literálu "Ahoj".
Při obou definicích se vytvořil řetězec "Ahoj", což je pole znaků (se zarážkou '\0' na konci). První je však uložen v části paměti, která je jen pro čtení, druhý je uložen v poli, se kterým můžete manipulovat.
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.
/* c09/pole1.c */
#include <stdio.h>
int main(void)
{
float pole[] = { 5.0, 6.0, 7.0, 8.0, 9.0 };
float *ukazatel;
ukazatel = pole;
/* prvni cast */
printf("%.1f ", pole[1]);
printf("%.1f ", pole[1] + 10.0);
printf("%.1f\n", (pole + 1)[2]);
/* druha cast */
printf("%.1f ", *ukazatel);
printf("%.1f ", *ukazatel + 10);
printf("%.1f ", *(ukazatel + 1));
printf("%.1f\n", *(ukazatel + 1) + 10);
return 0;
}
/*------------------------------------------------*/
Výstup z programu:
6.0 16.0 8.0 5.0 15.0 6.0 16.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ů byste 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)
Abych vás ještě trochu potrápil, ukáži vám příklad s dalším možným zápisem adresy pole:
/* c09/pole2.c */
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char** argv) {
char array[5] = {'a', 'b', 'c', 'd', 'e'};
// array[2] je totez jako 2[array]
printf("array[2] = '%c'\n", 2[array]);
return EXIT_SUCCESS;
}
/*------------------------------------------------*/
Tento způsob indexace pole není ukázkou dobrého stylu psaní a měli byste
ho používat jen k mučení učitele programování. Ukázka jen ukazuje to,
že je jedno, jestli se adresa paměti vypočte jako array + 2
,
nebo (jako v příkladu) 2 + array
.
array[2] = 'c'
Ukazatele na ukazatele
V jazyku C můžete 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“:
Můžete taktéž vytvořit pole ukazatelů (včetně vícerozměrných polí ukazatelů…).
Takto definované pole má velikost 10*sizeof(char *), tj. 10x velikost ukazatele, tj. např. 40 bajtů (pro 32-bitové adresování).
Příklad:
/* c09/unau1.c */
#include <stdio.h>
int main(void)
{
int x;
int *ukazatel_na_int;
int **ukazatel_na_ukazatel;
int pole[] = { 5, 7, 9, 11, 13 };
/* inicializace promennych */
x = 25;
ukazatel_na_int = &x;
ukazatel_na_ukazatel = &ukazatel_na_int;
/* pristup k promenne x */
printf("%2d = %2d = %2d\n", x, *ukazatel_na_int,
*(*ukazatel_na_ukazatel));
/* inicializace */
ukazatel_na_int = pole;
/* ukazatel_na_ukazatel = &ukazatel_na_int; ... toto prirazeni
* je o nekolik radek vyse a hodnota ukazatel_na_ukazatel ukazuje
* stale na ukazatel_na_int */
/* pristup k poli */
printf("%2d = %2d = %2d\n", pole[0], *ukazatel_na_int,
**ukazatel_na_ukazatel);
printf("%2d = %2d = %2d\n", pole[1], *(ukazatel_na_int + 1),
*((*ukazatel_na_ukazatel) + 1));
return 0;
}
/*------------------------------------------------*/
Výstup z programu:
25 = 25 = 25 5 = 5 = 5 7 = 7 = 7
Podívejte 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, jejíž výsledkem
je hodnota proměnné, na kterou ukazuje proměnná
ukazatel_na_ukazatel. V příkladě to znamená, že
*ukazatel_na_ukazatel je hodnota proměnné
ukazatel_na_int (to je adresa proměnné
x).
Dejme tomu, že adresa x je 123456.
Potom *(*ukazatel_na_ukazatel)
je
*(123456)
a to je hodnota v paměti na adrese 123456, tedy hodnota proměnné
x.
Teď už jen v rychlosti proberu výraz
*((*ukazatel_na_ukazatel) + 1)
.
Dejme tomu, že pole
začíná na adrese 12340. Potom
ukazatel_na_int bude po druhé
inicializaci v příkladu 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ů.
Výsledkem pak bude hodnota (kvůli hvězdičce) na adrese 12344 (pokud int zabírá 4 bajty).
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, kdybyste použití těchto proměnných libovolně
zaměňovali. Mám na mysli hlavně aplikaci pole1
na některé z dvourozmě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ý.
Datový typ pro indexaci pole
Standard jazyka C definuje datový typ size_t
pro
určování velikostí oběktů (a indexaci polí). Máte zaručeno, že datový typ size_t
je dost velký na to,
abyste mohli indexovat „libovolně“ velké pole.
(Pravděpodobně to bude ve skutečnosti něco jako unsigned int
nebo unsigned long
.)
Datový typ ptrdiff_t
se zase používá pro výsledek rozdílu pointerů.
Oba datové typy jsou definovány v knihovně <stddef.h>. Mohou být definovány i v jiných knihovnách (například knihovnách, které <stddef.h> sami využíají), takže není vždy nutné <stddef.h> includovat.
/* c09/typy.c */
#include <stdio.h>
#include <stddef.h>
int main(void)
{
char *text = "Hello world!";
char *u1 = text, *u2;
size_t i = 0;
ptrdiff_t diff;
while(text[i]) i++;
u2 = text+i;
diff = u1 - u2;
/* vypíše délku textu */
#ifdef _MSC_BUILD
printf("size_t i = %Iu, ptrdiff_t diff = %Ii\n", i, diff);
#else
printf("size_t i = %zu, ptrdiff_t diff = %ti\n", i, diff);
#endif
return 0;
}
/*------------------------------------------------*/
Ve zdrojovém kódu jsou 2 řádky s funkcí printf()
. První řádka se
použije ve Visual Studiu, druhá všude jinde. To je díky podmíněnému překladu, který
budu probírat později.
Důvodem je, jaké "%" sekvence používá printf()
pro výpis
těchto datových typů. Na druhé řádce je to podle standardu C99, na první řádce
je to podle standardu Microsoftu.
Pro datový typ size_t
používá printf()
dle standardu C99 %zu
,
ale Microsoft %Iu
.
Pokud používáte Dev-C++, musíte si nastavit
makro -D__USE_MINGW_ANSI_STDIO=1
,
jinak vám sekvence %zu
a podobné nebudou fungovat.
Výstup z programu:
size_t i = 12, ptrdiff_t diff = -12
Načtení řetězce pomocí funkce scanf()
V kapitole o Vstupu a výstupu jste se dozvěděli, že funkce
scanf()
používá pro řetězce
typ s
. A taky že maximální počet vstupních znků se dá omezit
pomocí „width
“. Z toho vychází následující příklad:
/* c09/scanstring.c */
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char znaky[10];
printf("Zadejte text: ");
fflush(stdout);
scanf("%9s", znaky);
printf("Nacteno: '%s'\n", znaky);
return 0;
}
/*------------------------------------------------*/
Bez fflush(stdout);
by mohl zůstat text tištěný
funkcí printf()
v bufferu, protože není ukončen novým řádkem '\n'
.
Možný výstup z programu:
Zadejte text: 1234567890 Nacteno: '123456789'
Načetlo se 9 znaků (jak jsem chtěl). Desátý znak se využije pro
označení konce řetězce '\0'
.
A ještě jedna ukázka:
Zadejte text: Ahoj světe! Nacteno: 'Ahoj'
Funkce scanf()
čte řetězec až k oddělovači (bílým znakům), takže
načte jen Ahoj.
Existují i jiné funkce pro načítání vstupu od uživatele, šikovnější než
scanf()
(třeba v tom, jak se jim předává maximální délka řetězce),
které načtou řetězec až ke konci řádku ('\n'
). S těmi vás
seznámím později.