Údaje v pamäti

umiestnenie údajov v pamäti, operátory referencie a dereferencie, odovzdávanie parametrov hodnotou (kópiou) vs adresou (referenciou)

Záznam z prednášky

Úvod

  • (slide) Dnešná téma, ktorej sa budeme na prednáške venovať, je pomerne kľúčová, čo sa týka programovania v jazyku C, preto s ňou začíname ako prvou. Rovnako tak bude dosť náročná, pretože sa budeme rozprávať o pamäti, jej prideľovaní a uvoľňovaní a o tom, kde a ako sa budú údaje ukladať.

  • Táto téma je špecifická pre jazyk C, pretože pokiaľ budete pracovať v niektorom vyššom programovacom jazyku, ako napr. Java, C# alebo Python, tak sa s dynamickou a vlastnou správou pamäte nestretnete.

Funkcia swap()

  • (slide) V predmete Základy algoritmizácie a programovania ste sa rozprávali o niekoľkých triediacich algoritmoch. Dôležitou súčasťou každého tohto algoritmu bola časť kódu, ktorej úlohou bolo porovnať navzájom dva prvky a rozlíšiť, ako sú usporiadané - či sú v správnom poradí alebo ich treba prehodiť. A práve na tento fragment kódu a konkrétne na časť, ktorá sa venuje výmene hodnôt týchto dvoch prvkov, sa skúsime pozrieť podrobnejšie.

  • Začnime teda jednoducho - fragmentom kódu, ktorého úlohou bude vymeniť obsah dvoch premenných medzi sebou:

    #include <stdio.h>
    
    int main(){
      int a=10, b=20;
    
      printf("Before: a=%d b=%d\n", a, b);
    
      // swap goes here
      int tmp = a;
      a = b;
      b = tmp;
    
      printf("After: a=%d b=%d\n", a, b);
    }
  • Pomocou tohto fragmentu teda dokážeme zameniť obsah dvoch premenných pomocou tretej premennej. Jeho výstup bude vyzerať nasledovne:

    Before: a=10 b=20
    After: a=20 b=10
  • (slide) Ak ale budeme chcieť tento fragment použiť na viacerých miestach, ako napr. vo viacerých triediacich algoritmoch, bolo by výhodnejšie ho osamostatniť a vytvoriť z neho funkciu. Skopírujeme teda uvedený fragment kódu do funkcie, ktorú nazveme swap():

    #include <stdio.h>
    
    void swap(int a, int b){
        int tmp = a;
        a = b;
        b = tmp;
    }
    
    int main(){
        int a=10, b=20;
    
        printf("Before: a=%d b=%d\n", a, b);
        swap(a, b);
        printf("After: a=%d b=%d\n", a, b);
    }
  • Keď však tento program spustíme, jeho výsledok nebude taký, ako by sme očakávali - čísla nebudú vymenené, ale zostanú nezmenené:

    Before: a=10 b=20
    After: a=10 b=20
  • Kde sa teda stala chyba? Prečo nám fragment kódu fungoval dovtedy, kým sme ho neosamostatnili a nespravili z neho funkciu?

Čo sa deje (pod povrchom)

  • Aby sme lepšie videli, čo sa deje pod povrchom, budeme náš program krokovať pomocou nástroja cgdb. Najprv ho teda spustíme:

    $ cgdb ./swap
  • Po spustení nastavíme bod prerušenia (breakpoint) na funkciu main():

    break main

    a následne program spustíme príkazom run, ktorého vykonávanie sa preruší okamžite vtedy, keď dôjde na začiatok funkcie main().

  • V debuggeri cgdb môžeme využiť príkaz print na vypísanie obsahu premennej v tvare print var.

  • Ak teda spustíme kód programu zavolaním príkazu run a budeme ho postupne krokovať príkazom step (alebo next, ak chceme preskočiť krokovanie funkcie printf()) tak zistíme, že funkcia swap() funguje presne tak, ako fungovať má. To znamená, že pred jej ukončením naozaj dôjde k zámene oboch premenných a aj b.

  • Problém však nastane v momente, keď sa vykonávanie vráti späť do funkcie main(). Tu sa v oboch premenných budú nachádzať ich pôvodné hodnoty bez akejkoľvek zámeny. Prečo teda došlo k zámene hodnôt len vo vnútri funkcie swap()? Prečo výsledok nepretrval aj po návrate späť do funkcie main()?

