Zacznijmy od tego czym tak na prawdę jest wskaźnik, a więc jest to typ zmiennej przechowujący adres innej zmiennej konkretnego typu.. tak więc wskaźnik to nie cały obiekt, a jedynie czysty adres.
Kiedy ich używać?
Nie ma konkretnego zakazu nieużywania wskaźników, ale nie ma też obowiązku tworzenia ich w każdym możliwym miejscu.. mówiąc najprościej to programista decyduje, czy chce w kodzie gdzieś wykorzystać wskaźniki, czy też nie.. Jednak wybór przeważnie nie jest kwestią gustu. Aby dobrze wykorzystywać wskaźniki należy najpierw znać ich właściwości.. oto kilka z nich:
1. Wskaźnik jest "lekki":
Skoro wskaźnik przechowuje tylko adres, a nie cały obiekt, przenoszenie obiektów przy ich pomocy jest znacznie szybsze niż przy używaniu kopii (wyjątek stanowią najbardziej podstawowe typy tj. float, int, char, bool itd.)
2. Automatyczna "aktualizacja" obiektu:
Wyobraźmy sobie taką sytuację.. mamy kamerę ustawioną na konkretny cel, jeżeli chcemy, by kamera cały czas miała pozycję gracza mamy dwie możliwości:
a) Cały czas przy aktualizacji pętli głównej gry przesyłać do kamery pozycję gracza (aktualizować pozycję ręcznie).
b) Przechowywać konkretny wskaźnik z pozycją gracza w obiekcie [kamera].. przez co kamera zawsze będzie wskazywać na gracza, chyba że zmienimy adres wskaźnika (automatyczna aktualizacja pozycji).
3. Przechowywanie oryginalnej zmiennej:
Skoro wskaźnik odwołuje się do obiektu poprzez jego adres oznacza to, że modyfikowanie obiektu przy pomocy wskaźnika wpłynie bezpośrednio na ten obiekt.. wskaźnik nie przechowuje kopii, a oryginał.
Prosty przykład:
int a=10;
int b=a; //przesyłanie wartości przez kopię
int *ptr_a=&a;
*ptr_a=30;
b=20;
Modyfikowanie zmiennej b, w żaden sposób nie wpłynie na wartość zmiennej a, możliwa jest natomiast zmiana wartości zmiennej a przy pomocy wskaźnika.. Na końcu programu okaże się, że a=30 i b=20.
4. Szybkie przesyłanie obiektów:
Jeżeli miałeś już do czynienia w funkcjami to powinieneś wiedzieć, że zapis:
int funkcja(int a, int b)
{
return a+b;
}
Oznacza, że do funkcji (jej argumenty) zostaną przesłane kopie oryginalnych zmiennych. Co prawda w tym wypadku nie jest to kosztowna operacja, ale co gdyby zamiast typu int, mielibyśmy do czynienia z rozbudowanym obiektem przechowującym ponad 40 różnych zmiennych? Wtedy stworzenie kopi zajęło by 40*2 razy dłużej niż przypisanie jakiejś wartości do typu int, ponieważ wszystkie wartości ze wszystkich zmiennych musiałby być przekopiowane. Aby rozwiązać ten problem możemy posłużyć się albo referencją:
obiekt funkcja(obiekt &a, obiekt &b)
{
return a+b;
}
która sprawi, że argumenty w tej funkcji nie będą już kopią, a oryginałem (nie myl ze wskaźnikami), albo możemy przesłać argumenty w postaci wskaźników:
obiekt funkcja(obiekt *a, obiekt *b)
{
return *a+*b; //oczywiście, musimy posłużyć się operatorem '*' aby zsumować obiekty, na które pokazują wskaźniki, a nie same adresy obiektów.
}
Wydaje się, że przesyłanie przez referencję jest ładniejsze w kodzie.. jest jednak kluczowa różnica.. do funkcji z referencjami nie możemy przesłać pustego adresu, natomiast przy wskaźnikach jest to już możliwe..
Prosty przykład w postaci całego kodu: http://cpp.sh/97m2f
5. Polimorfizm / dziedziczenie:
Zgaduję, że skoro jesteś dopiero na etapie wskaźników nie wiesz czym dokładnie jest polimorfizm.. dlatego nie będę się rozpisywał.. Postaram się wyjaśnić tylko niezbędne minimum na podstawie przykładu.. Powiedzmy, że mamy w grze kilka, rodzaju przeciwników.. np. Wilk, Niedźwiedź, Golem i Smok.. Teraz rodzi się pytanie.. jak przechować te wszystkie (różnego typu) obiekty w jednej tablicy.. (przypominam, że w jednej tablicy możemy przechowywać tylko jeden typ obiektu) w C++ jest jednak pewien myk, który umożliwi na przechowywanie wszystkich przeciwników, każdego typu.. wystarczy ustawić, że Wilk, Niedźwiedź, Golem i Smok są również Przeciwnikiem (Dokładnie mówiąc to Wilk dziedziczyłby z klasy Przeciwnik.. Niedźwiedź, Golem i Smok również), W takim przypadku mając tablicę ze wskaźnikami typu [Przeciwnik], moglibyśmy przechowywać w niej wszystkie obiekty typu [Przeciwnik] (to, że były by one też wilkiem, czy smokiem.. w tym wypadku nas nie interesuje, ważne że byłyby one również typu [Przeciwnik], czyli takiego jaki przechowujemy w tablicy).
Jak poruszać się po tablicach?
Najpierw należy wiedzieć co tak na prawdę oznacza używanie operatora '[...]'.
Prosty przykład (lubię przykłady ^^):
int Tab[10];
Tab[4]=5;
Zapis Tab[4]=5; jest tak na prawdę równoważny zapisowi:
*(Tab+4)=5;
Co tu się dokładnie dzieje?
Nazwa tablicy oczywiście jest jednocześnie adresem jej pierwszego elementu.. oznacza to, że Tab+4 to tak na prawdę adres jej piątego elementu:
Tab -> Tab+1 ->Tab+2 -> Tab+3 -> Tab+4
[0] [1] [2] [3] [4]
Używając gwiazdki informujemy kompilator, że chcemy odwołać się do wartości na którą wskazuje (w tym przypadku) adres Tab+4.
Ale działa to też w drugą stronę.. i to jest właśnie to, o co pytasz, ale to już najlepiej wyjaśnić na przykładzie prostego programu: http://cpp.sh/44h7