/ janturon.cz / Výuka / C / Správa paměti

Správa paměti

Jedna z nejsilnějších stránek Cčka je dobrá správa paměti. Tím se také programátorovi dává mnohem více možností, jak něco pokazit a začínající Cčkaři dělají chyby na každém kroku. Bude následovat kupa teorie, která vám ale může ušetřit hodiny dumání nad chybou.

Zásobník

Volání funkce je odskok do jiné části paměti (ASM instrukce CALL), po jejímž dokončení program pokračuje za místem, kde naposledy odskočil (intrukce RET). Ve funkcích jsou volány další funkce a program si musí zapamatovat, kam se má vrátit, až se funkce dokončí. Program si proto při spuštění řekne o paměť na tyto odskoky. Její výchozí velikost je kolem 1MB. Program si může říct o více, ale tato velikost je pevně určena při spuštění a nelze ji pak měnit. Správa této paměti funguje jako zásobník (stack): každý další odskok je uložen jako list papíru na vrchol zásobníku, návrat z funkce se podívá, na jaké adrese má ve vykonávání pokračovat a onen "list" ze zásobníku ostraní: jakmile je zásobník prázdný, program zdárně skončil. To je jednoduchý mechanismus (stačí přičítat nebo odčítat hodnotu registru ESP), který dělá tuto paměť velmi rychlou. Do zásobníku se krom toho ještě ukládají parametry a proměnné funkce. Proto dojde k chybě stack overflow když se funkce zacyklí. Proto proměnné automaticky přestávají platit, jakmile se "list" odstraní. Díky tomu se tato paměť označuje jako automatická.

Halda

Zhruba řečeno je zásobník paměť na vykonávání programu. Pokud program potřebuje načíst rozsáhlá data, může požádat OS pomocí funkce malloc, aby mu přidělil nějakou paměť za běhu. O paměť se perou všechny spuštěné programy a OS musí vyhradit souvislý blok požadované velikosti, a to tak, aby nedocházelo k přílišné fragmentaci. To je mnohem pomalejší než zásobník. Této paměti na data se říká halda (heap), nebo také dynamická paměť. Narozdíl od zásobníku se neuvolní po skončení funkce, ale až tehdy, když o to program přímo požádá funkcí free.

Stack a Heap jsou datové struktury, pomocí nichž se paměť spravuje. Automatická a Dynamická paměť jsou mechanismy přístupu k paměti. Fyzicky se pracuje s pamětí RAM, kterou programu přiděluje OS.

Práce se zásobníkem a haldou v kódu

#include "stdlib.h" #include "stdio.h" void main() { char* data = (char*)malloc(20); data = "Hello"; ... free(data); }

Knihovnu stdlib.h načítáme preprocesorem kvůli definic funkcí malloc a free.

Ta hvězdička za char říká, že v proměnné data nebude uložen znak, ale odkaz do haldy, kde se ten znak nachází. Jakýkoliv odkaz je číslo, na 32b strojích zabírá 4B (4*8 bitů), můžeme tak adresovat až 4GB paměti (pokud je k dispozici). Velikost požadované paměti je to číslo ve funkci malloc. Každá paměť má svůj typ, který říká, jak se má její obsah interpretovat. malloc ale vrací prostou posloupnost bytů (typ void*). Operátorem (char*) vyjadřujeme, že si tyto byty přejeme interpretovat jako znaky. Znak zabírá vždy 1B, OS nám tedy na haldě vyhradí místo pro 20 znaků (nebo náš nehorázný požadavek odmítne a funkce pak vrátí NULL, s čímž zde pro jednoduchost nepočítáme). Do této připravené paměti na haldě vložíme posloupnost znaků 'H','e','l','l','o','\0' a do proměnné data na zásobníku vložíme 4B adresy znaku 'H' na haldě.

Únik paměti

Na konci funkce main paměť na haldě uvolníme, tedy dáváme OS pokyn, že od této chvíle dále ji může poskytnout jiným programům. Kdyby ale funkce main byla volána v jiné funkci a na příkaz free bychom zapomněli, stalo by se toto:

Této nepříjemnosti se říká únik paměti (memory leak). Program funguje správně, ale ve správci úloh můžete vidět, že neustále roste množství paměti, které potřebuje. Díky tomu, že velikost paměti je dnes gigantická, si toho hodiny nemusíme všimnout. Pak se počítač začne čím dál více "sekat" (je nucen více využívat virtuální paměť) a v posledních záškubech nám možná zobrazí i modrou obrazovku o kritickém nedostatku systémových prostředků. Nenadávejte pak na Windows: chyba je na vaší straně. Stačí ale program zavřít a všechna paměť programu se uvolní. OS si narozdíl od nepořádného programu vždy pamatuje, komu co z paměti půjčil.

Práce s dokumentací

