- Ukazatele a volná paměť
- Ukazatele a filosofie
- Deklarování a inicializace ukazatelů
- Ukazatele a čísla
- Úkol
Konečně se podíváme na to, na co všichni čekáte a budete nenávidět – ukazatele, nebo-li pointery. Nechci tuto lekci dělat příliš dlouhou proto, aby byla snáze pochopitelná. Snad se to podaří.
Když počítačový program ukládá data, musí sledovat tři základní vlastnosti:
- kde je informace uložena
- jakou má hodnotu
- jaký druh informace je uložen
Doteď jste to používali: definovali jste jednoduchou proměnnou.
int a;
Poskytli jste typ (int) a symbolické jméno (a), které odkazuje na alokovanou paměť programem a toto místo sleduje. Jednoduše, proměnná musí být někde uložena, někde v paměti. Pojďme se ale nyní na to podívat z druhé stránky, která hraje důležitou roli v rozvoji tříd v C++. Jsou to ukazatele.
Ukazatel je proměnná, která,
spíše než hodnotu, ukládá její adresu. Než však pojednáme o ukazatelích, pojďme
se podívat, jak zjistíme adresu normálních proměnných. Je to velmi jednoduché. Na proměnnou pouze aplikujeme adresový operátor &, abyste zjistili její umístnění (adresu). Pokud máme proměnnou vek, tak
její adresa je &vek
.
int a=9;
cout << a << endl << &a;
Výsledek:
9
0x22ff7c
Adresa se samozřejmě bude lišit. Ukažme si to na dalším příkladu.
#include <iostream>
using namespace std;
int main()
{
int a = 6;
double cislo = 4.5;
cout << "hodnota promenne a = " << a;
cout << " a adresa promenne a = " << &a << " ";
cout << "hodnota promenne cislo = " << cislo;
cout << " a adresa promenne cislo = " << &cislo << " ";
cin.get();
return 0;
}
A výsledek:
hodnota promenne a = 6 a adresa promenne a = 0x22ff7c
hodnota promenne cislo = 4.5 a adresa promenne cislo = 0x22ff70
Když cout zobrazuje adresy, používá hexadecimálního označení – je to typické pro označení adresy. Podívejme se blíže:
→ 0x22ff7c − 0x22ff70 = 8
Je to logické, protože cislo je typu double a ten zaujímá 8 bajtů.
Na PC se reprezentace adres odrážejí metodou, která popisuje adresy pomocí hodnoty segmentu a ofsetu. Hodnota segmentu – 22 – u proměnné a identifikuje blok paměti, který se používá pro uložení dat. Ofsety – ff7c – reprezentují pozici paměti relativně k začátku segmentu.
9.11 Ukazatele a filozofie C++
Objektově orientované programování se liší od tradičního procedurálního v důrazu OOP na rozhodování během běhu programu namísto v době kompilace. Běh programu znamená, když se program provádí, a doba kompilace znamená, když kompilátor sestavuje program dohromady. Rozhodnutí v době běhu poskytuje pružnost v přizpůsobení se aktuálním okolnostem. Uveďme si příklad: pole – abychom ho mohli deklarovat, musíme zadat určitou velikost pole, tedy velikost pole nastavujeme před kompilací, jeho velikost se nastaví v době kompilace. Možná si myslíte, že pole o 20 prvcích je dostatečná ve většině případů, ale jednou budete potřebovat výjimku, a tak nastavíte velikost na 200 prvků. To má ale za následek plýtvání pamětí po většinu času. OOP se pokouší vytvořit program mnohem pružněji tím, že ponechává taková rozhodnutí až do doby běhu. Tímto způsobem, poté co program běží, můžeme říci, že potřebujeme v jednom okamžiku pouze 20 prvků nebo v jiném 205 prvků. Zkrátka velikost pole je rozhodnutím v době běhu.
Ale vraťme se k ukazatelům, k jakožto zvláštnímu typu proměnné. Ukazatel tedy obsahuje adresu hodnoty a jméno ukazatele reprezentuje umístnění. Použitím operátoru *, který se nazývá nepřímou hodnotou nebo dereferenčním operátorem, poskytuje hodnotu v dané lokaci. (Nehledejte v tom vědu, jestliže provincie je ukazatel, pak *provincie je hodnota uložená na této adrese – ekvivalent k obyčejné proměnné.) Možná lépe pochopíte na příkladu:
#include <iostream>
using namespace std;
int main()
{
int provincie = 6; // deklaruje proměnnou
int * farma; // deklaruje ukazatel na int
farma = &provincie; // přiřazuje adresu int ukazateli
// vyjadřuje hodnoty dvěma způsoby
cout << "Hodnoty: provincie = " << provincie;
cout << ", *farma = " << *farma << " ";
// vyjadřuje adresy dvěma způsoby
cout << "Adresy: &provincie = " << &provincie;
cout << ", farma = " << farma << " ";
// používá ukazatel na změnu hodnoty
*farma = *farma + 1;
cout << "Promenna provincie nyni = " << provincie << " ";
cin.get();
return 0;
}
Výsledky:
Hodnoty: provincie = 6, *farma = 6
Adresy: &provincie = 0x22ff7c, farma = 0x22ff7c
Promenna provincie nyni = 7
Jak můžete vidět, proměnná typu int provincie a ukazatelová proměnná farma jsou pouze dvě strany jedné mince. Detailně:
Proměnná provincie vypisuje primárně hodnotu, na získání její adresy použijeme operátor & → &provincie.
Proměnná farma vypisuje primárně adresu, na získání její hodnoty použijeme operátor * → *farma.
Poznámka: Je jedno, zda bude vypadat zápis int* farma
, int * farma
nebo int *farma
.
Použití mezer je volitelné, programátoři v C používali 3. způsob, v C++ spíše 1. způsob.
Nechápete stále, k čemu nám ukazatele jsou?
Neodborně vysvětlím. Berte ukazatele jako další typ proměnných (jako jsou int, double, float…). Pokud napíšeme int *farma
, vytvoří se nám proměnná, stejně jako když deklarujeme int farma
. Ovšem ukazatele mají tu zvláštnost, že jako hodnoty nejsou čísla a znaky, nýbrž adresy. Tudíž díky ukazateli můžeme přistupovat k samotné hodnotě proměnné jak klasicky, tak přes její adresu. Když to rozšířím, int *farma
má svou vlastní adresu, ale jako hodnotu si bere adresu cizí proměnné. Takže při změně hodnoty ukazatele int *farma
se změní i hodnota
proměnné, na kterou ukazuje. Musíte si to vyzkoušet. Uvedu zde takový malý
program, který slouží JEN k lepšímu pochopení.
#include <iostream>
using namespace std;
int main()
{
int luke=9;
int *pavel=&luke;
cout << luke <<endl << &luke <<endl << *pavel <<endl << pavel <<endl << &pavel <<endl <<endl ;
*pavel = 10;
cout << luke <<endl << &luke <<endl << *pavel <<endl << pavel <<endl << &pavel;
cin.get();
return 0;
}
9.2 Deklarování a inicializace ukazatelů
Deklarování ukazatelů
Počítač musí sledovat typ proměnné, na kterou se ukazatel odvolává. Proč? Adresa char nebo double vypadají stejně, ale každý používá jiný počet bajtů, proto musí deklarace ukazatele specifikovat, na jaký typ dat ukazatel ukazuje.
int *farma;
To stanovuje, že *farma
je typu typu int. Protože se operátor * používá ve vztahu k ukazateli, sama
proměnná farma je ukazatelem. Říkáme, že farma ukazuje na int, respektive farma je ukazatel (adresa) a *farma je int a nikoli ukazatel.
Ale pozor!
Buďte si vědomi věci, že deklarace
int *p1, p2;
vytváří jeden ukazatel (p1) a jednu obyčejnou proměnnou (p2) typu int. Pro každé jméno ukazatelové proměnné potřebujete *.
Na deklaraci ukazatelů ostatních typů použijeme stejnou syntaxi:
double *meridlo;
char *str;
Na ty, kteří nepozorně používají ukazatele, čeká nebezpečí. Jedním zvláště důležitým bodem při vytváření ukazatele je, že počítač alokuje paměť na úschovy adresy, ale nealokuje paměť na úschovu dat, na která adresa ukazuje. Vytvoření prostoru pro data je oddělený krok. Opomenutím tohoto kroku, jak je ukázáno v následující ukázce, je krokem k pohromě:
long *melnik; // vytváří ukazatel na long
*melnik = 123; //umístnění hodnoty do země nikoho
Jistě, melnik je ukazatel. Ale kam ukazuje? Programový kód selhal při přiřazení adresy do melnik. Nemůžeme ani říct, kam se uložila hodnota 123, protože melnik nebyla inicializována. Cokoli, co je hodnotou, program interpretuje jako adresu, na kterou se uloží 123. Když se přihodí, že melnik má hodnotu 1 200, potom se program pokusí umístit data na adresu 1 200, dokonce i když se stane, že je ta adresa uprostřed vašeho programového kódu. Tento druh chyby je jedním z nejzákeřnějších a těžce vystopovatelných opomenutí.
Vždy inicializujte ukazatel jednoznačnou a vhodnou adresou, než na něj použijete deferenční operátor (*).
9.3 Ukazatele a čísla
Ukazatele nejsou celočíselné typy, třebaže počítače spravují adresy jako celá čísla. Pojmově jsou ukazatele jinými typy než celá čísla. Celá čísla jsou čísla, která můžeme dělit, sčítat, odčítat apod., ale ukazatel popisuje lokaci a například nemá smysl násobit dvě lokace. Proto nemůžeme ukazateli jednoduše přiřadit celé číslo:
int *pi;
pi = 0xB8000000; // nesoulad typů
Zde je levá strana ukazatel na int, takže jí můžete přiřadit adresu, ale pravá strana je pouze celým číslem. Možná víte, že 0xB8000000 je složenou adresou segmentu a ofsetu video paměti vašeho systému, ale nic v systému programu neříká, že toto číslo je adresou. C vám taková přiřazení dovoluje, avšak C++ podporuje mnohem přísněji soulad typů a kompilátor vám dá chybové hlášení – nesoulad typů. Chcete-li tedy použít numerickou hodnotu jako adresu, musíte vhodně přetypovat:
int *pi;
pi = (int *) 0xB8000000; // typy se nyní shodují
Nyní obě strany příkazu reprezentují adresy, takže je přiřazení platné.
Ukazatele mají některé další zajímavé vlastnosti, o kterých dále pojednáme, jakmile budou relevantní.
Příště se podíváme na to, jak se mohou ukazatele používat na alokaci paměťového prostoru v době běhu programu.
9.4 Úkol
Vytvořte 5 proměnných a zrovna inicializujte na samé nuly. Necháte vypsat. Poté ke každé z nich vytvoříte jednoho ukazatele a pomocí nich změníte postupně hodnoty původních proměnných. Necháte vypsat. Nakonec necháte, aby uživatel zadal 1 číslo a o toto číslo se zvětší všechny hodnoty, avšak přes ukazatele! A úplně nakonec vypíšete postupně u každého čísla jeho adresu.
Př.:
1. cislo = 0
2. cislo = 0
3. cislo = 0
4. cislo = 0
5. cislo = 0
1. cislo = 5
2. cislo = 6
3. cislo = 8
4. cislo = 1
5. cislo = 5
zadej cislo : 5
1. cislo = 10
2. cislo = 11
3. cislo = 13
4. cislo = 6
5. cislo = 10