Statické pole
Pole v automatické paměti1 (na zásobníku) se označují jako statická pole (static arrays). Můžeme je vytvořit tak, že za název proměnné přidáme hranaté závorky s číslem udávající počet prvků v poli. Takto například vytvoříme pole celých čísel s třemi prvky:
1Pole můžeme tímto způsobem vytvořit také v globální paměti.
int pole[3];
Takováto proměnná bude obsahovat paměť pro 3 celá čísla (tedy nejspíše na vašem počítači dohromady 12 bytů). Počet prvků v poli se označuje jako jeho velikost (size).
Pozor na to, že hranaté závorky se udávají za název proměnné, a ne za název datového typu.
int[3] pole;
je tedy špatně.
Čísla takového pole budou v paměti uložena jeden za druhým2:
2Každý zelený čtverec na tomto obrázku reprezentuje 4 bytů v paměti (velikost jednoho int
u).
V jistém smyslu je tak pole pouze zobecněním normální proměnné. Pokud totiž vytvoříte pole o
velikosti jedna (int a[1]
), tak v paměti bude reprezentováno úplně stejně jako klasická proměnná
(int a
).
Pole lze vytvořit také na haldě pomocí dynamické alokace paměti. Všechny níže popsané koncepty jsou platné i pro dynamická pole, nicméně budeme je demonstrovat na statických polích, protože ty je jednodušší vytvořit.
Počítání od nuly
Pozice jednotlivých prvků v poli se označují jako jejich indexy (array indices). Tyto pozice
se číslují od hodnoty 0
(tedy ne od jedničky, jak můžete být jinak zvyklí). První prvek pole je
tedy ve skutečnosti na nulté pozici (indexu), druhý na první pozici, atd. (viz obrázek nahoře).
Počítání od nuly (zero-based indexing) je ve světě programování běžné a budete si na něj
muset zvyknout. Jeden z důvodů, proč se prvky počítají právě od nuly, se dozvíte
níže.
Z tohoto vyplývá jedna důležitá vlastnost - poslední prvek pole je vždy na indexu
<velikost pole> - 1
! Pokud byste se pokusili přistoupit k prvku na indexu <velikost pole>
,
budete přistupovat mimo paměť pole, což pravděpodobně způsobí
paměťovou chybu.
Konstantní velikost statického pole
Hodnota zadaná v hranatých závorkách by měla být konstantní (tj. buď přímo číselná hodnota anebo konstantní proměnná). Pokud budete potřebovat pole dynamické velikosti, tak byste měli použít manuální alokaci paměti.
Jazyk C od verze C99 již sice povoluje dávat do hranatých závorek i "dynamické hodnoty":
int velikost = ...; // velikost se načte např. ze souboru
int pole[velikost];
Nicméně tuto funkcionalitu není vhodné používat. Zásobník má omezenou velikost a není určen pro alokaci velkého množství paměti3. Pokud navíc velikost takovéhoto pole může ovlivnit uživatel programu (např. zadáním vstupu), může váš program jednoduše "shodit", pokud by zadal velké číslo a došlo by k pokusu o vytvoření velkého pole na zásobníku. Zkuste se tak vyvarovat používání dynamických hodnot při vytváření polí na zásobníku.
3Můžete si například zkusit přeložit následující program:
int main() {
int pole[10000000];
return 0;
}
Při spuštění by měl program selhat na
paměťovou chybu, i když váš počítač má
pravděpodobně více než 10000000 * 4
(cca 38
MiB) paměti. Pokud chcete alokovat více než několik
stovek bytů, použijte raději dynamickou alokaci na haldě.
Inicializace pole
Stejně jako u normálních lokálních proměnných platí, že pokud pole nenainicializujete, tak bude obsahovat nedefinované hodnoty. V takovém případě z pole nesmíte jakkoliv číst, jinak by došlo k nedefinovanému chování 💣! K inicializaci hodnoty můžete použít složené závorky se seznamem hodnot (oddělených čárkou), které budou do pole uloženy. Pokud nezadáte dostatek hodnot pro vyplnění celého pole, tak zbytek hodnot bude nulových.
int a[3]; // pole bez definované hodnoty, nepoužívat!
int b[3] = {}; // pole s hodnotami 0, 0, 0
int c[4] = { 1 }; // pole s hodnotami 1, 0, 0, 0
int d[2] = { 2, 3 }; // pole s hodnotami 2, 3
Hodnot samozřemě nesmíte zadat více, než je velikost pole.
Pokud využijete inicializaci statického pole, můžete vynechat velikost pole v hranatých závorkách. Překladač v tomto případě dopočítá velikost za vás:
int p[] = { 1, 2, 3 }; // p je pole s třemi čísly
Přístup k prvkům pole
K přístupu k jednotlivým prvkům pole můžeme využít ukazatelů. Proměnná pole se totiž chová jako ukazatel na první prvek (prvek na nultém indexu) daného pole, pomocí operátoru dereference tak můžeme jednoduše přistoupit k prvnímu prvku pole:
#include <stdio.h>
int main() {
int pole[3] = { 1, 2, 3 };
printf("%d\n", *pole);
return 0;
}
Abychom přistoupili i k dalším prvkům v poli, tak můžeme využít
aritmetiky s ukazateli. Pokud chceme
získat adresu prvku na i
-tém indexu, stačí k ukazateli na první prvek přičíst i
4:
4Všimněte si, že při použití operátoru dereference zde používáme závorky. Je to z důvodu
priority operátorů. Výraz *pole + 2
by se vyhodnotil jako první prvek z pole pole
plus 2
, protože *
(dereference) má větší
prioritu než sčítání.
#include <stdio.h>
int main() {
int pole[3] = { 1, 2, 3 };
printf("%d\n", *(pole + 0)); // první prvek pole
printf("%d\n", *(pole + 1)); // druhý prvek pole
printf("%d\n", *(pole + 2)); // třetí prvek pole
return 0;
}
Nyní už možná tušíte, proč se při práci s poli vyplatí počítat od nuly. Prvek na nultém indexu je totiž vzdálen nula prvků od začátku pole. Prvek na prvním indexu je vzdálen jeden prvek od začátku pole atd. Pokud bychom indexovali od jedničky, museli bychom při výpočtu adresy relativně k ukazateli na začátek pole vždy odečíst jedničku, což by bylo nepraktické.
Operátor přístupu k poli
Jelikož je operace přístupu k poli ("posunutí" ukazatele a jeho dereference) velmi
běžná (a zároveň relativně krkolomná), C obsahuje speciální operátor, který jej zjednodušuje.
Tento operátor se nazývá array subscription operator a má syntaxi <výraz a>[<výraz b>]
. Slouží
jako zkratka5 za *(<výraz a> + <výraz b>)
. Například pole[0]
je ekvivalentní výrazu
*(pole + 0)
, pole[5]
je ekvivalentní výrazu *(pole + 5)
atd:
5Takovéto "zkratky", které v programovacím jazyku nepřináší novou funkcionalitu, pouze zkracují či zjednoduššují často používané kombinace příkazů, se označují jako syntax sugar.
int pole[3] = { 1, 2, 3 };
pole[0] = 5; // nastavili jsme první prvek pole na hodnotu `5`
int c = pole[2]; // nastavili jsme `c` na hodnotu posledního prvku pole
Jelikož je používání hranatých závorek přehlednější než používání závorek a hvězdiček, doporučujeme je používat pro přistupování k prvkům pole, pokud to půjde.
Pozor na rozdíl mezi tímto operátorem a definicí pole. Obojí sice používá hranaté závorky, ale jinak spolu tyto dvě věci nesouvisejí. Podobně jako se
*
používá pro definici datového typu ukazatele a zároveň jako operátor dereference (navíc i jako operátor pro násobení). Vždy záleží na kontextu, kde jsou tyto znaky použity.
Použití polí s cykly
Pokud bychom k polím přistupovali po individuálních prvcích, tak bychom nemohli využít jejich plný
potenciál. I když umíme jedním řádkem kódu vytvořit například 100 různých hodnot (int pole[100];
),
pokud bychom museli psát pole[0]
, pole[1]
atd. pro přístup k jednotlivým prvkům, tak bychom
nemohli s polem efektivně pracovat. Smyslem polí je zpracovat velké množství dat jednotným způsobem
pomocí malého množství kódu. Jinak řečeno, chtěli bychom mít stejný kód, který umí zpracovat
pole o velikosti 2
i 1000
. K tomu můžeme efektivně využít cykly.
Velmi často je praktické použít řídící proměnnou cyklu k tomu, abychom pomocí ní indexovali pole.
Například, pokud bychom měli pole s velikostí 10
, tak ho můžeme "projít" pomocí cyklu for
:
#include <stdio.h>
int main() {
int pole[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (int i = 0; i < 10; i++) {
printf("%d ", pole[i]);
}
return 0;
}
Situace, kdy pomocí cyklu procházíte pole je velmi častý a určitě se s ním mnohokrát setkáte a použijete jej. Zkuste si to procvičit například pomocí těchto úloh.
Předávání pole do funkcí
Při předávání polí do funkcí si musíme dávat pozor zejména na dvě věci.
Převod pole na ukazatel
Už víme, že když předáváme argumenty do funkcí, tak se jejich hodnota zkopíruje. U statických polí tomu tak ovšem není, protože pole můžou být potenciálně velmi velká a provádění kopií polí by tak potenciálně mohlo trvat dlouhou dobu. Když tak použijeme proměnnou pole jako argument při volání funkce, dojde k tzv. konverzi pole na ukazatel (array to pointer decay). Pole se tak vždy předá jako ukazatel na jeho první prvek:
#include <stdio.h>
void vypis_pole(int* pole) {
printf("%d\n", pole[0]);
}
int main() {
int pole[3] = { 1, 2, 3 };
vypis_pole(pole);
return 0;
}
Pro parametry sice můžete použít datový typ pole:
void vypis_pole(int pole[3]) { ... }
nicméně i v tomto případě se bude takovýto parametr chovat stejně jako ukazatel (v tomto případě
tedy int*
). Navíc překladač ani nebude kontrolovat, jestli do takového parametru opravdu dáváme
pole se správnou velikostí. Pro parametry reprezentující pole tak radši používejte ukazatel.
Předávání velikosti pole
Když ve funkci přijmeme jako parametr ukazatel na pole, tak nevíme, kolik prvků v tomto poli je. Tato informace je ale stěžejní, bez ní totiž nevíme, ke kolika prvkům pole si můžeme dovolit přistupovat. Pokud tedy ukazatel na pole předáváme do funkce, je obvykle potřeba zároveň s ním předat i délku daného pole:
int secti_pole(int* pole, int velikost) {
int soucet = 0;
for (int i = 0; i < velikost; i++) {
soucet += pole[i];
}
return soucet;
}
Výpočet velikosti pole
Abyste při změně velikosti statického pole nemuseli ručně jeho velikost upravovat na více místech v
kódu, tak můžete ve funkci, kde definujete statické pole, vypočítat jeho velikost pomocí operátoru
sizeof
:
int pole[3] = { 1, 2, 3 };
printf("Velikost pole v bytech: %lu\n", sizeof(pole));
Abyste zjistili počet prvků ve statickém poli, můžete velikost v bytech vydělit velikostí každého prvku v poli:
int pole[3] = { 1, 2, 3 };
printf("Pocet prvku v poli: %lu\n", sizeof(pole) / sizeof(pole[0]));
Operátor
sizeof
bude pro toto použití fungovat pouze pro statické pole a pouze ve funkci, ve které statické pole vytváříte! Pokud pole pošlete do jiné funkce, už z něj bude pouze ukazatel, pro kterýsizeof
vrátí velikost ukazatele (což bude na vašem PC nejspíše8
bytů).