Místo ... v části Práce se zásobníkem a haldou v kódu můžeme napsat třeba příkaz puts(data);.Podívejte se do dokumentace, teď už jí budete rozumět:


int puts ( const char * str );

Write string to stdout

Writes the C string pointed by str to stdout and appends a newline character ('\n').
The function begins copying from the address specified (str) until it reaches the terminating null character ('\0').
This final null-character is not copied to stdout.


C string je ta posloupnost znaků ukončená \0. Slovíčkem const slibujeme, že paměť až do uvolnění nebudeme měnit (takže si ji procesor pro opakované použití může nechat v registrech). Nezkoušejte lhát, kompilátor to odhalí. stdout je v našem případě soubor zařízení konzole. Funkce tam kopíruje znak po znaku ze zadané adresy, dokud nenarazí na \0.

V předchozí lekci jsme ale používali pro řetězce úplně jiný typ char name[32]. Důvod, proč to funguje, je fakt, že pole je konstantní ukazatel. V tomto případě se ale oněch 32B vytvořilo v zásobníku, protože jsme nepoužili volání malloc.

I na zásobník můžeme ukázat: char* data = name; znamená, že data ukazuje na první z oněch 32B alokovaných polem name.

Takhle nějak tedy pracují řetězce na úrovni hardware.

Operátory * a &

V souvislosti s haldou se také používá dereferenční operátor *, za nímž následuje adresa. Místo adresy na haldě pak vrátí její obsah. Místo ... tak můžeme napsat putchar(*data);, vytiskne se znak na místě té paměti, tedy 'H'. Chceme-li vypsat znak za ním, můžeme to zapsat jako putchar(*(data+1)); Pokud se ale budeme pokoušet přistupovat k paměti, kam nemáme přístup, OS náš program bez milosti ukončí.

I automatické proměnné na zásobníku se někam ukládají. Jejich adresu zjistí operátor &. Můžeme tedy psát:

char c = 'C'; printf("adresa c je: %d",&c);

%d v printf znamená, že zadaná proměnná se vloží do řetězce jako číslo. Proměnné, které nejsou na haldě, se nazývají hodnotové. Nemusí být jen na zásobníku: klíčovým slovem static žádáme program, aby proměnnou vytvořil mimo zásobník. Ta pak bude k dispozici až do konce programu.

void test(int p) { static int i; if(p==0) i = 0; else i+= p; printf("i=%d\n",i); // i není na zásobníku, neuvolní se } void main() { test(0); // i=0 test(1); // i=1 test(2); // i=3 }

Rozdíly mezi hodnotovou a odkazovou proměnnou

char a = 'A'; // do a ulož 'A' char* b = &a; // do b vlož adresu a putchar(a); // zobraz hodnotu a putchar(*b); // zobraz hodnotu b printf("%d",&a); // zobraz adresu a printf("%d",b); // zobraz adresu b

Řetězcové konstanty

Přestože pole je chápáno jako konstantní ukazatel, následující dva podobné zápisy mají z pohledu paměti jiný význam:

const char* a = "Hello"; char b[] = "Hello";

V případě a se vyhradí konstantní statická paměť, jejíž ukazatel je uložen do proměnné. V případě b se vyhradí nekonstantní automatická paměť, jako bychom vytvořili pole znaků. Rozdíl vynikne obzvláště ve funkcích:

const char* getA() { // char* result = "Hello"; - chyba: const -> mutable const char* result = "Hello"; // result[4] = '!'; - chyba: paměť je konstantní return result; // - v pořádku: vrací se statická paměť } const char* getB() { char result[] = "Hello"; char* pointer = result; // - v pořádku pointer[4] = '!'; // - v pořádku: nekonstantní return pointer; // - chyba: vrací se automatická paměť }

V případě getB() je paměť obsahující Hell! po skončení funkce uvolněna (a v zásobníku přepsána při volání další funkce). Kompilace proběhne s varováním, návratová hodnota je ale neplatná paměť a chování funkce je nepředvídané! Jsou dva způsoby, jak z funkce dostat v ní vytvořené pole:

vytvoření dynamické paměti

char* getC() { char result[] = "Hello"; char* pointer = new char[5]; strcpy(pointer,result); return pointer; }

použití ukazatele v parametru

void getD(char* pointer) { char result[] = "Hello"; strcpy(pointer,result); } char* pointer = new char[5]; getD(pointer);

V prvním případě nese zodpovědnost za alokování paměti funkce a za její uvolnění kód, ve kterém je volána. To je špatný návrh: jedna věc je řešena na dvou místech. Druhý případ je hojně využíván v C - viz např. gets(). Vysokoúrovňovými programátory je toto "naplňování" místo "vracení" považováno za neintuitivní, protože vysokoúrovňový programátor se pamětí nezabývá (nebo jen povrchně). V objektově orientovaném programování C++ byl tento přístup nahrazen konstruktory a destruktory tříd (bude vysvětleno později).

 

Otázky a úkoly