Lekce 2 - Dynamická alokace paměti v jazyce C
V minulé lekci, Úvod do ukazatelů v jazyce C, jsme si uvedli pointery v jazyce C. Již víme, že nás jazyk C nechá pracovat s pamětí a naučili jsme se předávat parametry funkcí referencí.
To pravé programování v Céčku ovšem rozjedeme až v dnešním tutoriálu. Pochopíme totiž, jak funguje přidělování paměti a vymaníme se ze všech limitů délky statických polí a řetězců.
Statická a dynamická alokace paměti
Jak víme, o paměť si musí náš program říkat operačnímu systému, což není úplně snadné a proto se toho Céčko snaží udělat co nejvíce za nás.
Staticky alokovaná paměť
Když se náš program překládá, může překladač ve velkém množství
případů jednoduše zjistit, kolik paměti bude při běhu programu třeba.
Když si vytvoříme proměnnou typu int
, Céčko ví, že na ni
má vyhradit 32 bitů. Když si vytvoříme pole o 100 znacích, Céčko opět
ví, že má rezervovat 800 bitů. Pokud není při běhu programu třeba
žádná data přidávat, s touto automatickou alokací si bohatě vystačíme.
To je v podstatě způsob, jakým jsme programovali doposud.
Dynamicky alokovaná paměť v zásobníku
Možná vás napadlo, jak v jazyce C funguje přidělování paměti pro
lokální proměnné (to jsou ty definované uvnitř funkcí). Céčko přeci
neví kolikrát funkci zavoláme a tedy kolik proměnných bude ve finále
potřeba. Tato paměť je opravdu přidělována dynamicky až
za běhu programu. Vše se ovšem děje zas plně automaticky. Jakmile funkci
zavoláme, Céčko si řekne o paměť a jakmile funkce skončí, tato paměť
se uvolní. Toto je důvod, proč funkce nemůže vracet pole v ní vytvořené.
Jak již víme, pole se totiž nekopíruje (nepředává hodnotou jako třeba
int
), ale pracuje se s ním jako by to byl ukazatel. Jelikož
lokální proměnné po skončení funkce zaniknou, získali bychom ukazatel
někam, kde žádné pole již nemusí existovat.
Dynamicky alokovaná paměť v haldě
Zatím to vypadá, že Céčko dělá vše za nás. Kde je tedy problém? Představte si, že programujete aplikaci, která eviduje např. položky ve skladu. Víte dopředu, jak velké pole pro položky vytvořit? Bude jich 100, 1000, miliony? Pokud budeme deklarovat statické pole nějakých struktur, vždy budeme buď plýtvat místem nebo se vystavíme nebezpečí, že nám vyhrazené pole přestane stačit. A nemusíme ani zacházet do této situace, stačí zapřemýšlet nad tím, jak uložit textový řetězec přesně tak dlouhý, jak ho uživatel zadal.
Problém tedy tkví v tom, že někdy do spuštění programu nevíme kolik paměti bude třeba a proto ji za nás Céčko nemůže alokovat. Naštěstí nám ovšem nabízí funkce, kterými si můžeme za běhu programu říkat o libovolné množství paměti.
V textu byly zmíněny pojmy zásobník (stack) a halda (heap). Jedná se o 2 typy paměti v RAM, se kterými program pracuje. Zjednodušeně můžeme říci, že práce se zásobníkem je rychlejší, ale je velikostně omezený. Halda je určena primárně pro větší data, např. pro zmíněné položky ve skladu. Když si budeme říkat o paměť my sami, bude přidělena vždy na haldě.
Dynamická alokace paměti
Těžištěm práce s dynamickou pamětí je v jazyce C dvojice funkcí - malloc() a free().
malloc()
malloc()
řekne operačnímu systému o libovolné množství
paměti (kolik jí potřebujeme uvedeme do parametru funkce v
bajtech). Funkce vrátí pointer na první adresu, kde
začíná naše nová paměť. Již víme, že každý pointer má určitý typ,
resp. ukazuje na data nějakého typu. Aby byla funkce malloc()
univerzální, vrací pointer na typ void
. Její výsledek bychom
měli vždy přetypovat na takový pointer, který potřebujeme.
Pokud se alokace paměti nepodaří (např. nám došla, což se dnes již
teoreticky nestane, ale měli bychom s tímto případem počítat), vrátí
malloc()
hodnotu NULL
(tedy pointer nikam). Při
alokování paměti budeme vždy používat funkci sizeof()
,
protože nikdy nevíme jak je datový typ na daném operačním systému velký
(např. int
může zabírat 16 i 32 bitů). Volání
sizeof()
jsou nahrazena konstantami při kompilaci, takže nijak
negativně neovlivňují rychlost programu.
free()
Po každém zavolání funkce malloc()
musí někdy (třeba až
na konci programu) následovat zavolání funkce free()
, která
paměť označí opět jako volnou. Tato paměť je plně v naší režií a
nikdo jiný než my ji za nás neuvolní. Jakmile přestaneme nějakou dynamicky
alokovanou paměť potřebovat, měli bychom ji ihned uvolnit.
Alokujme si za běhu programu místo pro 100 int
ů:
int main(int argc, char** argv) { int *p_i; printf("Pokouším se alokovat paměť pro 100 intů.\n"); // Alokace 100 krát velikosti intu p_i = (int *) malloc(sizeof(int) * 100); // Kontrola úspěšnosti alokace if (p_i == NULL) { printf("Nedostatek paměti.\n"); exit(1); } // Uvolnění paměti printf("Uvolňuji paměť.\n"); free(p_i); p_i = NULL; // Pro jistotu vynullujeme ukazatel return (EXIT_SUCCESS); }
Výstup:
c_malloc
Pokouším se alokovat paměť pro 100 intů.
Uvolňuji paměť.
Zatím ještě nevíme jak k jednotlivým int
ům v paměti
přistupovat, vše si vysvětlíme hned příště. Funkce exit()
ukončí naši aplikaci. V parametru předáme chybový kód, který by měl
být v případě že program nedoběhl správně, nenulový.
Časté chyby při práci s pointery
Práce s ukazateli je poměrně nebezpečná, jelikož nás programátory při ní nikdo nehlídá. A udělat chybu v programu je velmi jednoduché a to člověk ani nemusí být začátečníkem. Zmiňme si několik bodů, na které je dobré při práci s ukazateli dávat pozor.
- Neuvolnění paměti - Pokud jednou zapomeneme uvolnit nějakou paměť, tak se v zásadě nic nestane. Problém je v případě, kdy paměť zapomeneme uvolnit uvnitř nějaké funkce, která se za běhu programu volá několikrát. Nejhorší situace je, když paměť zapomeneme uvolnit v nějakém cyklu. Paměť nám při této chybě samozřejmě za nějakou dobu dojde, aplikace spadne a uživatel přijde o data a zaplatí raději konkurenci, aby mu prodala funkční aplikaci
- Překročení hranic paměti - Stejně jako tomu bylo u polí, ani u pointerů nikdo nehlídá co do této paměti ukládáme. Pokud uložíme něco většího, než kolik místa máme vyhrazeno, nabouráme paměť jiné části aplikace. Tato chyba se může projevit naprosto kdekoli a pravděpodobně ji budeme velmi dlouho hledat. Následná chyba totiž nijak logicky nesouvisí s místem v programu, kde nám paměť přetekla. Může se najednou rozbít jakákoli část aplikace, protože do ní tato paměť vytekla
- Práce s uvolněnou pamětí - Může se nám stát, že
nějakou paměť uvolníme a poté se na tuto adresu pokusíme znovu něco
zapsat. V tu chvíli však zapisujeme opět na paměť, která nám nepatří,
následky viz. minulý bod. Proto je dobré uložit do ukazatele po uvolnění
jeho paměti hodnotu
NULL
, abychom se této chybě vyvarovali.
V příští lekci, Aritmetika ukazatelů v jazyce C, se naučíme tzv. pointerovou aritmetiku a zjistíme, že ukazatele v jazyce C jsou polím ještě podobnější, než jsme si mysleli.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 138x (31.81 kB)
Aplikace je včetně zdrojových kódů v jazyce C