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