3. seminář - Dynamická práce s pamětí

TEACHINGZPC2

Alokace paměti

Než si ukážeme jak alokovat paměť v jazyku C, tak si popíšeme jaké druhy paměti můžeme využít pro uložení hodnot proměnných.

Zásobník

Na zásobníku jsou uloženy všechny lokální a globální proměnné. Dále jsou zde uloženy informace o volaných funkcích. Podrobnější informace se dozvíte v předmětu Operační Systémy 1. Zásobník (angl. stack) je část paměti přiřazená OS spuštěnému programu a v průběhu se jeho velikost nemění. Uvažujme program níže, kde máme definovanou funkci multiply a funkci main , kde jsou definované 3 lokální proměnné x, y a z.


    int multiply(int left, int right) {
        return left * right;
    }
    
    int main() {
        int x = 10;
        int y = 20;
        int z = multiply(x, y);
        
        printf("%i\n", z);
        return 0;
    }

Na obrázku níže je ukázáno, jak se mění obsah zásobníku v průběhu vykonávání programu. Po spuštění programu se vytvoří místo na zásobníku pro globální proměnné a pro funkci main (tzv. stack frame). Funkce main obsahuje volání funkce multiply. To způsobí, že je nutné na zásobníku vytvořit stack frame pro funkci multiply. Zde jsou uloženy lokální proměnné funkce a argumenty. Naše funkce nemá žádné lokální proměnné a tak se na zásobník jen uloží hodnoty argumentů left a right a pak se provedou instrukce obsažené v těle funkce. Jakmile je dokončen výpočet a navrácena hodnota (pokud má), tak je stack frame vymazán. To je důvod proč nesmíme vracet ukazatel na lokální proměnnou funkce.

Call stack

Halda

Pokud chceme v těle funkce vytvořit hodnotu (například pole), tak kvůli výše uvedenému důvodu to nemůžeme provést následovně.


    int* create_array() {
        int array[200];
        return array; 
    }

Jakmile vrátíme hodnotu, tak dojde k vymazání stack frame funkce create_array a vrácený ukazatel bude ukazovat na místo v paměti o které nevíme co obsahuje. Dalším problémem je, že nemůžeme předat velikost pole jako argument protože pole můžeme definovat pouze pomocí číslených kostant a nebo konstant definovaných pomocí #define. Abychom mohli tento problém vyřešit musíme mít možnost uložit hodnoty proměnných jinam než na zásobník. Kromě zásobníku máme možnost uložit hodnoty na tzv. haldu (angl. heap). Prozatím předpokládejme, že máme funkci alloc. Funkce alokuje množství paměti potřebné pro uložení hodnoty typu int a jako výsledek vráci ukazatel na na toto místo v paměti.


    int* alloc_value(int number) {
        int *value = alloc();
        *value = number;
        return value; 
    }
    
    int main() {
        int a = 10;
        int *p = alloc_value(33);
        
        return 0;
    } 

Takto alokovaná není smazána jakmile skončí vykonávání funkce, protože halda a zásobník jsou na sobě nezavíslé. Na obrazku jsou znázorněny stavy zásobníku a haldy v průběhu vykonávání programu.

Heap

Po vykonání funkce alloc ve funkci alloc_value je alokována buňka na adrese 79 a následně je do buňky na této adrese vložena hodnota 33. Funkce alloc_value vrátí hodnotu 79, což je adresa alokované paměti a tato hodnota je přiřazena do proměnné p ve funkci main.

Funkce pro alokaci paměti

Všechny funkce pro práci s pamětí jsou obsaženy v knihovně <stdlib.h>. Pokud chceme alokovat paměť, tak stačí zavolat funkci malloc. Funkce má jediný argument a to hodnotu typu unsigned int, která udává počet bajtů které chceme alokovat. Funkce vrací ukazatel na void.

Deklarace


    void* malloc(size_t size);
            

