Ukazatele a adresy
Pro účely tohoto semináře si paměť reprezentujeme pomocí pole, kde káždý prvek (buňka) má pořadové číslo (adresu) a velikost každé buňky je 1B.
Uvažujme následující program, kde máme definované 2 proměnné, character a number. Proměnná charakter je typu
char
a number je typu int
. Jak víme, tak datový typ char
zabírá 1B v paměti,
což je v naší reprezentaci paměti jedna buňka. Datový typ int
zabírá 4B, což jsou 4 buňky paměti.
char character = '?';
int number = 125488632;
Adresa proměnné
Prozatím jsme pracovali pouze s hodnotami uloženými v paměti. Jazyk C umožňuje ale pracovat
i s adresami hodnot uložených v paměti. Pro to abychom mohli s touto adresou pracovat, tak musíme mít
operaci která by nám vrátila adresu proměnné jako výsledek. K tomu máme unární operátor reference
&
.
char character = '?';
int number = 125488632;
address = &number; // 27
address = &character; // 24
Pokud do proměnnné address
uložíme adresu proměnné character
, tak říkáme,
že address
ukazuje na character
. Ted již víme jak získat adresu proměnné
a uložit si ji. Pro získání hodnoty na dané adrese nám slouží unární operace dereference *
.
Abychom věděli na co se má vyhodnotit *address
musíme zavést nový typ "ukazatel na [typ]" (angl. pointer).
Ukazatel na typ
Ukazatel je datový typ, který slouží k uložení adresy v paměti počítače. V následujícím příkladu
definujeme ukazatel na char
a ukazatel na int
.
char character = '?';
int number = 125488632;
int *address_n = &number; // ukazatel na int
char *address_c = &character; // ukazatel na char
Do ukazele musíme jako první uložit adresu, bez toho by se program choval neočekávaně (viz dále). Ukazatel je omezen na konktrétní datový typ. V jazyce C je jedna vyjímka a to "ukazatel na void", ten slouží pro uchování ukazatele libovolného typu. Tento ukazatel ale nelze dereferencovat (ziskat jeho hodnotu).
Zápis hodnoty
Nálsedující příklad ukazuje jak změnit hodnotu uloženou v paměti na kterou ukazuje ukazatel.
Zápis provádíme pomocí operátoru dereference *
.
char character = '?';
int number = 125488632;
char *address_c; // Definujeme ukazatel na char
address_c = &character; // Do ukazatele uložíme adresu proměnné character
*address_c = ’!’; // Změníme hodnotu proměnné character
int *address_n; // Definujeme ukazatel na int
address_n = &number; // Do ukazatele uložíme adresu proměnné number
*address_n = 666; // Změníme hodnotu proměnné number
printf("%c, %i\n", character, number);
Po výkonání kódu výše bude paměť vypadat následovně:
Nyní se podíváme na situaci, kdy zapomeneme do ukazatele uložit adresu. Mějme tedy ukzatel
na char a chceme změnit jeho hodnotu (ne adresu) na hodnotu 19. Z obrázku níže je patrné, že
nevíme jakou adresu obsahuje ukazatel address_c
a nevíme tedy kam "ukazuje".
Při snaze změnit hodnotu na místě kam ukazatel ukazuje dojde k tzv. segmentation fault.
Jedná se o nepovolený přístup do paměti. Po spuštění je programu operačním systémem alokována
paměť kterou může během výpočtu program používat. Pokud program přitoupí mimo tuto povolenou oblast
tak docházi k nepovolenému přístupu do paměti a program se neočekávaně ukončí (spadne).
char *address_c;
*address_c = 19;
Použití ukazatele
Pokud máme ukazatel *pointer
na proměnnou number
, tak *pointer
můžeme používat v jakémkoliv kontextu,
ve kterém se může objevit number
.
int number = 10;
int *pointer;
pointer = &number;
printf("%i\n", number + 5 + *pointer); // 25
printf("%i\n", *pointer * 5); // 50
(*pointer)++;
printf("%i, %i\n", *pointer, number); // 11, 11
Ukazatele a argumenty funkcí
Jak víme, tak v jazyce C nemohou funkce ovlivňovat hodnoty předaných proměnných a to kvůli předávání argumentů hodnotou. V minulém semestru jsme zkoušeli naprogramovat funkci na prohození hodnot dvou proměnných.
Nesprávná implementace
void swap(int a, int b) {
int swap_help = a;
swap_help = a;
a = b;
b = swap_help;
}
int main() {
char width = 88; height = 90;
swap(width, height);
}
Výše uvedená implementace nefunguje protože hodnoty proměnných width
a height
se překopírují na jiné místo v paměti, viz obrázek níže.
Správná implementace
Níže uvedená implementace předává argumenty odkazem. Místo hodnoty se tedy předá ukazatel na proměnnou.
Díky tomu máme v argumentech funkce swap
uloženy adresy proměnných a můžeme tedy změnit jejich hodnoty.
void swap(char *a, char *b) {
int swap_help = *a;
*a = *b;
*b = swap_help;
}
int main() {
char width = 88; height = 90;
swap(&width, &height);
}
Aritmetika ukazatelů
Adresa je obyčejné číslo, takže s ním můžeme (skoro) normálně počítat.
K adrese můžeme přičítat/odečítat jiné hodnoty. Důležitý je typ ukazatele.
Jinak se počítá se ukazatelem na char
než na short
atp.
Pokud napíšeme pointer + 1, chceme tím získat adresu, kde se nachází další proměnná se stejným typem.
Příklad
char a[] = {2, 4, 8};
short n[] = {500, 600};
char *a_pointer = &a[0];
short *n_pointer = &n[0];
printf("%i\n", *(a_pointer + 1)); // 4
printf("%i\n", *(n_pointer + 1)); // 600
// Jakoby udelal: n_pointer + (1 * short_size)
Ukazatele na pole
V jazyce C více méně platí že pole = ukazatel. Identifikátor pole se chová jako konstantní ukazatel na první prvek pole.
Příklad
char a[] = {2, 4, 8, 16, 32};
char* first = a;
printf("%i, %i, %i\n", a[2], *(a + 2), *(first + 2)); // 8, 8, 8
// p[i] je ekvivalentni *(a + i)
Pokud chceme předat pole jako argument funkce tak ve skutečnosti předáváme funkci pouze ukazatel na první prvek tohoto pole. Samozřejmě je možné předat ukazatel na kterýkoliv prvek pole.
char a[] = {2, 4, 8, 16, 32};
// Ukazatel na 3 prvek pole
char *a_part = &a[2];
Nulová adresa a ukazatel na void
Nulová adresa je vždy neplatná. Používáme ji pro signalizaci toho, že ukazatel nikam neukazuje.
V jazyce C máme definovanou konstantu NULL
.
int *pointer;
pointer = 0 // Nepoužívat!!
pointer = NULL // Lepší řešení
// Pak můžeme lehce provést výpočet pouze pokud máme nenulový ukazatel.
if (pointer) {
// Práce s ukazatelem
}
Jak již bylo zmíněno, tak v jazyku C máme speciální typ ukazatele který může odkazovat na hodnotu libovolného typu. Problém je že nelze získat jeho hodnotu. Před dereferencí musím nejdříve ukazatel tzv. přetypovat na jiný typ.
int number = 5;
void *pointer = &number;
*pointer = 7; // NEJDE!!!
*((int*) pointer) = 7; // Přetypování na ukatazatel na int
Ukazatele na znaky
Řetězce v C jsou reprezentovány pole znaků, kde počet prvků pole je o 1 větší než je počet znaků v řetězci,
aby bylo možné na konec vnitřní reprezentace vložit znak konce řetězce '\0'
.
Nejčastěji používáme řetězce jako konstanty které předáváme jako argumenty funkcí.
printf("Zadejte celé číslo: ");
Funkci printf
je předán ukazatel na první prvek řetezce. Řetězcová konstanta nemusí být
jen argumentem funkce ale můžeme ji uložit do proměnné.
char *text_pointer;
text_pointer = "Zadejte celé číslo: ";
Nyní se podíváme na rozdíl mezi char text[]
a char *text
.
char text[] = "Zadejte celé číslo: "; // Pole
char *text_pointer = "Zadejte celé číslo: "; // Ukazatel
U textového řetězce definovaného jako pole můžeme měnit jeho hodntoty. Pokud ale definujeme
řetězovou konstantu jako ukazatel tak pří snaze změnit jednotlivé znaky dojde k neočekávanému chování.
tatto situace nastane protože ukazatel text_pointer
ukazuje na místo v paměti které slouží pouze
ke čtení.
Ukazatele na struktury
Struktury jsou stejně jako číslené hodnoty předávány hodnotou. Abychom dosáhli tohoto chování tak musím předat strukturu odkazem. Docílíme toho stejně jako u primitivních datových typů.
typedef struct {
int x;
int y;
} Point;
void move_point(Point *point, int x_offset, int y_offset) {
// Pokud pracujeme s ukazatelem na strukturu, tak místo . musíme
// k přístupu k hodnotám použít ->, což je kombinace operaci * a .
// Bez použití ->
(*point).x += x_offset;
// Lepší řešení
point->x += x_offset;
point->y += y_offset;
}
int main() {
Point point_a = {0, 0};
move_point(&point_a, -10, 33);
printf("[%i, %i]\n", point_a.x, point_a.y);
return 0;
}
Shrnutí
- Operátor reference
&
vrací adresu proměnné. - Operátor dereference
*
zjistí hodnotu proměnné na dané adrese. - Pokud deklarujeme nový ukazatel pomocí
int *ukaz;
, musíme do něj později uložit adresu, na kterou má ukazovat. - Pokud má funkce vracet ukazatel, přidáme za typ hvězdičku:
int* return_pointer() {...}
- Identifikátor pole slouží zároveň jako ukazatel na první prvek pole.
- Ukazatel můžeme indexovat stejně jako pole:
*(array + 3)
aarray[3]
je stejné.*(pointer + 3)
apointer[3]
také.
Úkoly
Povolené knihovny: stdlib.h
, stdio.h
K řešení použijte pointerovou aritmetiku.
&
a dereference *
.
Očekávaný výstup
Pointer : Demonstrate the use of & and * operator :
--------------------------------------------------------
m = 300
fx = 300.600006
cht = z
Using & operator :
-----------------------
address of m = 0x7ffda2eeeec8
address of fx = 0x7ffda2eeeecc
address of cht = 0x7ffda2eeeec7
Using & and * operator :
-----------------------------
value at address of m = 300
value at address of fx = 300.600006
value at address of cht = z
Using only pointer variable :
----------------------------------
address of m = 0x7ffda2eeeec8
address of fx = 0x7ffda2eeeecc
address of cht = 0x7ffda2eeeec7
Using only pointer operator :
----------------------------------
value at address of m = 300
value at address of fx= 300.600006
value at address of cht= z
Napište v jazyku C funkci int porovnej(char *t1, char *t2)
, která porovná předané textové řetězce a vrátí -1,
pokud je první řetězec menší než druhý, 0, pokud jsou řetězce shodné, nebo 1, pokud je druhý řetězec menší než první.
Při práci s textovými řetězci používejte výhradně ukazatele, operátor dereference a pointerovou aritmetiku.
Porovnávání řetězců by mělo být lexikografické, tj. obdobné uspořádání slov ve slovníku. Budou tedy porovnávány
jednotlivé odpovídající si dvojice znaků (i-tý znak prvního řetězce s i-tým znakem druhého řetězce) počínaje prvními
znaky obou řetězců, první rozdílná dvojice znaků pak určí výsledek porovnání obou řetězců. Tento způsob porovnání
textových řetězců plně odpovídá funkci strcmp
.
char *strrstr(const char *text, const char *hledany)
, která v daném textovém řetězci text vyhledá první výskyt
zadaného podřetězce hledany zprava. Funkce vrací ukazatel na první znak nalezeného podřetězce nebo konstantu NULL, pokud podřetězec
hledany nebyl nalezen. Vytvořenou funkci otestujte ve funkci main
.
void vypis(int *pole, int zacatek, int krok, int konec)
, která vypíše prvky pole od
indexu zacatek
po index konec
s krokem krok. Například pro pole = {1,2,3,4,5,6,7,8,9,10} a zacatek
= 0,
krok
= 2, konec
= 9 vypíše prvky 1, 3, 5, 7, 9.