Ukazatele
Abychom v C mohli manuálně pracovat s pamětí, potřebujeme mít možnost odkazovat se na jednotlivé
hodnoty v paměti pomocí adres. Adresa je číslo, takže
bychom mohli pro popis adres používat například datový typ unsigned int
1. To by ale nebyl dobrý
nápad, protože tento datový typ neumožňuje provádět operace, které bychom s adresami chtěli dělat
(načíst hodnotu z adresy či zapsat hodnotu na adresu), a naopak umožňuje provádět operace, které s
adresami dělat nechceme (například násobení či dělení adres obvykle nedává valný smysl).
1Nejnižší možná adresa je 0
, takže záporné hodnoty nemá cenu reprezentovat.
Z tohoto důvodu C obsahuje datový typ, který je interpretován jako adresa v paměti běžícího programu. Nazývá se ukazatel (pointer). Kromě toho, že reprezentuje adresu, tak každý datový typ ukazatele také obsahuje informaci o tom, jaký typ hodnoty by měl být uložen v paměti na adrese obsažené v ukazateli. Poté říkáme, že ukazatel "ukazuje na" daný datový typ.
Abychom vytvořili datový typ ukazatele, vezmeme datový typ, na který bude ukazovat, a přidáme za něj
hvezdičku (*
). Takto například vypadá proměnná datového typu "ukazatel na int
"2:
2Je jedno, jestli hvězdičku napíšete k datovému typu (int* p
) anebo k názvu proměnné
(int *p
), bílé znaky jsou zde ignorovány. Pozor však na vytváření více ukazatelů na
jednom řádku.
int* ukazatel;
Je důležité si uvědomit, co tato proměnná reprezentuje. Datový typ int*
zde říká, že v proměnné
ukazatel
bude uloženo číslo, které budeme interpretovat jako adresu. V paměti na této adrese poté
bude ležet číslo, které budeme interpretovat jako datový typ int
(celé číslo se znaménkem).
Ukazatele lze libovolně "vnořovat", tj. můžeme mít například "ukazatel na ukazatel na celé číslo"
(int**
). Ukazatel ale i tehdy bude prostě číslo, akorát ho budeme interpretovat jako adresu jiné
adresy. Pro procvičení je níže uvedeno několik datových typů spolu s tím, jak je interpretujeme.
int
- interpretujeme jako celé čísloint*
- interpretujeme jako adresu, na které je uloženo celé číslofloat*
- interpretujeme jako adresu, na které je uloženo desetinné čísloint**
- interpretujeme jako adresu, na které je uložena adresa, na které je uloženo celé číslo
Někdy chceme použít "univerzální" ukazatel, který prostě obsahuje adresu, bez toho, abychom striktně
určovali, jak interpretovat hodnotu na dané adrese. V tom případě můžeme použít datový typ void*
.
Velikost všech ukazatelů v programu je obvykle stejná a je dána použitým operačním systémem a překladačem. Ukazatele musí být dostatečně velké, aby zvládly reprezentovat libovolnou adresu, která se v programu může vyskytnout. Na vašem počítači to bude nejspíše 8 bytů, protože pravděpodobně používáte 64-bitový systém.
Inicializace ukazatele
Jelikož před spuštěním programu nevíme, na jaké adrese budou uloženy hodnoty, které nás budou
zajímat, tak obvykle nedává smysl inicializovat ukazatel na konkrétní adresu (např. int* p = 5;
).
Pro inicializaci ukazatele tak existuje několik standardních možností:
-
Inicializace na nulu Pokud chceme vytvořit "prázdný" ukazatel, který zatím neukazuje na žádnou validní adresu, tak se dle konvence inicializuje na hodnotu
0
. Takovému ukazateli se pak říká nulový ukazatel (null pointer). Jelikož datový typ výrazu0
jeint
, tak před přiřazením této hodnoty do ukazatele jej musíme přetypovat na datový typ cílového ukazatele:float* p = (float*) 0;
Jelikož tento typ inicializace je velmi častý, standardní knihovna C obsahuje makro
NULL
, které konverzi nuly na ukazatel provede za vás. Můžete jej najít například v souborustdlib.h
:#include <stdlib.h> // ... float* p = NULL;
-
Využití alokační funkce Pokud budete alokovat paměť manuálně, tak použijete funkce, které vám vrátí adresu jako svou návratovou hodnotu.
-
Využití operátoru adresy Pokud chcete ukazatel nastavit na adresu již existující hodnoty v paměti, můžete použít operátor adresy (address-of operator). Ten má syntaxi
&<proměnná>
. Tento operátor se vyhodnotí jako adresa předané proměnné3:3Všimněte si, že pro výpis ukazatelů ve funkci
printf
se používá%p
místo%d
.#include <stdio.h> int main() { int x = 1; int* p = &x; printf("%d\n", x); // hodnota proměnné x printf("%p\n", p); // adresa v paměti, kde je uložena proměnná x return 0; }
Výraz předaný operátoru
&
se musí vyhodnotit na něco, co má adresu v paměti (většinou to bude proměnná). Nedává tedy smysl použít něco jako&5
, protože 5 je číslo, které nemá samo o sobě žádnou adresu v paměti.Při použití tohoto operátoru je také třeba dávat si pozor na to, aby hodnota v paměti, jejíž adresu použitím
&
získáme, stále existovala, když se budeme později snažit k této adrese pomocí ukazatele přistoupit. V opačném případu by mohlo dojít k paměťové chybě 💣.
Přístup k paměti pomocí ukazatele
Když už máme v ukazateli uloženou nějakou (validní) adresu v paměti, tak k této paměti můžeme
přistoupit pomocí operátoru dereference. Ten má syntaxi *<výraz typu ukazatel>
. Při použití
tohoto operátoru na ukazateli program přečte adresu v ukazateli, podívá se do paměti a načte hodnotu
uloženou na této adrese. Podle toho, na jaký datový typ ukazatel ukazuje, se načte odpovídající
počet bytů z paměti:
#include <stdio.h>
int main() {
int cislo = 1;
int* ukazatel = &cislo;
printf("%p\n", ukazatel);
printf("%d\n", *ukazatel);
printf("%d\n", cislo);
return 0;
}
V tomto programu se do proměnné ukazatel
uloží adresa proměnné cislo
, a poté dojde k načtení
hodnoty (*ukazatel
) této proměnné z paměti přes adresu uloženou v ukazateli.
Interaktivní vizualizace kódu
Pokud chceme do adresy uložené v ukazateli naopak nějakou hodnotu zapsat, tak můžeme operátor dereference použít také na levé straně operátoru zápisu:
#include <stdio.h>
int main() {
int cislo = 1;
int* ukazatel = &cislo;
*ukazatel = 5;
printf("%d\n", cislo);
return 0;
}
Tento program vypíše 5
, protože jsme pomocí ukazatele změnili hodnotu na adrese v paměti, kde leží
proměnná cislo
. Když při výpisu poté načteme hodnotu proměnné cislo
, tak už v ní bude upravená
hodnota.
Interaktivní vizualizace kódu
Pokud provádíte operace s přímo s proměnnou ukazatele, budete vždy pracovat "pouze" s adresou, která je v něm uložena. Pokud chcete načíst nebo změnit hodnotu, která leží v paměti na adrese uložené v ukazateli, musíte použít operátor dereference.
Pozor na rozdíl mezi
*
používanou pro deklaraci datového typu ukazatel, operátorem dereference a operátorem násobení. Všechny tyto věci sice používají hvězdičku, ale jinak spolu nesouvisí. Vždy záleží na kontextu, kde jsou tyto znaky použity:// hvězdička říká, že datový typ proměnné `p` je ukazatel na `int` int* p; // hvězdička provede dereferenci návratové hodnoty funkce `vrat_ukazatel` int x = *vrat_ukazatel(); // hvězdička provede násobení dvou čísel int a = 5 * 6;
Aritmetika s ukazateli
Abychom se mohli v paměti "posouvat" o určitý kus dopředu či dozadu (relativně k nějaké adrese),
můžeme k ukazatelům přičítat či odčítat čísla. Toto se označuje jako aritmetika s ukazateli
(pointer arithmetic). Tato aritmetika má důležité pravidlo – pokud k ukazateli na konkrétní datový
typ přičteme hodnotu n
, tak se adresa v ukazateli zvýší o n
-násobek velikosti datového typu,
na který ukazatel ukazuje. Při aritmetice s ukazateli se tak neposouváme po jednotlivých bytech,
ale po celých hodnotách daného datového typu4.
4Z toho vyplývá, že aritmetiku nemůžeme provádět nad ukazateli typu void*
, protože ty neukazují
na žádný konkrétní datový typ.
Například, pokud bychom měli ukazatel int* p
s hodnotou 16
(tj. "ukazuje" na adresu 16
) a
velikost int
u by byla 4
, tak výraz p + 1
bude ukazatel s hodnotou 20
, výraz p + 2
bude
ukazatel s adresou 24
atd.
Je důležité rozlišovat, jestli při použití sčítání/odčítání pracujeme s hodnotou ukazatele anebo s hodnotou na adrese, která je v ukazateli uložena:
int x = 1;
int* p = &x;
*p += 1; // zvýšili jsme hodnotu na adrese v `p` (tj. proměnnou `x`) o `1`
p += 1; // zvýšili jsme adresu v `p` o `4` (tj. p nyní už neukazuje na `x`)
K čemu je aritmetika s ukazateli užitečná se dozvíte v sekci o práci s více proměnnými zároveň.
Kromě dereference a aritmetiky lze s ukazateli provádět také porovnávání (klasicky pomocí operátoru
==
). Díky tomu můžeme zjistit, jestli se dvě adresy rovnají.
Využití ukazatelů
Jak se dozvíte v následující sekci, ukazatele jsou nezbytné pro dynamickou alokaci paměti. Hodí se také při práci s více proměnnými zároveň. Kromě toho je ale lze použít také například v následujících situacích, které všechny souvisí s předáváním adres (ukazatelů) do funkcí:
-
Změna vnějších hodnot zevnitř funkce Hodnoty argumentů předávaných při volání funkcí se do funkce kopírují, nelze tak jednoduše zevnitř funkce měnit hodnoty proměnných, které existují mimo danou funkci. To je sice samo o sobě vhodná vlastnost, protože pokud bude funkce měnit pouze své lokální proměnné, případně parametry, tak bude jednodušší se v ní vyznat. Nicméně, někdy opravdu chceme ve funkci změnit hodnoty externích proměnných.
Toho můžeme dosáhnout tak, že si do funkce místo hodnoty proměnné pošleme její adresu v ukazateli, a pomocí této adresy pak hodnotu proměnné změníme. Takto například můžeme vytvořit funkci, která vezme adresy dvou proměnných a prohodí jejich hodnoty:
#include <stdio.h> void vymen(int* a, int* b) { int docasna_hodnota = *a; // načti hodnotu na adrese v `a` *a = *b; // načti hodnotu na adrese v `b` a ulož ji na adresu v `a` *b = docasna_hodnota; // ulož uloženou hodnotu na adresu v `b` } int main() { int x = 5; int y = 10; vymen(&x, &y); printf("Po prehozeni: x=%d, y=%d\n", x, y); return 0; }
Interaktivní vizualizace kódu
-
Vrácení více návratových hodnot Posílání adres proměnných do funkce můžeme využít také k tomu, abychom z funkce vrátili více než jednu návratovou hodnotu (do adres uložených v parametrech totiž můžeme zapsat "návratové" hodnoty). Toho bychom však měli využívat pouze, pokud je to opravdu nezbytné. Takovéto funkce je totiž složitější volat a nejsou čisté, protože obsahují vedlejší efekt - mění externí stav programu.
#include <stdio.h> void vrat_dve_hodnoty(int* a, int* b) { *a = 5; *b = 6; } int main() { int a = 0; int b = 0; vrat_dve_hodnoty(&a, &b); printf("a=%d, b=%d\n", a, b); return 0; }
-
Sdílení hodnot bez kopírování Pokud bychom měli proměnné, které v paměti zabírají velké množství bytů (například struktury), a předávali je jako argumenty funkci, tak může být zbytečně pomalé je pokaždé kopírovat. Pokud do funkce pouze předáme jejich adresu, tak dojde ke kopii pouze jednoho čísla s adresou, nezávisle na tom, jak velká je proměnná, která je na dané adrese uložena. Ukazatele tak můžeme použít ke sdílení hodnot v paměti mezi funkcemi bez toho, abychom je kopírovali.
Konstantní ukazatele
Pokud použijeme klíčové slovo const
v kombinaci s ukazateli, je
potřeba si dávat pozor na to, k čemu se tohle klíčové slovo váže. To závisí na tom, zda je const
v datovém typu před nebo za hvězdičkou. Zde jsou možné kombinace, které můžou vzniknout u
jednoduchého ukazatele:
int*
- ukazatel na celé číslo. Adresu v ukazateli lze měnit, hodnotu čísla na adrese v ukazateli také lze měnit.const int*
- ukazatel na konstantní celé číslo. Adresu v ukazateli lze měnit, hodnotu čísla na adrese v ukazateli nikoliv.int const *
- konstantní ukazatel na celé číslo. Adresu v ukazateli nelze měnit, hodnotu čísla na adrese v ukazateli lze měnit.const int const *
- konstantní ukazatel na konstantní celé číslo. Adresu v ukazateli nelze měnit, hodnotu čísla na adrese v ukazateli také nelze měnit.
Definice více ukazatelů najednou
Pokud byste chtěli vytvořit více ukazatelů
najednou, musíte si dát pozor na to, že
v tomto případě se hvězdička vztahuje pouze k jednomu následujícímu názvu proměnné. Tento kód tak
vytvoří ukazatel s názvem x
, a dvě celá čísla s názvy y
a z
:
int* x, y, z;
Pokud byste chtěli vytvořit tři ukazatele, musíte dát hvězdičku před každý název proměnné:
int* x, *y, *z;
Kvíz 🤔
-
Co vypíše následující program?
#include <stdio.h> int main() { int a = 2; int* p = &a; p = 5; printf("%d\n", a); return 0; }
Odpověď
Program vypíše
2
. Přiřazenímp = 5
změníme adresu uloženou v ukazatelip
na5
. Touto operací se tedy nijak nezmění hodnota proměnnéa
, jejíž adresu ukazatel před tímto přiřazením obsahoval. Aby k tomuto došlo, museli bychom napsat*p = 5
. -
Co vypíše následující program?
#include <stdio.h> int main() { int a = 2; int b = 3; int* p = &a; p = &b; *p += 1; printf("a = %d, b = %d\n", a, b); return 0; }
Odpověď
Program vypíše
a = 2, b = 4
. Nejprve jsme sice nastavili ukazatelp
na adresu proměnnéa
, ale poté jsme dop
zapsali adresu proměnnéb
. Řádek*p += 1;
tak zvedne hodnotu v paměti na adrese, kde ležíb
, o jedničku, jinak řečeno zvýší hodnotu proměnnéb
o jedničku. -
Co vypíše následující program?
#include <stdio.h> void zmen_ukazatel(int* p, int a) { p = &a; } int main() { int a = 2; int b = 3; int* p = &b; zmen_ukazatel(p, a); printf("%d\n", *p); return 0; }
Odpověď
Program vypíše
3
. Když předáme argument typu ukazatele do funkce, tak stejně jako u jiných datových typů dojde k tomu, že se ve funkci vytvoří nová proměnná a do ní se nakopíruje hodnota argumentu. Změna adresy v ukazatelip
uvnitř funkcezmen_ukazatel
tak neovlivní adresu v ukazatelip
uvnitř funkcemain
. A jelikožp
vmain
u ukazuje na proměnnoub
, tak dereference tohoto ukazatele se vyhodnotí jako hodnota3
. -
Co vypíše následující program?
#include <stdio.h> int main() { int a = 2; int* p = &a; *p = 4; int b = *p; *p = 8; printf("a = %d, b = %d\n", a, b); return 0; }
Odpověď
Program vypíše
a = 8, b = 4
. Při vytváření proměnnéb
se hodnota na adrese uložené v ukazatelip
uloží dob
. V danou chvíli je na této adrese uložena hodnota4
, proto se do proměnnéb
uloží právě hodnota4
. Další změny hodnot na adrese uložené vp
už proměnnoub
neovlivní. -
Co vypíše následující program?
#include <stdio.h> int main() { int a = 2; int* p = &a; printf("%d\n", p); return 0; }
Odpověď
Tento program obsahuje nedefinované chování 💣, protože jsme použili zástupný znak
%d
, který slouží k výpisu celých čísel, ale předali jsme funkciprintf
argumentp
, který je datového typu ukazatel.Správně můžeme buď použít zástupný znak
%p
, abychom vypsali adresu uloženou v ukazateli, nebo můžeme použít dereferenci a vypsat hodnotu uloženou na adrese v ukazateli:printf("%p\n", p); printf("%d\n", *p);
-
Co vypíše následující program?
#include <stdio.h> int main() { int a = 2; int b = 3; int* p = &a; int** px = &p; *px = &b; *p = 8; printf("a = %d, b = %d\n", a, b); return 0; }
Odpověď
Program vypíše
a = 2, b = 8
. Proměnnápx
je ukazatel na ukazatel naint
. Obsahuje adresu, kde v paměti leží proměnnáp
. Pomocí*px
změníme hodnotu na této adrese na&b
, tj. adresu proměnnéb
. V podstatě je to to stejné, jako kdybychom napsalip = &b
.Zkuste si na papír nakreslit, jak tento program bude vypadat v paměti, jaké adresy/hodnoty budou v jednotlivých proměnných. Výsledek si můžete ověřit touto vizualizací.