Příklad


    // Alokujeme paměť pro uložení 10 hodnot typu int.
    int *array = (int*) malloc(10 * sizeof(int));
    
    // Alokace může skončit chybou (např. nedostatek paměti).
    // Funkce malloc vrací NULL v případě, že se alokace nezdařila.
    
    if (array) {
        // Muzeme pracovat s alokovanou pameti
    } else {
        printf("Při alokaci nastala chyba");
        return 1;
    }
    
    // Druhý způsob alokace + test
    int *array;
    
    if ((array = (int*) malloc(10 * sizeof(int)))) {
         // Muzeme pracovat s alokovanou pameti
    } else {
        printf("Při alokaci nastala chyba");
        return 1;
    }
            

V jazyce C máme dále funkci calloc, která slouží k alokaci n prvků, kde každý prvek má szejnou velikost. Dále je pak alokovaná paměť vynulována.

Deklarace


    void* calloc(size_t num, size_t size);
            

Příklad


    int *array;
    
    if ((array = (int*) calloc(10, sizeof(int)))) {
        // Muzeme pracovat s alokovanou pameti
    } else {
        printf("Při alokaci nastala chyba");
        return 1;
    }
    
    // Výše uvedený kód je ekvivaletní s následujícím
    
    int *array;
    
    if ((array = (int*) malloc(10 * sizeof(int)))) {
        for (int i = 0; i < 10; i += 1) {
            array[i] = 0;
        } 
      
        // Muzeme pracovat s alokovanou pameti  
    } else {
        printf("Při alokaci nastala chyba");
        return 1;
    }
            

Uvolnění paměti

Uvolnění paměti je opačný proces než alokace. Obecně platí, že pokud již paměť nepotřebujeme , tak ji musíme vrátit k dalšímu použítí. K uvolnění paměti slouží funkce free.

Deklarace


    void free(void *ptr)
            

Příklad


    int *array;
    
    if ((array = (int*) calloc(10, sizeof(int)))) {
        // Muzeme pracovat s alokovanou pameti
        
        // Zde již paměť nepotřebujeme a tak ji uvolníme.
        free(array);
    
        // Funkce free uvolní paměť, ale nezmění hodnotu ukazatele a
        // proto je dobré vymazat adresu která je v něm uložená
        array = NULL;
    } else {
        printf("Při alokaci nastala chyba");
        return 1;
    }
            

Pokud bychom používali ukazatel array po zavolání funkce free , tak by došlo k práci z pamětí která už nám nepatří a vedlo by to k neočekávanému chování.

Realokace paměti

Pokud potřebujeme v průběhu programu zvětšit/zmenšit již alokovanou paměť, tak k tomu můžeme použít funkci realloc. Tato funkce uvolní již alokovanou paměť a vrací ukazatel na nově alokovanou paměť požadované velikosti. Maximální obsah původní paměti je zachován a pokud je nová velikost větší, tak paměť mimo původní může obsahovat jakékoliv hodnoty. Pokud je velikost nové paměti menší než původní, tak docházi ke ztrátě hodnot.

Deklarace


    void* realloc(void *ptr, size_t size)
            

Příklad


    int *array = malloc(10 * sizeof(int));
    
    if (!array) {
        printf("Při alokaci nastala chyba");
        return 1;
    }
    array[0] = 42;
    array[9] = 15;

    int *temporary = realloc(p, 1000000 * sizeof(int));
    
    if (temporary) {
        // Realokace se povedla
        
        array = temporary;
        printf("%d %d\n", array[0], array[9]); 
        free(array);
    } else {
        printf("Realokace pameti selhala."); 
        free(array);
        return 1;
    }
            

Další funkce pro práci s pamětí

V knihovně string.h máme k dispozici další fuknce pro práci z pamětí.

memcpy


    void* memcpy (void *destination, const void *source, size_t num);

