1. diel - Úvod do ukazovateľov (pointer) v jazyku C
Vitajte u prvej lekcie pokročilého kurzu o programovaní v jazyku C. V tomto kurze sa naučíme pracovať s dynamicky alokovanú pamäťou v jazyku C a dostaneme sa aj k práci so súbormi. Asi vás neprekvapí, že predpokladom ku zdolanie seriálu je znalosť základných konštrukcií jazyka C.
Adresy v pamäti
Keď sme sa prvýkrát zmieňovali o premenných, hovorili sme si, že
premenná je "miesto v pamäti", kam si môžeme uložiť nejakú hodnotu. Tiež
vieme, že premenné majú rôzne dátové typy (napr. int
) a tie
zaberajú v pamäti rôzne miesta (napr. Int zaberá 32 bitov, teda 32 núl a
jednotiek).
Pamäť počítača si môžeme predstaviť ako dlhú (takmer nekonečnú ) Rad núl a jednotiek. Niektoré časti pamäte sú obsadené inými aplikáciami a niektoré sú operačným systémom chápané ako voľné miesto. Aby sa dalo s pamäťou rozumne pracovať, je adresovaná, ako sú napr. Domy v ulici. Adresy sa väčšinou zapisujú v šestnástkovej sústave, ale stále sa jedná o obyčajná čísla. Adresy idú chronologicky za sebou a na každej adrese sa nachádza 1 bajt (teda 8 bitov, pretože adresovanie po drobných bitoch by bolo nepraktické).
Akonáhle v céčku deklarujeme nejakú premennú v zdrojovom kóde a aplikáciu spustíme, Céčko si povie operačnému systému o toľko pamäti, koľko je pre túto premennú treba. Od systému získa pridelenú adresu do pamäte, na ktorú môže hodnotu premennej uložiť (zjednodušene povedané). Tento proces nazývame alokácie pamäte.
Získanie adresy premenné
Jazyk C nás od adries zatiaľ plne odsťiňoval, pamäť alokovala za nás
as našimi premennými sme pracovali jednoducho pomocou ich mien. Vytvorme si
teraz jednoduchý program, ktorý založí premennú typu int
a do
nej uloží hodnotu 56
. Adresu tejto premennej si získame pomocou
tzv. Referenčného operátoru &
(ampersand) a vypíšeme ju do
konzoly. Vo formátovacom reťazci použijeme %p
, čo ju vypíše v
šestnástkovej sústave tak, ako sa na pamäťovú adresu sluší a patrí.
int main(int argc, char** argv) { int a; a = 56; printf("Proměnná a s hodnotou %d je v paměti uložená na adrese %p", a, &a); return (EXIT_SUCCESS); }
výsledok:
c_pointery
Proměnná a s hodnotou 56 je v paměti uložená na adrese 0x23aadc
Vidíte, že na mojom počítači si systém vybral adresu
0x23aadc
. Vy tam budete mať iné číslo. Situácia v pamäti
počítača bude vyzerať takto:
(Dátový typ int má 32 bitov, preto teda zaberá 4 osmice bitov na 4 adresách. Udávame vždy adresu začiatku hodnoty.)
Ukazovatele (pointer)
Získať číslo adresy je síce pekné, ale ak by sme s pamäťou takto pracovali, bolo by to trochu nepraktické. Z toho dôvodu jazyk C podporuje tzv. Ukazovatele (anglicky Pointer). Ukazovateľ je premenná, ktorej hodnotou je adresa niekam do pamäte. Céčko ukazovateľ však neberie ako obyčajné číslo, ale vie, že ho má používať ako adresu. Keď do ukazovateľa teda niečo uložíme alebo naopak vypíšeme jeho hodnotu, nevypisuje sa adresa (hodnota ukazovateľa), ale používa sa hodnota, na ktorú ukazovateľ ukazuje.
Vráťme sa opäť k nášmu programu. Tentoraz si okrem premenné a
definujeme aj ukazovateľ na premennú a
. Ten bude tiež typu int,
ale pred jeho názvom bude tzv. Dereferenční operátor * (hviezdička).
Zvyknite si Pointer pomenovávať vždy tak, aby začínali na p_
.
Vyhnete sa tak v budúcnosti veľkým problémom, pretože Pointer sú pomerne
nebezpečné, ako ďalej zistíme, a mali by sme si zrozumiteľne označiť, či
je premenná pointer alebo nie.
int main(int argc, char** argv) { int a, *p_a; a = 56; p_a = &a; // Uloží do p_a adresu proměnné a *p_a = 15; // Uloží hodnotu 15 na adresu v p_a printf("Ukazatel p_a má hodnotu %d ukazuje na hodnotu %d", p_a, *p_a); return (EXIT_SUCCESS); }
Aplikácia si vytvorí premennú typu int
a ďalej ukazovateľ
na int
. Ukazovatele tiež majú vždy svoj dátový typ podľa
toho, na hodnotu akého typu ukazujú. Do premennej a sa uloží hodnota 56.
Do ukazovateľa p_a
(zatiaľ bez hviezdičky) sa uloží adresa
premenné a, ktorú získame pomocou referenčného operátora
&
. Teraz chceme tam, kam ukazuje pointer p_a
,
uložiť číslo 15
. Použijeme dereferenční operátor
(*
) a tým neuložíme hodnotu do ukazovateľa, ale tam, kam
ukazovateľ ukazuje.
Následne vypíšeme hodnotu ukazovateľa (čo je nejaká adresa v pamäti,
obvykle vysoké číslo, tu ho vypisujeme v desiatkovej sústave) a ďalej
vypíšeme hodnotu, na ktorú ukazovateľ ukazuje. Kedykoľvek pracujeme s
hodnotou ukazovateľa (nie adresou), používame operátor *
.
výsledok:
c_pointery
Ukazatel p_a má hodnotu 23374500 ukazuje na hodnotu 15
Opäť si ukážme aj situáciu v pamäti:
Odovzdávanie referencií
Vieme teda na premennú vytvoriť ukazovateľ. K čomu je to ale dobré? Do premennej sme predsa vedeli ukladať aj predtým. Jednou z výhod pointer je tzv. Odovzdávanie referencií. Vytvorme si funkciu, ktoré prídu v parametri 2 čísla a my budeme chcieť, aby ich hodnoty prehodila (tejto funkcii sa anglicky hovorí swap). Naivne by sme mohli napísať nasledujúci kód:
// Tento kód nefunguje void prohod(int a, int b) { int pomocna = a; a = b; b = pomocna; } int main(int argc, char** argv) { int cislo1 = 15; int cislo2 = 8; prohod(cislo1, cislo2); printf("V cislo1 je číslo %d a v cislo2 je číslo %d.", cislo1, cislo2); return (EXIT_SUCCESS); }
výsledok:
c_pointery
V cislo1 je číslo 15 a v cislo2 je číslo 8.
Prečo že aplikácia nefunguje? Pri volaní funkcie prohod()
vo
funkcii main()
sa zoberú hodnoty premenných cislo1
a
cislo2
a tie sa skopírujú do premenných
a
a b
v definícii funkcie. Funkcia ďalej zmení
tieto premenné a a b, avšak pôvodné premenné
číslo1 a číslo2 zostanú nezmenené. Tomuto spôsobu, kedy
sa hodnota premennej do parametra funkcie skopíruje, hovoríme odovzdávanie
hodnodnou.
Všimnite si, že k prehodenie 2 čísel potrebujeme pomocnú
premennú. Keby sme vo funkcii prohod()
napísali len
a = b; b = a;
, Bola by v oboch premenných hodnota b
,
pretože hodnota a
sa prvým príkazom prepísala.
Ľubovoľnú premennú môžeme odovzdať referencií a to tak, že funkciu
upravíme aby prijímala v parametroch pointera. Pri volaní také funkcie potom
použijeme referenčné operátor &
:
void prohod(int *p_a, int *p_b) { int pomocna = *p_a; *p_a = *p_b; *p_b = pomocna; } int main(int argc, char** argv) { int cislo1 = 15; int cislo2 = 8; prohod(&cislo1, &cislo2); printf("V a je číslo %d a v b je číslo %d.", cislo1, cislo2); return (EXIT_SUCCESS); }
výsledok:
c_pointery
V cislo1 je číslo 8 a v cislo2 je číslo 15.
Keďže funkciu teraz odovzdávame adresu, je schopná zmeniť pôvodné premenné.
Niektorí programátori v jazyku C používajú často
parametre funkcií v odovzdávaní hodnoty. To však nie je príliš prehľadné
a ak nás netlačia výpočtovej čas a je to len trochu možné, mala by
funkcie vždy vracať len jednu hodnotu pomocou príkazu return
,
prípadne môže vracať štruktúru alebo ukazovateľ na štruktúru /
pole.
Možno vás napadlo, že konečne rozumiete funkciu scanf (), ktorá ukladá hodnoty do premenných odovzdaných parametre. Operátor & tu používame preto, aby sme funkciu odovzdali adresu, na ktorú má dáta uložiť:
int a; scanf("%d", &a);
Odovzdávanie poľa
Polia a Pointer majú v céčku veľa spoločného. Preto keď odovzdáme poľa do parametra nejakej funkcie a polia v nej zmeníme, zmeny sa v pôvodnom poli prejaví. Pole je na rozdiel od ostatných typov vždy odovzdávané referencií bez toho aby sme sa o to museli snažiť.
void napln_pole(int pole[], int delka) { int i; for (i = 0; i < delka; i++) { pole[i] = i + 1; } } int main(int argc, char** argv) { int cisla[10]; napln_pole(cisla, 10); printf("%d", cisla[5]); // Vypíše číslo 6 return (EXIT_SUCCESS); }
Ako sme si povedali skôr, pole je vlastne spojité miesto v pamäti. Ale také miesto musíme vedieť nejako adresovať. Adresujeme ho práve pomocou ukazovateľa. Premenná typu pole totiž nie je nič iné ako ukazovateľ. To znamená, že nám bez problémov prejde nasledujúce operácie priradenie:
int pole[10]; int* p_pole = cisla;
V kapitole Aritmetika ukazovateľov si ukážeme, že je úplne jedno, či máme poľa alebo ukazovateľ.
Null
Všetkým pointerům ľubovoľného typu môžeme priradiť konštantu
NULL
. Tá udáva, že je pointer prázdny a že zrovna na nič
neukazuje. Na väčšine platforiem sa NULL
rovná hodnote
0
a tak sa v niektorých kódoch môžete stretnúť s priradením
0
miesto NULL
. To sa všeobecne neodporúča kvôli
kompatibilite medzi rôznymi platformami. Túto hodnotu budeme v budúcnosti
hojne používať.
Čo si zapamätať: Pointer je premenná, v ktorej je uložená
adresa do pamäti. Môžeme pracovať buď s touto adresou alebo s hodnotou na
tejto adrese a to pomocou operátora *
. Adresu ľubovoľnej
premennej získame pomocou operátora &
.
Hoci sme si Pointer pomerne slušne uviedli, ich pravým účelom je najmä dynamické prideľovanie pamäte. Na ktoré sa pozrieme hneď v nasledujúcej lekcii, Dynamická alokácia pamäte v jazyku C .