Data in Memory

umiestnenie údajov v pamäti, operátory referencie a dereferencie

Záznam z prednášky

Introduction

  • (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.

Function 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?

Under the Hood

  • Aby sme lepšie videli, čo sa deje pod povrchom, použijeme debugger cgdb. Nastavíme breakpoint na funkciu main():

    break main

    a následne program spustíme príkazom run, ktorý sa preruší na začiatku 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 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 kópiou

  • (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.

  • Tu si môžeme všimnúť aj skutočnosť, že ak vypíšeme adresy oboch premenných na obrazovku, rozdiel medzi nimi budú práve 4 byty. Tieto premenné sú totiž v pamäti umiestnené hneď za sebou a keďže sa jedná o hodnoty typu integer, ich veľkosť v pamäti sú práve 4 byty.

           +----+
    0xd108 | 20 |
           +----+
    0xd10c | 10 |
           +----+
  • 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().

  • 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é kópiou. 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
    • adresou
  • 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é - kombinácia oboch predchádzajúcich možností, keď je parameter aj vstupom funkcie a funkcia po skončení v ňom odovzdá svoj výsledok

  • 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 * 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”)).

The Solution

  • 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");
        }
    }

Conclusion

  • (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)