Z pohledu programu se pamět jeví jako pole bajtů, kde indexu říkáme adresa. Je-li v paměti uložen nějaký objekt, jsou jeho jednotlivé bajty uloženy za sebou, přičemž první bajt je uložen na nejmenší adrese. Za adresu vícebajtového objektu považujeme právě adresu jeho prvního bajtu.
Program běžící na běžném OS má díky virtuální paměti přístup k celému adresnímu rozsahu na dané platformě. Adresní rozsah je množina adres, se kterými jsme schopni pracovat a je typicky dána velikostí adresy. Je-li například adresa 64-bitová, jak tomu je na moderních počítačích, adresní rozsah je od 0 do 264.
Při psaní programu se o to, na jakých konkrétních adresách jsou objekty uloženy, nestaráme. O tom rozhoduje operační systém. Pokus o zápis na nebo čtení z adresy, která nebyla programu operačním systémem zpřístupněna, vede k nedefinovanému chování.
Základní operace, které můžeme s pamětí dělat, je zjištění adresy na
níž je konkrétní objekt (např. proměnná). To se provádí pomocího
unárního prefixového operátoru adresy: &
.
int x = 10;
("%p\n", &x); // vypiseme adresu, na ktere je promenna x printf
Pomocí %p
vypisuje printf
adresy, je zvykem
je tisknout v hexadecimální soustavě.
Adresu můžeme uložit i do proměnné, k tomu ale potřebujeme znát její
typ. Ke každému existujícímu typu z je v programu automaticky i
typ adresa, na které je uložena hodnota typu z. Proměnným a
často i hodnotám tohoto nového typu programátoři říkají pointer na
z. Klíčovým slovem pro tento nový typ je pak z*
.
int x = 10;
int* ptr_x = &x;
Při definici pointeru se znak *
ve skutečnosti váže ke
jménu proměnné, to se ovšem projevuje pouze při definici více proměnných
na jednom řádku. To ovšem v kurzu nepovažujeme za dobrý styl a nebudeme
to dělat. Stejně tak můžou být před *
bílé znaky.
Je nutné si uvědomit, že samotná proměnná ptr_x
je v
paměti a operátorem adresy lze zjistit na jaké adrese.
int x = 10;
int* ptr_x = &x;
int** ptr_ptr_x = &ptr_x;
Druhou základní operací je přístup k hodnotě na dané adrese. To se
dělá pomocí unárního prefixového operátoru dereference: *
.
Tímto způsobem můžeme hodnotu číst i na dané místo zapisovat.
int x = 10;
int* ptr_x = &x;
("%i\n", *ptr_x); // vytiskne 10
printf*ptr_x = 20; // zapise na adresu ptr_x hodnotu 20
("%i\n", x); // vytiskne 20 printf
Je nutné si uvědomit, že k přístupu k hodnotě na dané adrese je nutné znát typ této hodnoty. To je zajištěno typovým systémem, kdy je v typu pointeru zachycen typ hodnoty na dané adrese.
Dereferencování pointeru, které vede k přístupu na adresu paměti, která není programu přidělena, vede k nedefinovanému chování. O takovém pointeru řekneme, že je neplatný. Obecně nelze testovat, jesli je pointer neplatný. Existuje ovšem jedna výjimka a tou je adresa 0 (jakéhokoliv typu). Ta je vždy neplatná a jako obecná pravdivostní hodnota odpovídá nepravdě. Je proto dobrým zvykem udržovat hodnoty neplatných pointerů rovny 0.
// pokud udrzujeme neplatne pointery rovny 0, nasledujici nevede k nedefinovanemu chovani
int* ptr_x = 0;
// nejaky kod, ktery mozna manipuluje s ptr_x
if (ptr_x) {
("%i\n", *ptr_x);
printf}
Jméno pole je konstantní pointer s adresou prvního prvku pole.
int a[] = { 0,1,2,3,4,5 };
int* ptr_3 = a;
("a[0] = %i\n", *a); // tiskne 0
printf*ptr_3 = 10;
("a[0] = %i\n", a[0]); // tiskne 10 printf
Pointer je konstatní, nelze tak měnit jeho hodnotu.
int b[] = { 10, 20 };
= b; // Chyba: a je konstantni pointer a nelze do nej priradit jinou hodnotu. a
Při předání pole jako argumentu funkce pole degeneruje na pointer.
void fce(int array[]) {
// array je typu int*
// size je tedy sizeof(int*) / sizeof(int)
int size = sizeof(array)/sizeof(array[0]);
("ve funkci: %i\n", size);
printf}
int main() {
int a[] = { 0,1,2,3,4,5 };
int size = sizeof(array) / sizeof(array[0]);
("v mainu: %i\n", size);
printf(a);
fce}
Kvůli degeneraci tak vlastně můžeme do funkce předat přímo pointer.
void fce(int* array)
Jak je to s přístupem k prvkům pole? To řeší pointerová aritmetika.
Protože pole je v paměti v souvislém bloku, lze adresu prvku na indexu
i
získat tak, že adresu začátku pole zvětšíme o
i
-krát velikost jednoho prvku pole. Tím se v paměti
posuneme z prvního bajtu prvku na indexu 0 na první bajt prvku na indexu
i
. V programu počet bajtů, o které je nutno se posunout
nemusíme počítat, aritmetické operace s pointery to udělají za nás. K
pointeru lze totiž přičíst, nebo od něj odečíst, celé kladné číslo.
Výsledkem přičtení čísla c
k pointeru pt
je
pointer, který získáme zvětšením pt
o c
-krát
velikost typu, na který pt
ukazuje. Pokud by byl například
pt
pointer na int
, tak bychom je zvětšili o
c
-krát sizeof(int
). Odečítání celého čísla
funguje analogicky.
int a[] = {0,1,2};
int* ptr = a;
("velikost int: %lu\n", sizeof(int));
printffor (int i = 0; i < 3; i += 1) {
("index: %i, adresa: %p\n", i, a + i);
printf}
Pointerové aritmetiky v kombinaci s operátorem dereference můžeme
využít pro přístup k prvkům pole. Použití []
je totiž jenom
syntaktický cukr pro pointerovou aritmetiku a následnou dereferenci.
int array[] = { 1, 2, 3, 4 };
int* ptr = array + 1;
("array[2] = %i\n", array[2]); // vytiskne 3
printf("*(array+2) = %i\n", *(array+2)); // vytiskne 3
printf("ptr[1] = %i\n", ptr[1]); // vytiskne 3 printf
Operátor dereference má větší prioritu než sčítání, je proto nutné použít závorky. Protože je navíc operátor sčítání komutativní, je korektní i následující kuriózní přístup k prvku na indexu 2.
("array[2] = %i\n", 2[array]); // vytiskne 3 printf
Všimněme si také přístupu pomocí pointeru ptr
. Ten
ukazuje, že pole v C skutečně jsou adresou prvního prvku a současně
každý pointer můžeme chápat jako adresu prvního prvku v poli (i když je
toto pole vlastně jednoprvkové). Přístup k jednotlivým prvkům je potom
realizován pomocí pointerové aritmetiky. V našem případě se můžeme na
ptr
dívat jako na pole, která začíná druhým prvkem
array
.
Pointery stejného typu můžeme od sebe odečíst, výsledkem je počet prvků daného typu, který se mezi obě adresy vejde. Je to tedy rozdíl adres (v bajtech) dělený velikostí typu. Toto dělení je celočíselné.
Pointery také můžeme mezi sebou porovnávat.
Pointeru na strukturu lze použít k přístupu k položkám struktury s
pomocí operátoru ->
, alternativně lze použít dereference
následované operátorem .
(tečka).
typedef struct {
int pol1;
float pol2;
} My_Str;
= {10, 13.1};
My_Str str * ptr = &str;
My_Str// oba radky vytisknou totez
("polozka1: %i, polozka2: %f\n", ptr->pol1, ptr->pol2);
printf("polozka1: %i, polozka2: %f\n", (*ptr).pol1, (*ptr).pol2); printf
Operátory adresy a dereference mají menší prioritu než operátor
tečky, proto jsme v příkladu museli použít závorky. V opačném případě
bychom udělali dvě chyby: přistupovali bychom k položce struktury pomocí
tečky a přitom ptr
je pointer na strukturu; a
dereferencovali bychom int
a float
. Naopak,
při použití operátorů adresy a dereference na položky struktury závorky
můžeme vynechat (můžeme je psát kvůli čitelnosti, nebo “pro
jistotu”).
("adresa prvni polozky: %p\n", &str.pol1); printf
Struktura nemůže obsahovat položku stejného typu jako je sama. Může ovšem obsahovat položku, která je pointerem na typ, které je sama. Toho často využíváme při kostrukci datových struktur, například spojového seznamu.
typedef struct _node {
int key; // data v uzlu
struct _node* next; // pointer na dalsi prvek v seznamu
} Node;
[10];
Node storagefor(int i = 0; i < 10; i += 1) {
// naplnime seznam
[i].key = i;
storageif (i < 9) {
[i].next = &storage[i+1];
storage}
else {
[i].next = 0; // posledni prvek nema souseda
storage}
}
// projdeme seznam a vytiskneme jej
*first = storage;
Node while(first) { // posledni uzel ma polozku next rovnu 0
("%i ", first->key);
printf= first->next;
first }
("\n"); printf
Už víme, že pokud předáme funkci pole jako argument a funkce změní některé jeho prvky, zachovají se tyto změny i po skončení funkce na místě, ze kterého jsme ji volali. Děje se tak proto, že volané funkci nepředáváme jako argument pole, ale jeho adresu.
Tohoto mechanismu můžeme využít i pro jiné argumenty než pole, pokud chceme umožnit, aby funkce mohla změnit jejich hodnotu. Stačí předat adresu argumentu, místo jeho hodnoty. Toho se často používá, potřebujeme-li z funkce vrátit více než jednu hodnotu.
// funkce pro celociselne deleni
// argument rem je predan odkazem, je to adresa, na kterou funkce zapisuje
int division(int a, int b, int* rem) {
*rem = a % b;
return a / b;
}
int main() {
int x = 0;
int y = 0;
= division(10, 3, &y); // tady predame adresu promenne y
x ("vysledek %i, zbytek %i\n", x, y); // vypise: vysledek 3, zbytek 1
printf}
Naprogramujte vlastní verze knihovních funkcí strlen
a strcmp
(hlavičkový souborstring.h
), které
nepoužijí indexů, ale pouze pointerové aritmetiky a
dereference.
Vytvořte strukturu s několika položkami a aspoň jednu její proměnnou. Vypište informace o layoutu této proměnné: pro každou položku vypište její adresu a velikost. Vypište i adresu a velikost samotné proměnné.
Naprogramujte funkci pro výpočet průměru a mediánu pole čísel
(např. float
). Jednu z hodnot vraťte pomocí argumentu
předaného odkazem.
Napište funkci swap_int
, která prohodí hodnoty dvou
proměnných typu int
.