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 při definici proměnné za její název přidáme hranaté závorky s číslem udávajícím počet prvků v poli. Takto například vytvoříme pole celých čísel s třemi prvky:
1Pole můžete tímto způsobem vytvořit také v globální paměti, pokud vytvoříte globální proměnnou datového typu pole.
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 jedno za druhým2:
2Každý zelený čtverec na tomto obrázku reprezentuje 4 byty 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.
Konstantní velikost statického pole
Hodnota zadaná v hranatých závorkách by měla být "konstantním výrazem", tj. buď přímo číselná hodnota anebo číselná hodnota pocházející z makra3. Pokud budete potřebovat pole dynamické velikosti, tak byste měli použít dynamickou alokaci paměti.
3Dokonce ani konstantní proměnná není v C "konstantním výrazem".
Jazyk C od verze C99 již sice povoluje dávat do hranatých závorek i "dynamické" hodnoty, tj. výrazy, jejichž hodnota nemusí být známa v době překladu:
int velikost = ...; // velikost se načte např. ze souboru
int pole[velikost];
Tato funkcionalita zvaná VLA (variable-length array)
je nicméně určená pro velmi specifické použití a nese s sebou různé nevýhody, proto ji v rámci předmětu
UPR nepoužívejte. Pokud si chcete být jisti, že se VLA ve vašem kódu nevyskytuje, překládejte své programy s
parametrem překladače -Werror=vla
.
Proč ne VLA?
Zásobník má značně omezenou velikost a není určen pro alokaci velkého množství paměti4. Pokud velikost takovéhoto pole může ovlivnit uživatel programu (např. zadáním vstupu), může váš program jednoduše "shodit" (v lepším případě) nebo způsobit přepsání existující paměti (v horším případě), pokud by zadal velké číslo a došlo by k pokusu o vytvoření moc velkého pole na zásobníku. VLA má také různé problémy s kompatibilitou mezi překladači a jeho implementace překladači není zdaleka triviální.
4Můž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ě.
Alokace paměti s dynamickou velikostí na zásobníku se může hodit ve velmi specifických případech, např. při vývoji embedded zařízení nebo při vysoce efektivní práci s I/O (vstup/výstup). Nicméně pro účely běžného programování v C a předmětu UPR rozhodně není potřeba, proto se VLA prosíme zkuste vyhnout.
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ž způsobí
paměťovou chybu.
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ě nesmíte hodnoty v poli jakkoliv číst, jinak by došlo k nedefinovanému chování 💣! K inicializaci pole 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 nastaveno na nulu.
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ě nemůžete 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řekladač si odvodí int p[3]
Přístup k prvkům pole
Abychom využili toho, že nám pole umožňují vytvořit větší množství paměti najednou, musíme mít možnost přistupovat k jednotlivým prvkům v poli. K tomu 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 k tomutu prvku můžeme jednoduše přistoupit:
#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
5:
5Vš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é.
Přistupování k prvkům pole se běžně označuje pojmem indexování pole.
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ý ji zjednodušuje. Tento operátor se nazývá array subscription operator a má syntaxi
<výraz a>[<výraz b>]
Slouží jako zkratka6 za výraz
6Takové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 syntactic sugar.
*(<výraz a> + <výraz b>)
Příklad:
pole[0]
je ekvivalentní výrazu*(pole + 0)
pole[5]
je ekvivalentní výrazu*(pole + 5)
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 (třetí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 umožnit zpracování velkého množství dat jednotným
způsobem pomocí krátkého kusu 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.
Č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"7 pomocí cyklu for
:
7Používá se také pojem proiterovat.
#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 projdeme pole, je velmi častá a určitě se s ní mnohokrát setkáte a využijete ji. Zkuste si to procvičit například pomocí těchto úloh.
Předávání pole do funkcí
Pole můžeme (stejně jako hodnoty jiných datových typů) předávat jako argumenty do funkcí. Musíme si při tom však 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 brzdit provádění programu. 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ěji rovnou používejte ukazatel,
abychom čtenáře kódu nemátli.
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ů). Více v kvízech níže.
Kvíz 🤔
-
Co vypíše následující program?
#include <stdio.h> int main() { int pole[3] = { 1, 4, 7 }; int a = *pole + 1; int b = *(pole + 1); printf("a = %d, b = %d\n", a, b); return 0; }
Odpověď
Program vypíše
a = 2, b = 4
. Jelikož má operátor dereference (*
) větší prioritu než operátor sečtení (+
), tak se do proměnnéa
uloží hodnota (2
). Nejprve se totiž provede výraz*pole
, kde dojde k dereferenci ukazatele na první prvek pole, čímž vznikne hodnota1
, a k ní se poté přičte jednička.V případě proměnné
b
se nejprve ukazatel na první prvek pole posune o jeden prvek dopředu, tj. na adresu druhého prvku pole, který má hodnotu4
. Poté dojde k dereferenci adresy tohoto prvku, do proměnnéb
se tak uloží hodnota4
. -
Co vypíše následující program?
#include <stdio.h> void prijmi_pole(int p[3]) { p[2] += 1; } int main() { int pole[3] = { 1, 2, 3 }; prijmi_pole(pole); printf("{ %d, %d, %d }\n", pole[0], pole[1], pole[2]); return 0; }
Odpověď
Program vypíše
{ 1, 2, 4 }
. Při předávání statického pole do funkce dojde pouze k předání ukazatele na jeho první prvek (i když má parametr typint p[3]
). Pokud tedy pomocí ukazatelep
změníme hodnotu třetího prvku pole, tato změna se nám projeví i ve funkcimain
, protože stále pracujeme s tou stejnou pamětí. -
Co vypíše následující program?
#include <stdio.h> int main() { int pole[3] = { 1, 2, 3 }; int *p = pole; p[1] = 5; pole[0] = 8; printf("%d, %d\n", *p, pole[1]); return 0; }
Odpověď
Program vypíše
8, 5
. Do ukazatelep
jsme si uložili adresu prvního prvku v poli. Pomocíp[1]
posuneme ukazatel o jeden prvek v paměti "dopředu" (bude tedy ukazovat na druhý prvek pole) a rovnou na tuto adresu v paměti zapíšeme hodnotu5
. Poté změníme hodnotu prvního prvku pole na8
. Jelikožp
ukazuje na první prvek v poli, tak při jeho dereferenci získáme právě hodnotu8
. A jelikož jsme předtím pomocí ukazatelep
změnili druhý prvek pole na5
, takpole[1]
také vrátí hodnotu5
. -
Co vypíše následující program?
#include <stdio.h> int main() { int pole[3] = { 1, 2, 3 }; printf("%d\n", pole); return 0; }
Odpověď
Tento program obsahuje nedefinované chování 💣, protože jsme použili zástupný znak
%d
, který slouží k výpisu celých čísel, ale předali jsme funkciprintf
argumentpole
, který je datového typu pole (resp. ukazatel na první prvek tohoto pole). -
Co vypíše následující program?
#include <stdio.h> int main() { int p[3] = { 1, 2, 3 }; for (int i = 0; i <= 3; i++) { printf("%d\n", p[i]); } return 0; }
Odpověď
Tento program obsahuje nedefinované chování 💣, protože jsme přistoupili (dereferencovali) paměť mimo rozsah pole! Pole
p
má pouze tři prvky, nesmíme tedy přistoupit k indexu3
či vyššímu, což se však v tomto programu stane, protože proměnnái
nabývá hodnot0
,1
,2
a3
.Ať už tento program při konkrétním spuštění vypíše cokoliv, nemá cenu se tím zaobírat. Tento program obsahuje paměťovou chybu, která může způsobit pád programu, libovolnou změnu hodnot v paměti nebo cokoliv jiného. Chybu musíte nejprve odstranit, jinak program nebude správně fungovat.
-
Co vypíše následující program?
#include <stdio.h> int main() { int pole[3] = { 1, 2, 3 }; 2[pole] = 5; printf("%d\n", pole[2]); return 0; }
Odpověď
Program vypíše
5
. I když to vypadá zvláštně, tak jelikož je sčítání komutativní, a operátora[b]
je definován jako*(a + b)
, tak je jedno, jestli napíšetea[b]
nebob[a]
. Takovýto zápis je nicméně nestandardní a nepoužívá se, tato úloha pouze měla demonstrovat, že jej takto teoreticky použít lze, a žea[b]
opravdu není nic jiného, než zkratka za*(a + b)
. -
Co vypíše následující program?
#include <stdio.h> int main() { char pole[3]; char* ptr = pole; printf("%d\n", (int) sizeof(pole)); printf("%d\n", (int) sizeof(ptr)); return 0; }
Odpověď
Program vypíše toto (na 32-bitovém systému by druhé číslo bylo pravděpodobně 4):3 8
Operátor
sizeof
vrátí velikost celého statického pole, pokud jej do něj předáme. Pokud však do něj dáme pouze ukazatel, taksizeof
neví, jak velká paměť leží na adrese uložené v tomto ukazateli, proto nám místo toho vrátí pouze velikost daného ukazatele, což bude na 64-bitovém systému pravděpodobně8
bytů. -
Co vypíše následující program?
#include <stdio.h> void print_size(char pole[3]) { printf("%d\n", (int) sizeof(pole)); } int main() { char pole[3]; print_size(pole); return 0; }
Odpověď
Program pravděpodobně vypíše řádek s hodnotou 8 (na 64-bitovém systému) či 4 (na 32-bitovém systému). Pokud použijeme datový typ pole jako parametr funkce, tak se k němu překladač bude víceméně chovat jako k běžnému ukazateli. Je to z toho důvodu, že překladač neví, jak velkou paměť do funkce předáváme (můžeme tuto funkci zavolat s ukazatelem na různě velká pole!). Z toho důvodu je tak lepší pro parametry funkcí vždy používat rovnou ukazatel a ne pole, abychom zamezili nejasnostem.Tyto následující tři signatury funkce jsou tedy v podstatě totožné:
void print_size(char pole[3]); void print_size(char pole[]); void print_size(char* pole);