Dynamická alokace paměti
V této kapitole se již nebudete učit nové konstrukce jazyka C, ale ukáži vám některé funkce ze standardní knihovny. Konkrétně funkce pro získávání dynamické paměti.
Každý program lze v zásadě rozdělit na dvě části. Na část
datovou, kde jsou uloženy data, se kterými program pracuje (tj.
proměnné, textové literály, konstanty) a na část programovou,
která obsahuje instrukce programu.
Ihned po spuštění programu jsou data která ukládáte do proměnných
uložena v paměti počítače. Z této paměti lze data číst a lze do ní i
zapisovat.
Takto získaná paměť má své pro i proti. Výhodou je v zásadě rychlost a zaručená dostupnost paměti. Ovšem jsou situace, kdy dopředu nemůžete vědět, kolik budete paměti potřebovat. Například budete chtít program, který načte od uživatele několik čísel a pak je setřídí od nejmenšího do největšího. Jelikož dopředu nevíte, kolik čísel bude uživatel chtít setřídit, můžete to jenom odhadnout a vytvořit si pro tyto čísla pole dlouhé např. 1000 položek typu float. To už je velké pole, které zvětší velikost programu a zpomalí jeho chod, přičemž se může stát, že uživatel bude využívat v průměru třeba jen 10 položek. Na druhou stranu se také může stát, že bude potřebovat 1001 položek.
Naštěstí nejste odkázáni jen na paměť představovanou proměnnými, které definujete ve zdrojovém kódu (taková místa v paměti se nazývají statická). Můžete žádat o paměť pro proměnnou (nebo celá pole proměnných) za chodu programu. A tomu se říká „dynamická alokace paměti“.
I dynamická alokace paměti má své výhody a nevýhody. Výhodou je, že program využívá právě tolik paměti, kolik potřebuje. Navíc získanou paměť můžete (měli byste) během programu uvolnit. Takže když například jednou potřebujete 10 MiB dat a podruhé 15 MiB dat, nejdříve požádáte o 10 MiB, využijete je, potom uvolníte, pak požádáte o těch potřebných 15 MiB dat … Takže i když jste potřebovali celkem 25 MiB dat, v jeden okamžik jste jich potřebovali maximálně 15.
Jistou nevýhodou je zvýšená pracnost s dynamickými strukturami (a časová režie při získávání a uvolňování paměti). Proto vždy rozvažte, zda se vyplatí (například pro ukládání řetězce čteného od uživatele) vytvářet dynamické data, nebo využít statických polí.
Získání a uvolnění paměti pomocí malloc() a free()
Nejdříve se podívejte na deklarace funkcí
malloc()
a free()
a pak popíši jejich funkce. Obě funkce najdete v hlavičkovém souboru
<stdlib.h>. Datový typ size_t
, který
se používá k určování velikosti získávané paměti, je
definován též v <stddef.h>. Je to datový typ definovaný pomocí
typedef.
Je to celočíselný typ, něco jako unsigned int
.
Máte zaručeno, že size_t
je dostatečně velký na to, aby se v něm
mohla uložit velikost paměti (přesněji řečeno velikost datového typu).
Proto používejte size_t
a ne unsigned int
,
který nemusí být na všech platformách dostatečně velký.
Funkce malloc()
má jako jediný
argument počet bajtů paměti, které chcete od operačního systému získat (alokovat).
Zajímavá je návratová hodnota, která je deklarována jako ukazatel
void *
. Asi vás už
napadlo, že s dynamicky alokovanou pamětí se bude pracovat pomocí
ukazatelů. Novou paměť vám totiž přidělí operační systém a funkce
malloc()
vrátí ukazatel na začátek paměti, která byla programu
operačním systémem přidělena. Samo sebou se může stát, že již není
dostatek volné paměti. V takovém případě funkce
malloc()
vrací hodnotu NULL
.
Vrácený ukazatel je třeba přetypovat na správný datový typ, který budete chtít v alokovaném datovém poli (nebo jedné proměnné) používat.
Paměť, kterou takto získáte, je souvislá oblast dlouhá
size_t bajtů s nedefinovaným obsahem. Jsou v ní tedy náhodné
hodnoty bajtů. Pokud použijete funkci
malloc()
vícekrát, získáte
několik souvislých oblastí, ale vzájemně již souvislé být nemusí a
také většinou nejsou. Maximální velikost dynamicky alokované paměti
je dána jednak fyzickými možnostmi vašeho počítače a také velikostí
adresování paměti. U 32 bitových programů je možné adresovat až
4GB, u 16 bitových je to samozřejmě méně a u 64 bitových více.
Jak jsem již v začátku kapitoly předeslal, dynamicky alokovanou
(získanou) paměť je možné operačnímu systému vrátit a tím snižovat
nároky programu na paměť. To se provádí funkcí
free()
. Tato
funkce má jako argument ukazatel na začátek alokované paměti (tedy
hodnotu, kterou vrací funkce malloc()
, nebo
calloc()
, nebo realloc()
, viz níže). Pokud
funkci free()
v programu nepoužijete,
je alokovaná paměť vrácena operačnímu systému až po skončení programu.
Přesto byste měli funkci free()
v programu vždy volat, a to co nejdříve to jde.
Pokud program neuvolňuje nepotřebnou paměť, dochází k takzvanému „úniku paměti“ (memory leaks). Déle běžící aplikace si tak říká o stále více a více paměti, až nakonec spotřebuje všechnu, která je dostupná. Takovými problémy často trpěla například Mozilla Firefox (ale i mnoho jiných profesionálních aplikací).
Po volání funkce free()
již paměť
není přidělena programu a pokus o zápis do této paměti
(nebo čtení z ní) by byl po
zásluze potrestán sejmutím programu nebo dokonce operačního
systému (pokud je tak hloupý a nechá si to líbit).
Následující zdrojový kód ukazuje použití funkcí malloc()
a
free()
.
Jde o program, který načte od uživatele číslo určující počet následně
zadaných čísel, které se poté setřídí. Všimněte si, jak je v programu
pomocí NULL kontrolováno, zda byla dynamická paměť skutečně alokována,
jak je přetypována návratová hodnota a jak se určuje velikost získávaného
datového pole.
- *------------------------------------------------*/
- /* c18/dynamic1.c */
- #define _CRT_SECURE_NO_WARNINGS
- #include <stdio.h>
- #include <stdlib.h>
- #include <stddef.h>
- #ifdef _MSC_VER
- #define ZU "Iu"
- #define SZU "lu"
- #else
- #define ZU "zu"
- #define SZU "zu"
- #endif
- /**
- * funkce pro setrideni pole od nejmensiho do nejvetsiho
- */
- {
- float pom;
- i = 0;
- do {
- pom = pole[i];
- pole[i] = pole[i + 1];
- pole[i + 1] = pom;
- }
- i++;
- }
- }
- {
- /*
- ukazatel na dynamicky alokovane cislo, ktere bude urcovat
- delku pole pro tridene cisla. V tomto pripade by bylo
- lepsi pouzit statickou promennou (size_t i);
- je to tu tak udelano jen pro nazorny priklad */
- /* ukazatel na dynamicky alokovane pole, do ktereho budou
- ulozeny tridene cisla */
- }
- /* u funkce scanf by se melo kontrolovat, zda skutecne
- nacetla to, co mela. Pro strucnost prikladu to nedelam */
- /* pozadam o tolik bytu, kolik ma
- typ float krat pocet prvku pole */
- /* taky zpusob kontroly :-) */
- {
- }
- }
- setrid(*ui, uf);
- }
- }
- /*------------------------------------------------*/
Makro _CRT_SECURE_NO_WARNINGS
je tu kvůli funkci scanf()
,
viz scanf().
Popis definic ZU
a SZU
viz datový typ pro ukazatel.
Výstup z programu:
Zadejte pocet cisel: 5 Zadejte cislo [ 1]: 4 Zadejte cislo [ 2]: -3.5 Zadejte cislo [ 3]: 3.8 Zadejte cislo [ 4]: 2 Zadejte cislo [ 5]: 0.5 -3.50 0.50 2.00 3.80 4.00
V druhém příkladě uvidíte, jak lze dynamicky vytvořit
dvourozměrné pole. Jeho velikost bude 100*200*sizeof(int)
bajtů.
Především si všimněte, jak je paměť uvolňována.
- /*------------------------------------------------*/
- /* c18/dynamic2.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include <stddef.h>
- #ifdef _MSC_VER
- #define ZU "Iu"
- #else
- #define ZU "zu"
- #endif
- #define X 100 /* pocet radku */
- #define Xv1 10 /* vypis radek Xv1 az Xv2 */
- #define Xv2 20
- #define Y 200 /* sloupce */
- #define Yv1 50 /* vypis sloupcu Yv1 az Yv2 */
- #define Yv2 60
- {
- }
- }
- /* jak byla pamet alokovana, tak musi byt dealokovana.
- * Jednoduseji to opravdu nejde. */
- }
- }
- /*------------------------------------------------*/
Pokud bych zavolal jen free(ui);
, uvolnila by se jen
paměť alokovaná prvním zavoláním funkce malloc()
. Navíc by
se tak ztratili všechny ukazatele na paměť získávanou v cyklu na řádku 32.
To by byla krásná ukázka úniku paměti.
Získání paměti pro struktury pomocí calloc()
Funkce calloc()
je deklarována následovně:
Funkce calloc()
alokuje paměť pro
nmemb položek velikosti size.
Velikost položky může být například sizeof(int)
.
Ovšem častěji se využívá pro vytvoření dynamického pole struktur.
Tato funkce, na rozdíl od funkce malloc(),
vyplní alokovanou paměť nulami.
Všechno ostatní je stejné jako u funkce malloc()
(návratová hodnota, uvolňování pomocí free()
atd.).
Musíte samozřejmě počítat s nějakou časovou režijí, kterou zabere nulování paměti.
Na příkladu ukáži, jak lze vytvářet seznam.
Seznam obsahuje datové objekty (struktury), které jsou mezi sebou
provázány ukazateli. První prvek seznamu ukazuje na druhý, druhý na
třetí atd. V příkladě bude prvek obsahovat (kromě ukazatele na
další prvek v seznamu) pole typu char
, do kterého bude ukládat slova načtené
od uživatele. Po skončení načítání se slova lexikograficky seřadí.
Struktura bude také obsahovat číslo určující kolikáté bylo slovo načteno.
K setřídění použiji funkci strcmp()
,
kterou popíši v kapitole věnované standardnímu souboru
<string.h>.
Tato funkce porovnává lexikograficky dva řetězce a vrací nulu, pokud jsou si rovny.
- /*------------------------------------------------*/
- /* c18/dynamic3.c */
- #define _CRT_SECURE_NO_WARNINGS
- #include <stdio.h>
- #include <stdlib.h>
- #include <stddef.h>
- #include <string.h>
- #include <stdbool.h>
- #define MAXSLOVO 20
- #define NACTISLOVO "%20s"
- struct veta {
- };
- /**
- * pomocna funkce na kopirovani retezce
- */
- {
- do {
- cil[i] = zdroj[i];
- }
- /**
- * funkce vypisujici seznam
- */
- {
- dalsi = zacatek;
- do {
- }
- dalsi = dalsi->dalsi; /* prejdi na dalsi slovo */
- }
- }
- {
- dalsi = zacatek;
- do {
- /* dosli jsme na konec a nic se nezmenilo?
- pak mame setrideno */
- dalsi = zacatek;
- }
- pom = *dalsi;
- strcopyruj(dalsi->slovo, dalsi->dalsi->slovo);
- dalsi->poradi = dalsi->dalsi->poradi;
- strcopyruj(dalsi->dalsi->slovo, pom.slovo);
- dalsi->dalsi->poradi = pom.poradi;
- }
- dalsi = dalsi->dalsi;
- }
- /**
- * Funkce pro uvolneni pameti. Prvni polozka je staticka, proto se
- * pomoci free() neuvolnuje a take se musi jeji atribut "dalsi"
- * nastavit na NULL */
- {
- v = zacatek->dalsi;
- pom = v->dalsi;
- v = pom;
- }
- zacatek->dalsi = NULL;
- }
- {
- int navrat;
- "Zadavani vet ukoncete pomoci znaku konec souboru.\n"
- "(tj. CTRL+D v unixech nebo CTRL+Z + Enter v DOSu a Windows)\n\n",
- MAXSLOVO);
- poradi = 0;
- /* Struktura "prvni" je prvni prvek ze seznamu. Ukazatel
- "posledni" ukazuje na posledni prvek v seznamu. */
- posledni = &prvni;
- do {
- poradi++;
- posledni->poradi = poradi;
- /* EOF * -> konec vstupu,
- * navrat !=1 -> nebyl nacten spravne retezec */
- /* dalsi prvek v seznamu jiz nebude. seznam ukoncime
- zarazkou NULL */
- posledni->dalsi = NULL;
- }
- }
- posledni = posledni->dalsi;
- setrid_seznam(&prvni);
- vypis_veta(&prvni);
- zabij(&prvni);
- }
- /*------------------------------------------------*/
Možný výstup z programu:
Zadavejte slova ne delsi nez 20 znaku. Zadavani vet ukoncete pomoci znaku konec souboru. (tj. CTRL+D v unixech nebo CTRL+Z + Enter v DOSu a Windows) Kdyz nejde hora k Moha-Medovi, musi Moha-Med k hore. 1: Kdyz 7: Moha-Med 5: Moha-Medovi, 3: hora 9: hore. 4: k 8: k 6: musi 2: nejde
Lepším příkladem na použití calloc()
by byl
asi příklad s řídkou maticí (tj matice, která obsahuje spoustu nul).
Tam by se ukázala výhoda inicializace paměti nulami funkcí calloc()
,
oproti neinicalizované paměti vrácené funkcímalloc()
(paměť
alokované funkcí malloc()
může obsahovat všelijaké „smetí“).
Změna velikosti získané paměti pomocí realloc()
Funkce realloc()
umožňuje změnit
velikost dříve alokované paměti s tím, že data v již alokované paměti
zůstanou nezměněna. Deklarace funkce je následující:
První argument funkce, ptr je adresa paměti
(anglicky pointer), která byla alokována pomocí funkcí
malloc()
,
calloc()
nebo
realloc()
. Velikost
tohoto paměťového bloku se zvětší (či zmenší) na size bajtů.
Návratovou hodnotou je ukazatel na „přealokovanou“ paměť. To ovšem
nemusí být stejné místo v paměti jako původní paměť (na kterou
ukazoval argument ptr
)!
Proto po volání funkce realloc()
se
již nepokoušejte přistupovat do paměti pomocí hodnoty v ukazateli
ptr, ani ji uvolňovat,
ale používejte jen paměť na kterou ukazuje hodnota vrácená funkcí
realloc()
.
Pokud by byl argument ptr
NULL,
pak by bylo volání realloc()
obdobné jako
malloc()
. Pokud byste velikost pole
size nastavili na nulu,
bylo by to stejné, jako byste volali pro
ptr funkci free()
.
Nově alokovaná paměť má nedefinované hodnoty (není vynulována
jako u calloc()
). Původní části paměti,
kterou jste přealokovali, bude obsahovat stejná data, jako před alokací.
(Jen mohou být na jiném místě v paměti, takže pozor na používání odkazů
inicializovaných před realokací).
Pokud se funkci realloc()
nepodaří získat
dostatek nové paměti, vrátí ukazatel NULL. Proto byste neměli v
programu používat následující konstrukci:
Takto by se totiž do ukazatele uložila hodnota NULL a vy byste přišli o ukazatel na původní alokovanou paměť, která by zůstala alokovaná, ale už by jste se k ní neměli jak dostat.
V příkladu bude uživatel zadávat programu požadovanou velkost v MiB alokované paměti a program jí bude alokovat.
/* c18/dynamic4.c */
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#define MIB 1024*1024
int main(void)
{
unsigned long x; size_t a;
char *pom, *v = NULL;
do {
printf("Zadejte pocet megabajtu, 0 pro konec: ");
scanf("%lu", &x);
pom = (char *) realloc((void *) v, x * MIB);
if ((pom == NULL) && (x))
printf("Nedostatek pameti!!\n");
else {
v = pom;
/* vyplneni pameti jednickami.
* To pocitac trosilinku zamestna :-) */
for (a = 0; a < x * MIB; a++) {
v[a] = '1';
}
}
} while (x);
return 0;
}
/*------------------------------------------------*/
Pokud spustíte program v Linuxu, můžete na konzoli pomocí příkazu
free
sledovat,
jak vám roste a klesá velikost využité paměti počítače
podle toho, kolik si jí zrovna žádáte.
V DOSu a Windows 3.11 bude
1 MiB souvislé paměti příliš velký požadavek1). Pokud překládáte
program tam, žádejte o paměť raději po kibibytech :-).
1) LOL, to už je ale starej tutoriál :D. Ale pořád aktuální ;-).