Seminář 1

Pointery

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;
printf("%p\n", &x);  // vypiseme adresu, na ktere je promenna x

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; 
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ý. 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) { 
    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 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]); 
    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) 

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;
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. Toto dělení je celočíselné.

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 (můžeme je psát kvůli čitelnosti, nebo “pro jistotu”).

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é 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;


Node storage[10];
for(int i = 0; i < 10; i += 1) {
    // naplnime seznam
    storage[i].key = i;
    if (i < 9) {
        storage[i].next = &storage[i+1];
    }
    else {
        storage[i].next = 0;       // posledni prvek nema souseda
    }
}

// projdeme seznam a vytiskneme jej
Node *first = storage;
while(first) {   // posledni uzel ma polozku next rovnu 0
    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 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;
    x = division(10, 3, &y);  // tady predame adresu promenne y
    printf("vysledek %i, zbytek %i\n", x, y); // vypise: vysledek 3, zbytek 1
}

Úkoly

  1. Naprogramujte vlastní verze knihovních funkcí strlen a strcmp (hlavičkový souborstring.h), které nepoužijí indexů, ale pouze pointerové aritmetiky a dereference.

  2. 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é.

  3. 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.

  4. Napište funkci swap_int, která prohodí hodnoty dvou proměnných typu int.