2. díl - Dynamická alokace paměti v jazyce C

C++ Dynamická práce s pamětí Dynamická alokace paměti v jazyce C

V minulém tutoriálu 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ž dnes. 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. 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ž 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.

Pozn.: 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 začátek adresy, kde leží naše nová paměť. Již víme, že každý pointer má určitý typ, resp. ukazuje na nějaký typ. Aby byla funkce malloc() univerzální, vrací pointer na typ void. 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 systému velký (např. int může zabírat 16 i 32 bitů). Volání sizeof() je nahrazeno konstantou při kompilaci, takže nijak negativně neovlivňuje 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čí 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:

Funkce malloc v jazyce C

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ěť neuvolníme 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, protože následná chyba nijak logicky nesouvisí s místem v programu, kde nám paměť přetekla. Může se to projevit opravdu jakkoli :)
  • 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ím dílu 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.


 

Stáhnout

Staženo 67x (31.81 kB)
Aplikace je včetně zdrojových kódů v jazyce c

 

  Aktivity (2)

Článek pro vás napsal David Čápka
Avatar
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.

Jak se ti líbí článek?
Celkem (6 hlasů) :
4.833334.833334.833334.833334.83333


 



 

 

Komentáře
Zobrazit starší komentáře (6)

Avatar
mnauik
Člen
Avatar
mnauik:

Ahoj, lze zjistit, zda jsem např. na konci programu uvolnil všechnu paměť?

Odpovědět 19.5.2015 13:01
minusuj mě, ale zdůvodni to ;)
Avatar
Martin Dráb
Redaktor
Avatar
Odpovídá na mnauik
Martin Dráb:

Ahoj, lze zjistit, zda jsem např. na konci programu uvolnil všechnu paměť?

Nemyslím, že na to existuje nějaký způsob "už od výrobce". Ale celkem snadno si tuto funkcionalitu můžeš přidat. Stačí si napsat vlastní vezri alokační a dealokační funkce (malloc a free), přičemž rozdíly budou následující:

  1. alokační funkce zajistí alokaci nového bloku (např. přes výchozí malloc a informaci o této alokaci někam zapíše),
  2. dealokační funkce uvolní zadaný blok (klidně pomocí výchozího free) a odstraní informaci o tom, že byl blok alokovaný (tuto informaci ukládala alokační funkce).

Alokované bloky můžeš například řetězit do nějaké datové struktury. Jelikož detekovat neuvolněnou paměť budeš potřebovat hlavně při ladění programu, vystačíš si se spojovým seznamem (je to pomalé, ale to nikoho při ladění obvykle netíží). Přičemž budeš možná u každého bloku potřebovat prostor, abys jej mohl do té datové struktury zařadit (ale záleží na implementaci). To nevadí, protože můžeš alokovat větší blok, než volající požaduje (standardní alokátor to taky tak dělá) a předstírat, že jsi alokoval přesně tolik, kolik bylo požadováno.

Pokud bys chtěl něco lepšího než jen tuto detekci úniků paměti, lze pomocí vlastních alokačních funkcí a případných maker dosáhnout ještě následujícího:

* při reportování nedealokovaného bloku se dozvíš, v jaké funkci, v jakém souboru zdrojáku a na jakém jeho řádku byl tento blok alokován,
* detekuješ pokus o uvolňění bloku, který alokován vůbec nebyl,
* zjistíš, zda nedochází k přetečení (či "podtečení") bloku, když do něj někdo ukládá data (alokuješ větší blok, než je potřeba, a na jeho začátek a konec strčíš nějakou signaturu, kterou čas od času kontroluješ).

Což mi připomíná, že bych na toto téma mohl i někdy něco napsat, když už tyto typy alokátorů používám v podstatě pořád :-).

Pokud vím, existují i speciální alokátory, které dělají +- to, co jsem tu popsal. A určitě i něco navíc.

Odpovědět  +1 19.5.2015 13:39
2 + 2 = 5 for extremely large values of 2
Avatar
coells
Redaktor
Avatar
Odpovídá na Martin Dráb
coells:

V debug režimu už to dělá samotný malloc/free, nemusíš vynalézat kolo.

 
Odpovědět 19.5.2015 13:56
Avatar
Martin Dráb
Redaktor
Avatar
Odpovídá na coells
Martin Dráb:

V debug režimu už to dělá samotný malloc/free, nemusíš vynalézat kolo.

To asi záleží na konkrétní implementaci (překladači). U *NIXu jsem to viděl a i MSVS lze k něčemu podobnému donutit za pár řádků kódu.

Odpovědět 19.5.2015 17:34
2 + 2 = 5 for extremely large values of 2
Avatar
Pavel Habžanský:

Nepřetypováváš přímo tu adresu, jenom tím říkáš, co se na tý adrese bude ukládat (v tomto případě int). Takhle bych to dovedl asi nejsnáze vysvětlit

Odpovědět 6.12.2015 23:12
Čím větší výzva, tím větší zkušenost
Avatar
David Novák
Tým ITnetwork
Avatar
Odpovědět  +1 7.12.2015 10:30
Chyba je mezi klávesnicí a židlí.
Avatar
Taskkill
Redaktor
Avatar
Odpovídá na mnauik
Taskkill:

gcc -d main.c / g++ -d main.cpp
valgrind ./a.out
jak pise David N. Valgrind je nejlepsi varianta..navic kdyz do kompilace pridas prepinac -d bude se ti to kompilovat s debugovacima symbolama ... takze se potom v tom valgrindu podivas cos pokazil...

pripadne jestli chces mi fakt cistej kod kompiluj takhle
gcc -d -Wall -pedantic main.c / g++ -d -Wall -pedantic main.cpp

to ti z kazdyho warningu udela error a rve to na tebe kdyz porusujes standardy .. ;) bav se

 
Odpovědět  +1 7.12.2015 11:17
Avatar
David Novák
Tým ITnetwork
Avatar
Odpovídá na Taskkill
David Novák:

Trochu poopravím..

gcc -Wall -Wextra -pedantic // vypíše všechny možné varování
gcc -Wall -Wextra -pedantic -Werror // bere je jako chyby - nutí tě psát validně
gcc -g // přidává debugovací informace

Můžeš prosím tě v krátkosti říct, co dělá -d ? Nechce se mi to studovat a kompilace s tímto přepínačem mi vypíše "no input files". Nakoukl jsem do manuálu a myslím, že to má produkovat výstupy jednotlivých fází překladu. -da vyprodukuje vše.. Docela zajímavý výstup.. Ale myslím, že jsi asi myslel -g :)

Odpovědět  +1 7.12.2015 11:50
Chyba je mezi klávesnicí a židlí.
Avatar
David Novák
Tým ITnetwork
Avatar
Odpovídá na David Novák
David Novák:

Jo a u toho -pedantic je ještě velmi vhodné uvést konkrétní standard, jinak tě ti to bude nutit výchozí, což bude asi iso 89..

gcc -std=c11 -pedantic // varuje, když použiješ něco, co není v C11 standardu
Odpovědět 7.12.2015 11:52
Chyba je mezi klávesnicí a židlí.
Avatar
Taskkill
Redaktor
Avatar
Odpovídá na David Novák
Taskkill:

urcite jsem myslel -g :D jen jsem nepremyslel diky za doplneni ...

 
Odpovědět  +1 7.12.2015 12:47
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 16. Zobrazit vše