V tomto článku bych se rád pokusil vysvětlit, co jsou to ukazatele v jazyce C a jak s nimi pracovat. Uvidíme, že se ve skutečnosti jedná o jednoduchý koncept, kterého není třeba se bát.
Každý program spuštěný v operačním systému je načtený do paměti (RAM) a spuštěn. Operační paměť není nic jiného než posloupnost bajtů, z nichž každý má svoji adresu, což není nic jiného než obyčejné číslo (bez znaménka). 64-bitové operační systémy jsou charakteristické tím, že adresa každého bajtu paměti je 64-bitové číslo (tj. 8-bajtové číslo).
Operační systém vytváří pro každý spuštěný program (proces) takzvaný virtuální adresový prostor. Z hlediska programu to vypadá, že běží na počítači sám. Jakákoli adresa v paměti programu je tzv. virtuální adresa. Když program na tuto adresu přistoupí (ať už pro čtení, nebo pro zápis), elektronická součástka v procesoru (MMU = memory management unit) tuto adresu transparentně překládá na skutečnou - fyzickou adresu v RAM paměti - tento překlad je běžnému programu zcela skryt. Jakákoli adresa, se kterou v programech pracujeme, je vždy virtuální - různé běžící programy mohou používat jednu a tu samou virtuální adresu, která je pokaždé "namapovaná" na jinou fyzickou adresu - podle toho, jaký program na ni přistupuje.
Ukazatel v jazyce C je proměnná, která obsahuje (virtuální) adresu. Na 64-bitovém systému jsou tedy ukazatele 64-bitová čísla (tj. zabírají 8 bajtů). Takže ukazatel je vlastně obyčejná celočíselná hodnota (bez znaménka). Jazyk C poskytuje unární operátor *, tzv. operátor dereference. Tento operátor je možné použít pouze na ukazatel a jeho použitím přistoupíme na adresu, kterou ukazatel obsahuje.
Krom toho máme ještě unární operátor &, což je operátor "dej (virtuální) adresu proměnné".
Podívejme se na následující program:
#include <stdio.h>
int main(int argc, char** argv) {
int cislo1 = 666, cislo2 = 42;
int* ukazatel = &cislo1;
printf("&cislo1 = : %u\n", &cislo1);
printf("&cislo2 = : %u\n", &cislo2);
printf("ukazatel = : %u\n", ukazatel);
printf("*ukazatel: %d\n", *ukazatel);
printf("sizeof(int) = %d\n", sizeof(int));
printf("sizeof(int*) = %d\n", sizeof(int*));
printf("sizeof(char*) = %d\n", sizeof(char*));
printf("&ukazatel = %u\n", &ukazatel);
return 0;
}
Toto je výstup na 64-bitovém Linuxu:
root@kaliLinux-1:/dev/shm# gcc -o ukazatele ukazatele.c
root@kaliLinux-1:/dev/shm# ./ukazatele
&cislo1 = : 2289156860
&cislo2 = : 2289156856
ukazatel = : 2289156860
*ukazatel: 666
sizeof(int) = 4
sizeof(int*) = 8
sizeof(char*) = 8
&ukazatel = 2289156848
root@kaliLinux-1:/dev/shm#
Vidíme, že proměnná cislo1
začíná na zásobníku na adrese 2289156860.
Protože je typu int, má velikost 4 bajty - proto nás nepřekvapí, že o 4 bajty vedle
je hned další proměnná - cislo2
, a to na adrese 2289156856.
Všimněte si, že ukazatel
není nic než obyčejná proměnná (o velikosti 8 bajtů), která
začíná na adrese 2289156848, tj. 8 bajtů před proměnnou cislo2
.
Protože ukazatel
je proměnná, mohli bychom mít i ukazatel na ukazatel - např. kdybychom v programu
napsali
int** ukazatel2 = &ukazatel;
Představme si, že máme lokální proměnnou int cislo = 5;
a chceme, aby nějaká jiná funkce mohla
změnit její hodnotu. Podívejme se na následující program:
#include <stdio.h>
void nastav_na_deset(int co) {
printf("co = %d\n", co);
printf("&co = %u\n", &co);
co = 10;
printf("co = %d\n", co);
}
int main(int argc, char** argv) {
int cislo = 5;
printf("cislo = %d\n", cislo);
printf("&cislo = %u\n", &cislo);
nastav_na_deset(cislo);
printf("cislo = %d\n", cislo);
return 0;
}
Výstup:
root@kaliLinux-1:/dev/shm# gcc -o ukazatele ukazatele.c
root@kaliLinux-1:/dev/shm# ./ukazatele
cislo = 5
&cislo = 1540877036
co = 5
&co = 1540876988
co = 10
cislo = 5
root@kaliLinux-1:/dev/shm#
Vidíme, že změnit hodnotu proměnné cislo
se nám nepovedlo. Proč? Protože v jazyce C se při volání
funkcí předávají argumenty hodnotou, tj. volané funkci se hodnoty argumentů zkopírují na její část
zásobníku a k proměnným z volající funkce nemá vůbec přístup! Všimněte si, že proměnná cislo
se v paměti
nachází jinde, než proměnná co
- není divu, že se nám nepovedlo změnit její hodnotu.
No dobře, co když ale vážně chceme, aby funkce nastav_na_deset
změnila hodnotu proměnné cislo
?
Tak prostě funkci nastav_na_deset
předáme adresu proměnné cislo
.
#include <stdio.h>
void nastav_na_deset(int* co) {
printf("*co = %d\n", *co);
printf("co = %u\n", co);
printf("&co = %u\n", &co);
*co = 10;
printf("*co = %d\n", *co);
}
int main(int argc, char** argv) {
int cislo = 5;
printf("cislo = %d\n", cislo);
printf("&cislo = %u\n", &cislo);
nastav_na_deset(&cislo);
printf("cislo = %d\n", cislo);
return 0;
}
Výstup:
root@kaliLinux-1:/dev/shm# gcc -o ukazatele ukazatele.c
root@kaliLinux-1:/dev/shm# ./ukazatele
cislo = 5
&cislo = 2080744732
*co = 5
co = 2080744732
&co = 2080744680
*co = 10
cislo = 10
root@kaliLinux-1:/dev/shm#
Podařilo se! Při volání funkce nastav_na_deset
jsme této funkci předali adresu proměnné cislo
, a tato funkce
si ji uložila do ukazatele co
. Tentokrát jsme funkci předali adresu proměnné cislo
, nikoli jenom její hodnotu.
Dereferencováním ukazatele co
se nám proto podařilo přistoupit na místo v paměti, kde se nachází proměnná cislo
, a upravit její hodnotu.
Představme si, že chceme napsat funkci, které předáme ukazatel na int
a ona provede dynamickou alokaci pole 10 integerů, přičemž
ukazatel na toto pole zapíše do našeho ukazatele. Zkusíme bezmyšlenkovitě vysolit první kód, který nás napadne:
#include <stdio.h>
#include <stdlib.h>
void alokuj_a_nastav(int* ukazatel) {
printf("ukazatel = %u\n", ukazatel);
printf("&ukazatel = %u\n", &ukazatel);
ukazatel = malloc(sizeof(int) * 10);
printf("ukazatel = %u\n", ukazatel);
}
int main(int argc, char** argv) {
int* ukaz = NULL;
printf("ukaz = %u\n", ukaz);
printf("&ukaz = %u\n", &ukaz);
alokuj_a_nastav(ukaz);
printf("ukaz = %u\n", ukaz);
return 0;
}
Výstup:
root@kaliLinux-1:/dev/shm# gcc -o ukazatele ukazatele.c
root@kaliLinux-1:/dev/shm# ./ukazatele
ukaz = 0
&ukaz = 4173788056
ukazatel = 0
&ukazatel = 4173788008
ukazatel = 1933059104
ukaz = 0
root@kaliLinux-1:/dev/shm#
Kde je problém? Když jsme volali funkci alokuj_a_nastav
, tak jsme jí předali ukaz
. Ale ukaz
je ukazatel na int
, který jsme navíc nastavili na NULL
. Funkci alokuj_a_nastav
jsme tedy omylem předali NULL
- tj. hodnotu ukazatele ukaz
. To nechceme - my jí chceme předat adresu ukazatele ukaz
, aby do ní mohla zapsat výsledek funkce malloc
.
Tzn. budeme funkci alokuj_a_nastav
předávat adresu ukazatele - proměnná obsahující adresu ukazatele se nazývá ukazatel na ukazatele. Takto bude náš kód vypadat správně:
#include <stdio.h>
#include <stdlib.h>
void alokuj_a_nastav(int** ukazatel) {
printf("ukazatel = %u\n", ukazatel);
printf("&ukazatel = %u\n", &ukazatel);
printf("*ukazatel = %u\n", *ukazatel);
*ukazatel = malloc(sizeof(int) * 10);
printf("*ukazatel = %u\n", *ukazatel);
}
int main(int argc, char** argv) {
int* ukaz = NULL;
printf("ukaz = %u\n", ukaz);
printf("&ukaz = %u\n", &ukaz);
alokuj_a_nastav(&ukaz);
printf("ukaz = %u\n", ukaz);
return 0;
}
Výstup:
root@kaliLinux-1:/dev/shm# gcc -o ukazatele ukazatele.c
root@kaliLinux-1:/dev/shm# ./ukazatele
ukaz = 0
&ukaz = 3610751272
ukazatel = 3610751272
&ukazatel = 3610751208
*ukazatel = 0
*ukazatel = 4137292832
ukaz = 4137292832
root@kaliLinux-1:/dev/shm#