Skip to content

Instantly share code, notes, and snippets.

@TrinityCoder
Last active December 11, 2016 13:19
Show Gist options
  • Save TrinityCoder/a68320a0a3b4dffdb2f35c34f663a33e to your computer and use it in GitHub Desktop.
Save TrinityCoder/a68320a0a3b4dffdb2f35c34f663a33e to your computer and use it in GitHub Desktop.
Ukazatele v C

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.

Co je to ukazatel?

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;

Použití ukazatele

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.

Trošku složitější příklad na závěr

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#
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment