2. seminář - Ukazatele

TEACHINGZPC2

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.

computer memory

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;

computer memory

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

computer memory

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

pointers

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ě:

pointers

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;

pointers

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.

function call memory

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);
    }

function call memory

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)

function call memory

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)

function call memory

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

String pointers

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) a array[3] je stejné. *(pointer + 3) a pointer[3] také.

Úkoly

Povolené knihovny: stdlib.h, stdio.h

K řešení použijte pointerovou aritmetiku.

Napiště program v jazyce C který demostruje použití operátoru reference & 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.

Napište v jazyku C funkci 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.
Naprogramujte funkci 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.