Dynamická paměť

Už víme, že pomocí automatické paměti na zásobníku nemůžeme alokovat velké množství paměti a nemůžeme ani alokovat paměť s dynamickou velikostí (závislou na velikosti vstupu programu). Abychom tohoto dosáhli, tak musíme použít jiný mechanismus alokace paměti, ve kterém paměť alokujeme i uvolňujeme manuálně.

Tento mechanismus se nazývá dynamická alokace paměti (dynamic memory allocation). Pomocí několika funkcí standardní knihovny C můžeme naalokovat paměť s libovolnou velikosti. Tato paměť je alokována v oblasti paměti zvané halda (heap). Narozdíl od zásobníku, prvky na haldě neleží striktně za sebou, a lze je tak uvolňovat v libovolném pořadí. Můžeme tak naalokovat paměť libovolné velikosti, která přežije i ukončení vykonávání funkce, díky čemuž tak můžeme sdílet (potenciálně velká) data mezi funkcemi. Nicméně musíme také tuto paměť ručně uvolňovat, protože (narozdíl od zásobníku) to za nás nikdo neudělá.

Alokace paměti

K naalokování paměti můžeme použít funkci malloc (memory alloc), která je dostupná v souboru stdlib.h ze standardní knihovny C. Tato funkce má následující signaturu1:

1Datový typ size_t reprezentuje bezznaménkové celé číslo, do kterého by měla jít uložit velikost největší možné hodnoty libovolného typu. Často se používá pro indexaci polí nebo právě určování velikosti (např. alokací).

void* malloc(size_t size);

Velikost alokované paměti

Parametr size udává, kolik bytů paměti se má naalokovat. Tuto velikost můžeme "tipnout" manuálně, nicméně to není moc dobrý nápad, protože bychom si museli pamatovat velikosti datových typů (přičemž jejich velikost se může lišit v závislosti na použitém operačním systému či překladači!). Abychom tomu předešli, tak můžeme použít operátor sizeof, kterému můžeme předat datový typ2. Tento výraz se poté vyhodnotí jako velikost daného datového typu:

2Případně výraz, v tom případě si sizeof vezme jeho datový typ.

#include <stdio.h>
int main() {
    printf("Velikost int je: %lu\n", sizeof(int));
    printf("Velikost int* je: %lu\n", sizeof(int*));
    return 0;
}

Návratový typ void* reprezentuje ukazatel na libovolná data. Funkce malloc musí fungovat pro alokaci libovolného datového typu, proto musí mít jako návratový typ právě univerzální ukazatel void*. Při zavolání funkce malloc bychom měli tento návratový typ přetypovat na ukazatel na datový typ, který alokujeme.

Při zavolání mallocu dojde k naalokování size bytů na haldě. Adresa prvního bytu této naalokované paměti se poté vrátí jako návratová hodnota mallocu. Zde je ukázka programu, který naalokuje paměť pro jeden int ve funkci, adresu naalokované paměti poté vrátí jako návratovou hodnotu a naalokovaná paměť je poté přečtena ve funkci main:

#include <stdlib.h>

int* naalokuj_pamet() {
    int* pamet = (int*) malloc(sizeof(int));
    *pamet = 5;
    return pamet; 
}
int main() {
    int* pamet = naalokuj_pamet();
    printf("%d\n", *pamet);

    free(pamet); // uvolnění paměti, vysvětleno níže

    return 0;
}
Interaktivní vizualizace kódu

Iniciální hodnota paměti

Stejně jako u lokálních proměnných, i u dynamicky naalokované paměti platí, že její hodnota je zpočátku nedefinovaná. Než se tedy hodnotu dané paměti pokusíte přečíst, musíte jí nainicializovat zápisem nějaké hodnoty! Jinak bude program obsahovat nedefinované chování 💣.

Pokud byste chtěli, aby naalokovaná paměť byla rovnou při alokaci vynulována (všechny byty nastavené na hodnotu 0), můžete místo funkce malloc použít funkci calloc3. Případně můžete použít užitečnou funkci memset, která vám vyplní blok paměti zadaným bytem.

3Pozor však na to, že tato funkce má jiné parametry než malloc. Očekává počet hodnot, které se mají naalokovat, a velikost každé hodnoty.

Uvolnění paměti

S velkou mocí přichází i velká zodpovědnost, takže při použití dynamické paměti sice máme více možností, než při použití automatické paměti (resp. zásobníku), ale zároveň MUSÍME tuto paměť korektně uvolňovat (což se u automatické paměti provádělo automaticky). Pokud bychom totiž paměť neustále pouze alokovali a neuvolňovali, tak by nám brzy došla.

Abychom paměť naalokovanou pomocí funkcí malloc či calloc uvolnili, tak musíme použít funkci free:

#include <stdlib.h>

int main() {
    int* p = (int*) malloc(sizeof(int)); // alokace paměti
    *p = 0;                              // použití paměti
    free(p);                             // uvolnění paměti

    return 0;
}

Jako argument této funkci musíme předat ukazatel navrácený z volání malloc/calloc. Nic jiného do této funkce nedávejte, uvolňovat můžeme pouze dynamicky alokovanou paměť! Nevolejte free s adresami např. lokálních proměnných4.

4Je však bezpečné uvolnit "nulový ukazatel", tj. free(NULL) je validní (v tomto případě funkce nic neudělá).

Jakmile se paměť uvolní, tak už k této paměti nesmíte přistupovat! Pokud byste se pokusili přečíst nebo zapsat uvolněnou paměť, tak dojde k nedefinovanému chování 💣. Nesmíte ani paměť uvolnit více než jednou.

Při práci s dynamicky alokovanou pamětí tak dbejte zvýšené opatrnosti a ideálně používejte při vývoji Address sanitizer. (Neúplný) seznam věcí, které se můžou pokazit, pokud kombinaci dynamické alokace a uvolňování paměti pokazíte, naleznete zde.

Alokace více hodnot zároveň

Jak jste si mohli všimnout ze signatury funkce malloc, můžete jí dát libovolný počet bytů. Nemusíte se tak omezovat velikostí základních datových typů, můžete například naalokovat paměť pro 5 intů zároveň, které poté budou ležet za sebou v paměti a bude tak jednoduché k nim přistupovat v cyklu. Jak tento koncept funguje se dozvíte v sekci o dynamických polích.

Kdy použít dynamicky alokovanou paměť?

Řiďte se pravidlem, že pokud lze použít automatickou paměť na zásobníku, tak ji využijte a malloc nepoužívejte. Až v momentě, kdy z nějakého důvodu nebude stačit naalokovat paměť na zásobníku, tak se obraťe na malloc.

Seznam situací, ve kterých se může dynamická paměť hodit, se nachází v sekci o automatické paměti.


Kvíz 🤔

Podívejte se na sekci o paměťových chybách pro příklad toho, co všechno se může při práci s dynamickou pamětí a ukazateli pokazit.