Pointery
Text níže se týká uživatelských programů běžících na běžném OS.
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.
Díky mechanismu virtuální paměti má běžící program 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 64bitová, jak tomu je na moderních počítačích, adresní rozsah je od 0 do 2^64 - 1.
V programu nemůžeme úplně libovolně určit, na jakých adresách jsou objekty uloženy. 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í operací, které můžeme s pamětí dělat, je zjištění adresy
na níž je uložen konkrétní objekt (např. proměnná).
Děláme to pomocí unárního prefixového operátoru adresy: &.
int x = 10;
printf("%p\n", &x); // vypiseme adresu, na ktere je promenna x
Pomocí %p vypisuje printf adresy, obvykle v hexadecimální
soustavě.
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*. Adresu můžeme
uložit do proměnné.
// typ 'pointer na int' zapisujeme int*
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 mohou
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;
printf("%i\n", *ptr_x); // vytiskne 10
*ptr_x = 20; // zapise na adresu ptr_x hodnotu 20
printf("%i\n", x); // vytiskne 20
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ý. Testovat, jestli je pointer neplatný, obecně nejde. 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)
{
printf("%i\n", *ptr_x);
}
Pole, pointery a pointerova aritmetika
Jméno pole je konstantní pointer s adresou prvního prvku pole.
int a[] = { 0,1,2,3,4,5 };
int* ptr_3 = a;
printf("a[0] = %i\n", *a); // tiskne 0
*ptr_3 = 10;
printf("a[0] = %i\n", a[0]); // tiskne 10
Pointer je konstatní, nelze tak měnit jeho hodnotu.
int b[] = { 10, 20 };
a = b; // Chyba: a je konstantni pointer a nelze do nej priradit jinou hodnotu.
Při předání pole jako argumentu funkci toto 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]);
printf("ve funkci: %i\n", size);
}
int main()
{
int a[] = { 0,1,2,3,4,5 };
int size = sizeof(array) / sizeof(array[0]);
printf("v mainu: %i\n", size);
fce(a);
}
Kvůli degeneraci tak vlastně můžeme do funkce předat přímo pointer.
void fce(int* array)
K prvků pole přistupujeme pomocí pointerové aritmetiky.
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 ptr je pointer, který získáme zvětšením ptr
o c-krát velikost typu, na který ptr ukazuje, bajtů. Pokud by byl například ptr
pointer na int, tak bychom jej 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;
printf("velikost int: %lu\n", sizeof(int));
for (int i = 0; i < 3; i += 1)
{
printf("index: %i, adresa: %p\n", i, a + i);
}
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;
printf("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
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.
printf("array[2] = %i\n", 2[array]); // vytiskne 3
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 (celočíselné dělení).
Pointery také můžeme mezi sebou porovnávat.
Pointery a struktury
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;
My_Str str = {10, 13.1};
My_Str* ptr = &str;
// oba radky vytisknou totez
printf("polozka1: %i, polozka2: %f\n", ptr->pol1, ptr->pol2);
printf("polozka1: %i, polozka2: %f\n", (*ptr).pol1, (*ptr).pol2);
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.
printf("adresa prvni polozky: %p\n", &str.pol1);
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ého je sama. Toho často využíváme při konstrukci datových struktur, například spojového seznamu.
typedef struct _node
{
int key; // data v uzlu
struct _node* next; // pointer na další prvek v seznamu
} Node;
Node storage[10];
for(int i = 0; i < 10; i += 1)
{
storage[i].key = i;
if (i < 9)
{
storage[i].next = &storage[i+1];
}
else
{
// .next == 0 znamená, že uzel nemá souseda, tj. je to poslední uzel v seznamu
//
storage[i].next = 0;
}
}
//
// teď je v seznamu 0, 1, 2, 3 ... 9
//
//
// projdeme seznam a vytiskneme jej
//
Node *first = storage;
while(first)
{
printf("%i ", first->key);
first = first->next;
}
printf("\n");
Parametry předané odkazem
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 celočíselné dělení
// argument rem předáme odkazem; je to adresa, na kterou funkce zapíše zbytek po dělení
//
int division(int a, int b, int* rem)
{
*rem = a % b;
return a / b;
}
int main()
{
int x = 0;
int y = 0;
x = division(10, 3, &y);
printf("vysledek %i, zbytek %i\n", x, y);
}
Úkoly
-
Naprogramujte vlastní verze knihovních funkcí
strlenastrcmp(string.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ř.
int). Jednu z hodnot vraťte pomocí argumentu předaného odkazem. (Funkce může změnit pořadí prvků v poli.) -
Napište funkci
swap_int, která prohodí hodnoty dvou proměnných typuint. -
Napište funkci, která v poli řetězců najde znak, který se vyskytuje v nejvíce řetězcích v tomto poli.