Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 2 - Dynamická správa paměti v C++

V minulé lekci, Úvod do ukazatelů v 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++ 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í.

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++ 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, kompilátor ví, že na ni má vyhradit 32 bitů. Když si vytvoříme pole o 100 znacích, C++ 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++ 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++ 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++ 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.

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++ 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 klíčových slov - new a delete.

new

Klíčové slovo new řekne operačnímu systému o množství paměti, které potřebuje typ, se kterým new voláme. 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.

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í volání new hodnotu NULL (tedy pointer nikam).

delete

Po každém zavolání new musí někdy (třeba až na konci programu) následovat zavolání delete, 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 int a double

int* cislo = new int;
double* desetinne_cislo = new double;
//program
delete cislo;
cislo = NULL;
delete desetinne_cislo;
desetinne_cislo = NULL;

V programu s ukazateli pracujeme stejně, jak jsme si ukázali v minulém díle. Na konci programu musíte paměť uvolnit pomocí delete. Všimněte si následného přiřazení NULL zpět do ukazatele. Pokud 2x zavoláte delete na stejný ukazatel, program pravděpodobně spadne. Bude se snažit smazat paměť, která mu již nepřísluší a operační systém jej shodí. Když přiřadíme NULL, delete sice nic nesmaže, ale alespoň nám program nespadne, je tedy bezpečnější za každým voláním delete přiřadit do ukazatele NULL.

Č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.

Alokace polí

Pole alokujeme stejně, jako by se jednalo o typ. Například pole 10-ti celých čísel alokujeme takhle:

int* pole = new int[10];
pole[5] = 125;

Nejprve si všimněme, že k ukazateli můžeme skutečně přistupovat jako k poli. Více informací se dozvíte v dalším díle. Pořád platí pravidlo, že C++ nekontroluje překročení pole. C++ nám dovolí přistoupit například na patnáctý prvek (pole[15]=15), ale opět se snažíme dostat na místo, které nám nepatří. V lepším případě program načte náhodné data, v tom horším program spadne.

Co se ale liší je uvolnění paměti. Tentokrát musíme kompileru říci, že uvolňujeme pole. Uděláme to pomocí hranatých závorek za delete:

delete [] pole;

Díky závorkám C++ ví, že má mazat pole. Je také důležité, aby typ ukazatele byl stejný jako typ uložených dat (protože ukazatele můžeme přetypovat). Pokud změníte typ ukazatele a potom se jej pokusíte odstranit, bude výsledek nejistý a program může spadnout nebo uvolnit špatnou velikost paměti. V každém případě se program dostane do situace, která by v žádném případě neměla nastat.

V příští lekci, Aritmetika ukazatelů v 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.


 

Předchozí článek
Úvod do ukazatelů v C++
Všechny články v sekci
Pokročilé konstrukce C++
Přeskočit článek
(nedoporučujeme)
Aritmetika ukazatelů v C++
Článek pro vás napsal Patrik Valkovič
Avatar
Uživatelské hodnocení:
68 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity