Tento příspěvek vznikl jako doplňující materiál ke kurzu BI-PA2 v roce 2015 Díky Páje za jeho připomenutí :) Za fakultním přihlášením je přístupný zmigrovaný original na courses ↗.


Proměnná je jako hotovost. Ukazatel je jako bankovní účet – znáte číslo účtu a na peníze nelze šáhnout přímo, ale pouze zkrz číslo účtu.

Co je to pointer?

Ukazatel (anglicky Pointer) je proměnná, která místo hodnoty ukládá adresu jiné proměnné.

Adresa proměnné

Zdrojový kód (v C++):

#include <iostream>
using namespace std;

int main() {
    int a = 0;
    cout << "Adresa promenne 'a' = "<< &a << endl;
    cout << "Hodnota promenne 'a' = "<< a << endl;
}

Výpis:

Adresa promenne 'a' = 0x23cabc
Hodnota promenne 'a' = 0

Vysvětlení: Každá proměnná je v paměti (na RAMce) uložena na nějaké pozici. Tato pozice má svou unikátní Adresu. Adresu proměnné a zjistíme příkazem &a, v našem případě to bylo 0x23cabc. Vypsaná adresa proměnné bude pro každý běh programu nejspíše jiná, protože program dostane pokaždé novou část paměti, se kterou může pracovat.

Nastavení pointeru

Zdrojový kód:

#include <iostream>
using namespace std;

int main() {
    int a = 4;
    cout << "Adresa promenne 'a' = "<< &a << endl;
    cout << "Hodnota promenne 'a' = "<< a << endl;
    cout << endl;

    int * p;
    p = &a;
    cout << "Adresa promenne 'p' = "<< &p << endl;
    cout << "Hodnota promenne 'p' = "<< p << endl;

    return 0;
}

Výpis:

Adresa promenne 'a' = 0x23cabc
Hodnota promenne 'a' = 4

Adresa promenne 'p' = 0x23cab0
Hodnota promenne 'p' = 0x23cabc

Vysvětlení: Nyní jsme vytvořili ukazatel na proměnnou typu int – to znamená, že do hodnoty proměnné p jsme uložili adresu proměnné a (proměnná a je typu int). Ukazatel na proměnnou nějakého typu se značí typem s hvězdičkou. Takže ukazatel na int se značí int*, ukazatel na char je char* a ukazatel na objekt Auto je Auto*. Z příkladu vidíme, že hodnota proměnné p je adresa proměnné a. To je jediný a nejdůležitější rozdíl mezi běžnou proměnnou a ukazatelem.

Nastavení hodnoty proměnné přes ukazatel

Zdrojový kód:

#include <iostream>
using namespace std;

int main() {
    int a = 4;
    cout << "Hodnota promenne 'a' -> "<< a << endl;

    int * p;
    p = &a;
    *p = 42;

    cout << "Hodnota promenne 'a' -> "<< a << endl;

    return 0;
}

Výpis:

Hodnota promenne 'a' -> 4
Hodnota promenne 'a' -> 42

Vysvětlení: Stejně jako v minulém případě jsme nastavili hodnotu ukazatele p na adresu proměnné a. Dereference (neboli hvězdička v *p) je operace, kterou můžeme použít pouze na ukazatel. Dereference proměnné p znamená, že pracujeme s proměnnou na adrese hodnoty ukazatele! Díky této operaci můžeme pracovat s hodnotou proměnné a, aniž bychom nastavovali proměnnou a přímo. Použití *p má stejný výsledek jako kdybychom napsali a, takže *p = 42 je to samé jako a = 42.

Pochopení principu adresy a dereference je zásadní v pochopení ukazatelů!

Jak může funkce přepsat hodnotu předávaného argumentu?

Zdrojový kód:

#include <iostream>
using namespace std;

int funkce1(int b){
    b = 42;
}

int funkce2(int *p){
    *p = 42;
}

int main() {
    int a = 4;
    cout << "Hodnota promenne 'a' -> "<< a << endl;

    funkce1(a);
    cout << "Hodnota promenne 'a' -> "<< a << endl;

    funkce2(&a);
    cout << "Hodnota promenne 'a' -> "<< a << endl;

    return 0;
}

Výpis:

Hodnota promenne 'a' -> 4
Hodnota promenne 'a' -> 4
Hodnota promenne 'a' -> 42

