| tags:[ programming tutorial ]
Jak na pointery?!
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é.