Odovzdávanie parametrov funkcii hodnotou

  • (slide) Pozrime sa na to, na akých adresách sú naše premenné v pamäti uložené. Na to, aby sme sa túto informáciu dozvedeli, použijeme operátor referencie &. Príkazom

    print &var

    teda vypíšeme na obrazovku adresy oboch premenných a aj b utriedené podľa “vzdialenosti” od začiatku adresného priestoru:

    0xda6c 10 a
    0xda68 20 b
    0x0000
  • Tu si môžeme všimnúť, že rozdiel medzi oboma premennými sú 4 byty. Tieto premenné sú totiž v pamäti umiestnené hneď za sebou, a keďže sa jedná o hodnoty typu integer, každá jedna premenná zaberá práve tieto 4 byty.

  • Tu si však všimnime problém, ktorý nastane po vstúpení do funkcie swap() - ak sa pozrieme na adresy parametrov funkcie a a b, zistíme, že sú iné, ako tie priamo vo funkcii main().

    // main()
    0x7fffffffda6c 10 a
    0x7fffffffda68 20 b
    
    // swap()
    0x7fffffffda38 10 a
    0x7fffffffda30 20 b
    
    0x0000
  • Ako je možné, že adresy parametrov funkcie swap() sú iné ako tie, ktoré sme jej odovzdali pri jej volaní?

  • Táto skutočnosť je spôsobená tým, že parametre boli funkcii odovzdané hodnotou (z angl. passing by value). To znamená, že sa vytvorila kópia ich obsahu v pamäti a funkcia swap() dostala pri volaní práve adresy týchto skopírovaných parametrov. Akékoľvek operácie s týmito premennými sa teda robili s kópiami a nie s originálmi týchto premenných.

Odovzdávanie parametrov funkcii adresou

  • Aby sme tento problém vyriešili, nemôžeme pracovať s kópiou hodnôt premenných, ale potrebovali by sme do funkcie posunúť adresy pôvodných premenných z funkcie main().

  • Jazyk C totiž umožňuje odovzdávať parametre funkciám oboma možnými spôsobmi:

    • hodnotou, z angl. by value
    • adresou, z angl. by reference
  • Ak teda chceme zabezpečiť odovzdanie adresy premennej, pri jej volaní použijeme operátor &:

    swap(&a, &b);
  • Ak sa však pokúsime preložiť program, skončíme s nasledovným hlásením prekladača:

    error: incompatible pointer to integer conversion passing
    'int *' to parameter of type 'int'; remove &
  • (slide) Samozrejme, aby všetko fungovalo správne, potrebujeme upraviť aj samotnú funkciu swap(). Jej parametre tentokrát dostanú adresu, ktorú sme funkcii poslali pomocou operátora referencie &. Aby sme túto adresu mohli vo funkcii prijať, jej parametre budú typu ukazovateľ na typ int. Za týmto účelom použijeme operátor dereferencie *. Signatúra funkcie swap() bude vyzerať nasledovne:

    void swap(int *a, int *b);
  • Telo funkcie swap() zakomentujeme a necháme ho prázdne. Program bude teda vyzerať takto:

    #include <stdio.h>
    
    void swap(int *a, int *b){
    /*
        int tmp = b;
        a = b;
        b = tmp;
    */
    }
    
    int main(){
        int a=10, b=20;
    
        printf("Before: a=%d b=%d\n", a, b);
        swap(&a,&b);
        printf("After: a=%d b=%d\n", a, b);
    }
  • Opäť preskúmame, čo sa v skutočnosti stalo pomocou nástroja na ladenie cgdb. Nastavíme breakpoint na funkciu main() a pomocou príkazu step začneme krokovať program až do funkcie swap().

  • Tu si môžeme všimnúť, že:

    • vypísaním obsahu parametra a sa zobrazí adresa, na ktorej sa nachádza hodnota premennej a z funkcie main()

      print a
    • vypísaním adresy parametra a sa zobrazí adresa, na ktorej sa nachádza uložená adresa pôvodnej premennej a z funkcie main()

      print &a
    • k hodnote, na ktorú ukazuje adresa uložená v parametri a, sa dostaneme pomocou operátora dereferencie

      print *a
  • Zostáva nám teda už len vykonať potrebné úravy vo funkcii swap():

    #include <stdio.h>
    
    void swap(int *a, int *b){
        int tmp = *a;
        *a = *b;
        *b = tmp;
    }
    
    int main(){
        int a=10, b=20;
    
        printf("Before: a=%d b=%d\n", a, b);
        swap(&a,&b);
        printf("After: a=%d b=%d\n", a, b);
    }
  • Po preklade a spustení konečne dôjde k výmene obsahu oboch premenných.

Pass by Reference vs Pass by Value

  • (slide) V krátkosti zosumarizujme rozdiel medzi odovzdávaním parametra hodnotou (kópiou) a odovzdávaním parametra odkazom (referenciou). Celú situáciu prezentuje nasledujúci obrázok:

    Pass by Reference vs Pass by Value (zdroj)
    • ak odovzdávame parameter referenciou, je odovzdávaný objekt v pamäti len raz a akákoľvek zmena jeho obsahu sa prejaví na všetkých miestach, ktoré s ním pracujú

    • ak odovzdávame parameter kópiou, vznikne v pamäti kópia pôvodného objektu a po akejkoľvek zmene kópie zostane originál nezmenený

Input, Output and Input-Output Types of Function Parameters

  • Jazyk C je špecifický aj v tom, že parametre funkcií môžu byť troch typov:

    • Vstupné - keď parameter použijem len ako vstup do funkcie bez jeho následného modifikovania, napr. konverzia zo stupňov celzia na kelviny

    • Výstupné - keď sa v parametri, ktorý do funkcie pošlem, bude nachádzať výsledok operácie funkcie, napr.

    • Vstupno-Výstupné - Predstavuje kombináciu oboch predchádzajúcich možností. V tomto prípade pri volaní funkcie parameter funkcii poskytuje vstupnú hodnotu a po skončení v ňom funkcia odovzdá aj svoj výsledok. Príkladom tohto typu je práve vytvorená funkcia swap().

  • Hovorili sme o tom, že ak chceme vyriešiť problém s výmenou hodnôt dvoch premenných pomocou funkcie, musíme na to použiť vstupno-výstupné parametre a parameter samotný potrebujeme tejto funkcii odovzdať adresou a nie hodnotou.

  • Nie každý programovací jazyk umožňuje použiť všetky uvedené typy parametrov funkcií. Rovnako tak nie v každom jazyku dokážete implementovať funkciu swap().

String Compare

  • Pozrieme sa na ďalší problém - porovnanie dvoch reťazcov. Vychádzajme z predpokladu, že ak pomocou operátora == vieme porovnať dve ľubovoľné hodnoty dvoch ľubovoľných základných typov, vieme rovnakým operátorom porovnávať navzájom aj reťazce:

    #include <stdio.h>
    
    int main(){
        char str1[] = "hello";
        char str2[] = "hello";
    
        if(str1 == str2){
            printf("Equal\n");
        }else{
            printf("Differ\n");
        }
    }
  • Ak tento program spustíme, miesto očakávanej správy Equal dostaneme správu Differ. Prečo?

  • Vieme už, že premenná, ktorá je typu “reťazec”, je vlastne adresou reťazca umiestneného v pamäti. To znamená, že operátor == teda nebude porovnávať obsahy reťazcov, ale dve adresy navzájom. Ak sú tieto adresy zhodné, tak aj “reťazce sú zhodné”.

  • Túto skutočnosť si opäť vieme overiť pomocou nástroja cgdb. Ak však napíšeme priamo príkaz print str1, zistí z lokálneho kontextu, že sa jedná o jednorozmerné pole a vypíše priamo jeho obsah. Adresu reťazca preto získame zadaním príkazu print &str1.

  • Ak nás naopak bude zaujímať obsah, ktorý sa na danej adrese nachádza, použijeme operátor dereferencie * v tvare: print *str1. Uvidíme však, že na danej adrese sa nachádza len jedno písmeno, čo znamená, že adresa poľa je vlastne adresou prvého prvku daného poľa.

Smerníková aritmetika

  • Týmto spôsobom sa vieme dostať aj priamo na jednotlivé prvky poľa:

    print *(str1 + 2)

String Literal

  • Zhodu vieme dosiahnuť tak, že miesto poľa použijeme konštantný reťazec, kedy sa reťazec “hello” bude v pamäti nachádzať len raz, ale budú na neho ukazovať dva smerníky:

    #include <stdio.h>
    
    int main(){
        char *str1 = "hello";
        char *str2 = "hello";
    
        if(str1 == str2){
            printf("Equal\n");
        }else{
            printf("Differ\n");
        }
    }
  • Tu však pozor! V tomto prípade budeme reťazec “hello” nazývať reťazcovým literálom, o ktorom platí, že

    • bude v pamäti uložený len raz, pretože je pre premennú str1 aj str2 rovnaký, a
    • bude uložený v pamäti, ktorá je určená len na čítanie (akýkoľvek pokus o zápis končí s nedefinovaným správaním (“Segmentation fault”)).

Finálne riešenie

  • Porovnať dva reťazce teda neznamená porovnať dve adresy, ale obsah, ktorý sa nachádza na uvedených adresách. S výhodou vieme v tomto prípade použiť funkciu strcmp(), ktorá tento problém vyrieši za nás:

    #include <stdio.h>
    #include <string.h>
    
    int main(){
        char str1[30];
        char str2[30];
    
        printf("Type some string: ");
        scanf("%s", str1);
    
        printf("Type another string: ");
        scanf("%s", str2);
    
        if(strcmp(str1, str2) == 0){
            printf("Equal\n");
        }else{
            printf("Differ\n");
        }
    }

Zhrnutie

  • (slide) Na záver tu už mám len nadčasové video na tému smerníky, ktoré vám určite pomôže v utrasení vašich vedomostí na túto tému. Proste to nemôže dopadnúť ináč, ako dobre. Enjoy ;)

    Pointer Fun (zdroj)