Lekce 1 - Úvod do ukazatelů v jazyce C
Vítejte u první lekce pokročilého kurzu o programování v jazyce C. V tomto kurzu se naučíme pracovat s dynamicky alokovanou pamětí v jazyce C a dostaneme se také k práci se soubory. Asi vás nepřekvapí, že předpokladem ke zdolání seriálu je znalost základních konstrukcí jazyka C.
Adresy v paměti
Když jsme se poprvé zmiňovali o proměnných, říkali jsme si, že
proměnná je "místo v paměti", kam si můžeme uložit nějakou hodnotu.
Také víme, že proměnné mají různé datové typy (např. int
)
a ty zabírají v paměti různě místa (např. int zabírá 32 bitů, tedy 32
nul a jedniček).
Paměť počítače si můžeme představit jako dlouhou (téměř
nekonečnou ) řadu nul a
jedniček. Některé části paměti jsou obsazené jinými aplikacemi a
některé jsou operačním systémem chápány jako volné místo. Aby se dalo s
pamětí rozumně pracovat, je adresována, jako jsou např. domy v ulici.
Adresy se většinou zapisují v šestnáctkové soustavě, ale stále se jedná
o obyčejná čísla. Adresy jdou chronologicky za sebou a na každé adrese se
nachází 1 bajt (tedy 8 bitů, protože adresování po drobných bitech by
bylo nepraktické).
Jakmile v céčku deklarujeme nějakou proměnnou ve zdrojovém kódu a aplikaci spustíme, Céčko si řekne operačnímu systému o tolik paměti, kolik je pro tuto proměnnou třeba. Od systému získá přidělenou adresu do paměti, na kterou může hodnotu proměnné uložit (zjednodušeně řečeno). Tento proces nazýváme alokace paměti.
Získání adresy proměnné
Jazyk C nás od adres zatím plně odsťiňoval, paměť alokoval za nás a s
našimi proměnnými jsme pracovali jednoduše pomocí jejich jmen. Vytvořme si
nyní jednoduchý program, který založí proměnnou typu int
a do
ní uloží hodnotu 56
. Adresu této proměnné si získáme
pomocí tzv. referenčního operátoru &
(ampersand) a
vypíšeme ji do konzole. Ve formátovacím řetězci použijeme
%p
, což ji vypíše v šestnáctkové soustavě tak, jak se na
paměťovou adresu sluší a patří.
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ýsledek:
c_pointery
Proměnná a s hodnotou 56 je v paměti uložená na adrese 0x23aadc
Vidíte, že na mém počítači si systém vybral adresu
0x23aadc
. Vy tam budete mít jiné číslo. Situace v paměti
počítače bude vypadat takto:

(Datový typ int má 32 bitů, proto tedy zabírá 4 osmice bitů na 4 adresách. Udáváme vždy adresu začátku hodnoty.)
Ukazatelé (pointery)
Získat číslo adresy je sice hezké, ale pokud bychom s pamětí takto pracovali, bylo by to poněkud nepraktické. Z toho důvodu jazyk C podporuje tzv. ukazatele (anglicky pointery). Ukazatel je proměnná, jejíž hodnotou je adresa někam do paměti. Céčko ukazatel však nebere jako pouhé číslo, ale ví, že ho má používat jako adresu. Když do ukazatele tedy něco uložíme nebo naopak vypíšeme jeho hodnotu, nevypisuje se adresa (hodnota ukazatele), ale použije se hodnota, na kterou ukazatel ukazuje.
Vraťme se opět k našemu programu. Tentokrát si kromě proměnné a
definujeme i ukazatel na proměnnou a
. Ten bude také typu int,
avšak před jeho názvem bude tzv. dereferenční operátor * (hvězdička).
Zvykněte si pointery pojmenovávat vždy tak, aby začínaly na
p_
. Vyhnete se tak v budoucnu velkým problémům, protože
pointery jsou poměrně nebezpečné, jak dále zjistíme, a měli bychom si
srozumitelně označit, zda je proměnná pointerem či nikoli.
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); }
Aplikace si vytvoří proměnnou typu int
a dále ukazatel na
int
. Ukazatelé také mají vždy svůj datový typ podle toho, na
hodnotu jakého typu ukazují. Do proměnné a se uloží hodnota 56.
Do ukazatele p_a
(zatím bez hvězdičky) se uloží adresa
proměnné a, kterou získáme pomocí referenčního operátoru
&
. Nyní chceme tam, kam ukazuje pointer p_a
,
uložit číslo 15
. Použijeme dereferenční operátor
(*
) a tím neuložíme hodnotu do ukazatele, ale tam, kam ukazatel
ukazuje.
Následně vypíšeme hodnotu ukazatele (což je nějaká adresa v paměti,
obvykle vysoké číslo, zde ho vypisujeme v desítkové soustavě) a dále
vypíšeme hodnotu, na kterou ukazatel ukazuje. Kdykoli pracujeme s hodnotou
ukazatele (ne adresou), používáme operátor *
.
Výsledek:
c_pointery
Ukazatel p_a má hodnotu 23374500 ukazuje na hodnotu 15
Opět si ukažme i situaci v paměti:

Předávání referencí
Umíme tedy na proměnnou vytvořit ukazatel. K čemu je to ale dobré? Do proměnné jsme přeci uměli ukládat i předtím. Jednou z výhod pointerů je tzv. předávání referencí. Vytvořme si funkci, které přijdou v parametru 2 čísla a my budeme chtít, aby jejich hodnoty prohodila (této funkci se anglicky říká swap). Naivně bychom mohli napsat následující 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ýsledek:
c_pointery
V cislo1 je číslo 15 a v cislo2 je číslo 8.
Proč že aplikace nefunguje? Při volání funkce prohod()
ve
funkci main()
se vezmou hodnoty proměnných cislo1
a
cislo2
a ty se zkopírují do proměnných
a
a b
v definici funkce. Funkce dále změní tyto
proměnné a a b, avšak původní proměnné cislo1
a cislo2 zůstanou beze změny. Tomuto způsobu, kdy se hodnota
proměnné do parametru funkce zkopíruje, říkáme předávání
hodnodnou.
Všimněte si, že k prohození 2 čísel potřebujeme pomocnou
proměnnou. Kdybychom ve funkci prohod()
napsali jen
a = b; b = a;
, byla by v obou proměnných hodnota b
,
protože hodnota a
se prvním příkazem přepsala.
Libovolnou proměnnou můžeme předat referencí a to tak, že funkci
upravíme aby přijímala v parametrech pointery. Při volání takové funkce
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ýsledek:
c_pointery
V cislo1 je číslo 8 a v cislo2 je číslo 15.
Jelikož funkci nyní předáváme adresu, je schopna změnit původní proměnné.
Někteří programátoři v jazyce C používají často
parametry funkcí k vracení hodnoty. To ovšem není příliš přehledné a
pokud nás netlačí výpočetní čas a je to jen trochu možné, měla by
funkce vždy vracet jen jednu hodnotu pomocí příkazu return
,
případně může vracet strukturu nebo ukazatel na strukturu/pole.
Možná vás napadlo, že konečně rozumíte funkci scanf(), která ukládá hodnoty do proměnných předaných parametry. Operátor & zde používáme proto, abychom funkci předali adresu, na kterou má data uložit:
int a; scanf("%d", &a);
Předávání pole
Pole a pointery mají v céčku mnoho společného. Proto když předáme pole do parametru nějaké funkce a pole v ní změníme, změny se v původním poli projeví. Pole je na rozdíl od ostatních typů vždy předáváno referencí aniž bychom se o to museli snažit.
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); }
Jak jsme si řekli dříve, pole je vlastně spojité místo v paměti. Ale takové místo musíme umět nějak adresovat. Adresujeme ho právě pomocí ukazatele. Proměnná typu pole totiž není nic jiného než ukazatel. To znamená, že nám bez problémů projde následující operace přiřazení:
int pole[10]; int* p_pole = pole;
V kapitole Aritmetika ukazatelů si ukážeme, že je úplně jedno, zda máme pole nebo ukazatel.
NULL
Všem pointerům libovolného typu můžeme přiřadit konstantu
NULL
. Ta udává, že je pointer prázdný a že zrovna na nic
neukazuje. Na většině platforem se NULL
rovná hodnotě
0
a tak se v některých kódech můžete setkat s přiřazením
0
místo NULL
. To se obecně nedoporučuje kvůli
kompatibilitě mezi různými platformami. Tuto hodnotu budeme v budoucnu hojně
používat.
Co si zapamatovat: Pointer je proměnná, ve které je uložena
adresa do paměti. Můžeme pracovat buď s touto adresou nebo s hodnotou na
této adrese a to pomocí operátoru *
. Adresu libovolné
proměnné získáme pomocí operátoru &
.
Ačkoli jsme si pointery poměrně slušně uvedli, jejich pravým účelem je zejména dynamické alokování paměti.
Příště, v lekci Dynamická alokace paměti v jazyce C, se podíváme na dynamické alokovaní paměti.