Vysvětlení: Parametry funkce jsou vždy zkopírované argumenty, které při volání předáváme. Takže když jsme zavolali funkce1(a), tak se hodnota proměnné a zkopírovala a když jsme ji poté přepsali, tak jsme přepsali kopii a originál zůstal nezměněn. Při volání funkce2(&a) jsme předali adresu místo hodnoty, předaná adresa se zkopírovala do proměnné p. Hodnotu proměnné p jsme neměnili, ale použili jsme ji k přístupu do paměti na místo proměnné a. Při změně *p se tedy měnila hodnota proměnné a, a tak byly změny hodnoty permanentní.

V mnoha jazicích (i v c++) lze navíc předat argumenty funkce referencí. Samotné předání pak vypadá jako funkce3(a), ale hodnota a může být přepsána. Takto to například funguje pro neprimitovní hodnoty v pythonu či javě. Předání reference je pouze syntaktický fígl, který se nakonec stejně překládá na předání ukazatele.

Ukazatele na ukazatele

Zdrojový kód:

#include <iostream>
using namespace std;

int main() {
    int a = 4;
    int *b = &a;
    int **c = &b;
    int ***d = &c;
    int ****e = &d;
    int *****f = &e;
    int ******g = &f;
    int *******h = &g;
    int ********i = &h;
    int *********j = &i;
    int **********k = &j;

    cout << a << endl;
    cout << &a << " " << b << endl;

    **********k = 20;

    cout << a << endl;
    cout << &a << " " << b << endl;

    *********k = (int*)20;
    //**********k = 20; // segfault!

    cout << a << endl;
    cout << &a << " " << b << endl;

    return 0;
}

Výpis:

4
0x23cab4 0x23cab4
20
0x23cab4 0x23cab4
20
0x23cab4 0x14

Vysvětlení: Ukazatele můžou ukazovat na jiné ukazatele. Ukazatel typu int** ukazuje na proměnnou typu int* apod. V tomto příkladu jsme vytvořili řetězec ukazatelů hloubky 10. Abychom dereferencovali ukazatel k až k hodnotě a musíme použít 10 hvězdiček (každá nás posune v řetězci na další ukazatel – blíže k proměnné a). Když použijeme hvězdiček 9, tak nastavujeme hodnotu proměnné b. Pokud se potom budeme snažit o přístup k proměnné a, tak se k ní nemůžeme dostat, protože na ni ukazatel b neukazuje. Proměnná b ukazuje do paměti na adresu 0x14 (to je hexadecimálně 20). Tento úsek paměti nepatří našemu programu a pokud do něj budeme chtít zapsat, tak program spadne (segfault).

Ukazatel jsou taky jen 0ly a 1ky

Zdrojový kód: (při kompilaci dostanete warning)

#include <iostream>
using namespace std;

int main() {
    int a = 4;
    int b = 54;
    cout << "Hodnota promenne 'a, b' -> "<< a << ", " << b << endl;

    int c;
    c = b;
    b = a;
    a = c;

    cout << "Hodnota promenne 'a, b' -> "<< a << ", " << b << endl;

    int * p = &c;
    *p = b;
    b = a;
    a = *p;

    cout << "Hodnota promenne 'a, b' -> "<< a << ", " << b << endl;

    p = (int*)b;
    b = a;
    a = (long)p;

    cout << "Hodnota promenne 'a, b' -> "<< a << ", " << b << endl;

    return 0;
}

Výpis:

Hodnota promenne 'a, b' -> 4, 54
Hodnota promenne 'a, b' -> 54, 4
Hodnota promenne 'a, b' -> 4, 54
Hodnota promenne 'a, b' -> 54, 4

Vysvětlení: První sekce je normální prohození proměnných pomocí třetí proměnné. Druhá sekce nahradila proměnnou c ukazatelem na proměnnou c, všiměte si, že je to stejné jako v prvním případě, jen jsou tam navíc hvězdičky. Třetí sekce ukládá hodnotu přímo do p, Ano, tam má sice být adresa, ale můžete tam klidně uložit i hodnotu té proměnné! Ale kdybychom to dereferencovali *p, tak bychom se v paměti dostali na nesmyslná data. Velikost datového typu ukazatele int* závisí na architektuře (32 vs 64 bitové procesory), proto nejsou takového operace úplně bezpečné.

Další reference

http://www.cplusplus.com/doc/tutorial/pointers/ ↗