Funkce zkopíruje obsah pamětí na kterou ukazuje ukazatel source do pamětí na kterou ukazuje ukazatel destination. V paměti source musí být dostatečné množství bytů (alespoň n) pro čtení a v destination musí být dostatečný počet bytů pro zápis. Pokud se paměti překrývají, tak chování tétu funkce není definováno. Modifikátor const u argumentu source označí tento argument jako poute pro čtení a nelze tedy měnit hodnoty paměti uložené v source. Funkci můžeme použít například pro kopírování polí.


    #define SIZE 9

    int source[] = {1,2,3,4,5,6,7,8,9};
    int destination[SIZE];
    
    memcpy(destination, source, sizeof(int) * SIZE);

memmove


    void* memmove (void *destination, const void *source, size_t num);

Funkce funguje obdobně jako memcpy, s tím rozdílem, že funkce se chová korektně i v případě, že se zdrojová i cáílová paměť překrývají. Příkladem může být posunout prvky pole o jeden index dopředu.


    #define SIZE 9

    int source[] = {1,2,3,4,5,6,7,8,9};   
     
    memmove(source ,source + 1,sizeof(int) * SIZE - 1);

memset


    void* memset (void *ptr, int value, size_t num);

Funkce slouží k nastavení všech bytů v paměti předané ukazatelem ptr na hodnotu value. Parametr num udává počet bytů které má funkce nastavit.


    #define SIZE 9

    int source[] = {1,2,3,4,5,6,7,8,9};   
    memset(source, 0, sizeof(int) * SIZE);     

Otázka k zamyšlení: Je možné položky pole typu int nastavit na hodnotu 1 pomocí funkce memset?

Struktura odkazující na jinou strukturu

Struktura může obsahovat i ukazatel na jinou strukturu. Jako příklad si ukážeme implementaci lineárního seznamu, kde prvek seznamu je reprezetován strukturou Node. Hodnota data obsahuje číselnou hodnotu typu int uloženou v daném uzlu a nex odkazuje na další prvek seznamu. Pokud se jedná o polední prvek, tak položka next obsahuje hodnotu NULL.


    typedef struct node {
        int value;
        struct node *next ; 
    } Node;

Strukturu Node NEMŮŽEME definovat následovně:


    typedef struct {
        int value;
        Node *next ; 
    } Node;

Protože ve chvíli, kdy definujeme položku next, tak nemáme ještě definovaný typ Node pomocí typedef.

Funkce pro přidání prvku do seznamu


    #include <stdio.h>
    #include <stdlib.h>

    typedef struct node {
        int value;
        struct node *next ;
    } Node;
    
    // Funkce vloží prvek na začátek seznamu
    void push(Node **list, int value) {
        Node *new_node;
        new_node = (Node*) malloc(sizeof(Node));
        new_node->value = value;
        new_node->next = *list;
        *list = new_node;
    }
    
    int main() {
        Node* list = (Node*) malloc(sizeof(Node));
        list->value = 1;
        list->next = NULL;
        
        // Funkci add předáváme ukazatel na ukazatel na list.
        push(&list, 2);
        push(&list, 3);
        
        return 0;
    }

Úkoly

Naprogramujte funkci pro spojení dvou textových řetězců. Argumentem funkce jsou dva textové řetězce ke spojení, funkce vrátí nový řetězec (jako ukazatel).
Funkce pro práci se seznamem. Napište funkci, která:
  • vypíše všechny prvky seznamu.
  • zjistí délku seznamu.
  • přidá prvek na konec seznamu.
  • smaže prvek na začátku seznamu.
  • smaže prvek na konci seznamu.
  • pro zadané i vypíše i-tý prvek seznamu.
  • pro zadané i vypíše i-tý prvek seznamu od konce.
  • pro zadané i smaže i-tý prvek seznamu
  • vytvoří kopii seznamu. Funkce musí fungovat tak, že pokud změníme kopii seznamu, originální seznam se nezmění.