Ú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; ("Before: a=%d b=%d\n", a, b); printf // swap goes here int tmp = a; = b; a = tmp; b ("After: a=%d b=%d\n", a, b); printf}
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
Poznámka
Samozrejme fajnšmekri sa môžu zamyslieť nad tým, ako je možné vymeniť obsah dvoch premenných len s použitím týchto dvoch premenných. Bez gúglenia ;)
(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; = b; a = tmp; b } int main(){ int a=10, b=20; ("Before: a=%d b=%d\n", a, b); printf(a, b); swap("After: a=%d b=%d\n", a, b); printf}
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 funkciemain()
.V debuggeri cgdb môžeme využiť príkaz
print
na vypísanie obsahu premennej v tvareprint var
.Ak teda spustíme kód programu zavolaním príkazu
run
a budeme ho postupne krokovať príkazomstep
(alebonext
, ak chceme preskočiť krokovanie funkcieprintf()
) tak zistíme, že funkciaswap()
funguje presne tak, ako fungovať má. To znamená, že pred jej ukončením naozaj dôjde k zámene oboch premennýcha
ajb
.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 funkcieswap()
? Prečo výsledok nepretrval aj po návrate späť do funkciemain()
?
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&var print
teda vypíšeme na obrazovku adresy oboch premenných
a
ajb
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.Poznámka
To samozrejme nemusí byť pravda vždy. Veľkosť, ktorú hodnoty typu
integer
v pamäti zaberajú, záleží od vášho systému, resp. platformy. Túto veľkosť môžete vo vašom systéme zistiť pomocou operátorasizeof(int);
poprípade priamo v prostredí programu
cgdb
môžete napísať:sizeof(int) print
Tu si však všimnime problém, ktorý nastane po vstúpení do funkcie
swap()
- ak sa pozrieme na adresy parametrov funkciea
ab
, zistíme, že sú iné, ako tie priamo vo funkciimain()
.// 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
&
:(&a, &b); swap
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 typint
. Za týmto účelom použijeme operátor dereferencie*
. Signatúra funkcieswap()
bude vyzerať nasledovne:void swap(int *a, int *b);
Poznámka
Operátor dereferencie má niekoľko ďalších názvov. Napríklad sa môžete stretnúť s názvom go-to operator alebo value-of operator, pretože pomocou tohto operátora je možné prejsť na obsah príslušnej adresy. Alebo sa dá často stretnúť s názvom smerník, pretože smeruje na konkrétnu adresu v pamäti.
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; ("Before: a=%d b=%d\n", a, b); printf(&a,&b); swap("After: a=%d b=%d\n", a, b); printf}
Opäť preskúmame, čo sa v skutočnosti stalo pomocou nástroja na ladenie
cgdb
. Nastavíme breakpoint na funkciumain()
a pomocou príkazustep
začneme krokovať program až do funkcieswap()
.Tu si môžeme všimnúť, že:
vypísaním obsahu parametra
a
sa zobrazí adresa, na ktorej sa nachádza hodnota premenneja
z funkciemain()
print a
vypísaním adresy parametra
a
sa zobrazí adresa, na ktorej sa nachádza uložená adresa pôvodnej premenneja
z funkciemain()
&a print
k hodnote, na ktorú ukazuje adresa uložená v parametri
a
, sa dostaneme pomocou operátora dereferencie*a print
Poznámka
Jeden smerník zaberá v pamäti práve 8B a je jedno, o aký údajový typ sa jedná. To si vieme overiť príkazom:
print sizeof(int*)
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; ("Before: a=%d b=%d\n", a, b); printf(&a,&b); swap("After: a=%d b=%d\n", a, b); printf}
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:
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
Poznámka
V projektoch sme vás nútili zvyknúť dodržiavať pravidlo, že vstupná premenná funkcie sa nemôže počas jej vykonávania meniť. To sme vynucovali použitím kvalifikátora
const
.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.
Poznámka
Ak nás bude zaujímať konkrétna hodnota (adresa) a budeme ju chcieť vypísať na obrazovku, ako znak konverzie vo formátovacom reťazci funkcie
printf()
použijemep
.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){ ("Equal\n"); printf}else{ ("Differ\n"); printf} }
Upozornenie
Pokiaľ tento kód preložíme pomocou príkazu
make
, pričom budú nastavené všetky prepínače prekladača, ktoré sa nachádzajú v premennejCFLAGS
a používajú sa pre potreby predmetu, prekladač zahlási chybu:error: array comparison always evaluates to false
Pre ilustrovanie preberaného problému preto program stačí preložiť len samotným prekladačom
gcc
bez parametrov.Ak tento program spustíme, miesto očakávanej správy
Equal
dostaneme správuDiffer
. 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íkazprint 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íkazuprint &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:
*(str1 + 2) print
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){ ("Equal\n"); printf}else{ ("Differ\n"); printf} }
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
ajstr2
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”)).
- bude v pamäti uložený len raz, pretože je pre premennú
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]; ("Type some string: "); printf("%s", str1); scanf ("Type another string: "); printf("%s", str2); scanf if(strcmp(str1, str2) == 0){ ("Equal\n"); printf}else{ ("Differ\n"); printf} }
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 ;)