O Wskaźnikach.
Transkrypt
O Wskaźnikach.
Czym są wskaźniki? Wskaźnik (ang. pointer) – typ zmiennej odpowiedzialnej za przechowywanie adresu do innej zmiennej (innego miejsca w pamięci) w obrębie naszej aplikacji. Wskaźnik może wskazywać na jakąś zmienną, strukturę, tablicę a nawet funkcję. Oto podstawowe operatory niezbędne do operowania wskaźnikami: * – operator wyłuskania wartości zmiennej, na którą wskazuje wskaźnik (wyciąga wartość ze wskaźnika) & – operator pobrania adresu danej zmiennej, tablicy, struktury itp (pobiera adres zmiennej) Po co są wskaźniki? W języku C++ możemy do funkcji przekazać dowolną ilość parametrów. Modyfikując parametry w obrębie ciała funkcji oryginalne zmienne nie zmienią się. Przekazując zmienne do funkcji poprzez wartość (czyli w sposób standardowy), tworzymy wewnątrz funkcji ich kopię! Co z tego faktu wynika? Każda funkcja w C++ może zmodyfikować maksymalnie jedną zmienną, za pomocą wartości zwracanej return. Co zrobić, aby jedną funkcją zmodyfikować 3 zmienne na raz? Nie można użyć rozkazu return 3 razy, ponieważ każda funkcja zwraca tylko jedną wartość. W tej sytuacji trzeba skorzystać ze wskaźników. Jeżeli przekażemy do funkcji jako jej argument wskaźnik, wtedy operacje na wskaźniku zmieniają zmienną oryginalną z poza ciała funkcji – nie operujemy na kopii zmiennej. Dzięki temu, nawet jeżeli funkcja jest typu void i nic nie zwraca, możemy modyfikować wiele zmiennych z poza ciała funkcji: Jak używać wskaźników Zmienna wskaźnikowa (czyli wskaźnik) poprzedzona jest gwiazdką (*) i przechowuje adres pamięci (a nie wartość) zmiennej , na którą wskazuje. Deklarując wskaźnik postępujemy tak jak ze zwykłymi zmiennymi, jednak nazwę wskaźnika poprzedzamy gwiazdką. Uwaga! Gwiazdka przed nazwą wskaźnika nie ma związku z operatorem wyłuskania! int telefon; //zmienna liczbowa int *wsk; //zmienna wskaźnikowa typu liczbowego Tak wygląda tworzenie wskaźnika. Utworzone przez nas zmienne są puste. Zmienna telefon nie zawiera żadnej wartości a wskaźnik wsk nie wskazuje żadnej wartości. Bardzo ważne jest aby nie korzystać ze wskaźnika który nie wskazuje na żadną zmienną! Prowadzi to zawsze do błędów i niesie ze sobą nieprzewidziane konsekwencje! Rozbudujmy powyższy przykład: int telefon = 12345; //zmienna liczbowa int *wsk = &telefon; //wskaźnik wsk zawiera adres zmiennej telefon Za pomocą operatora pobrania adresu (&) pobraliśmy adres zmiennej telefon. Adres zmiennej został przypisany wskaźnikowi wsk. Pamiętaj że gwiazdka przed nazwą wskaźnika to nie operator wyłuskania! Chcąc wyświetlić wartość wskaźnika posłużymy się operatorem wyłuskania czyli gwiazdką (*). #include <iostream> using namespace std; int main() { int telefon = 12345; //zmienna liczbowa int *wsk = &telefon; //wskaźnik wsk zawiera adres zmiennej telefon cout << *wsk << endl; return 0; } Powyższy przykład wyświetli na ekranie wartość zmiennej telefon. Przed wyświetleniem wskaźnika został użyty operator wyłuskania. Pobiera on wartość zmiennej spod adresu ze zmiennej wskaźnikowej. Bez użycia operatora wyłuskania, została by wyświetlona wartość zmiennej wskaźnikowej wsk czyli adres zmiennej telefon: int telefon = 12345; //zmienna liczbowa int *wsk = &telefon; //przypisanie wskaźnikowi adresu zmiennej telefon cout << *wsk << endl; //wyświetlenie wyłuskanej wartości wskaźnika (12345) cout << wsk << endl; //wyświetlenie adresu zmiennej telefon cout << &wsk << endl; //wyświetlenie adresu wskaźnika cout << &telefon << endl; //wyświetlenie adresu zmiennej telefon Rzeczą normalną są wskaźniki do wskaźników. Przy ich wyświetlaniu ważna jest ilość operatorów wyłuskania. Jeżeli do wskaźnika drugiego stopnia użyjemy jednego operatora wyłuskania wtedy otrzymamy adres wskaźnika a nie wartość zmiennej na którą wskazują. int telefon = 12345; //zmienna liczbowa int *wsk = &telefon; //wskaźnik wsk zawiera adres zmiennej telefon int **wsk2 = &wsk; //wskaźnik wsk2 zawiera adres wskaźnika wsk cout << **wsk2 << endl; int telefon = 12345; //zmienna liczbowa int *wsk = &telefon; //wskaźnik wsk zawiera adres zmiennej telefon int **wsk2 = &wsk; //wskaźnik wsk2 zawiera adres wskaźnika wsk cout << **wsk2 << endl; Wartość wskaźnika (zmiennej na którą wskazuje) możemy modyfikować bez użycia nazwy zmiennej: int telefon = 12345; //zmienna liczbowa int *wsk = &telefon; //wskaźnik wsk zawiera adres zmiennej telefon *wsk = 999; cout << *wsk << endl; Totalnie błędna byłaby sytuacja, kiedy chcemy nadać zmiennej wskaźnikowej określoną wartość, bez wcześniejszego przypisania wskaźnika do określonej zmiennej: int *wsk; *wsk = 999; //źle!! Wskaźniki i funkcje W języku C++ przekazujemy argumenty do funkcji poprzez tzw. przekazywanie przez wartość. W języku C oraz C++ możemy przekazywać wartości do funkcji poprzez przekazywanie przez wskaźnik. Wskaźnik będzie wtedy argumentem funkcji. #include <iostream> using namespace std; // funkcja przyjmuje jako argument wskaźnik void zwieksz_liczbe (int *liczba); int main() { int numerek = 5; int *wsk = &numerek; zwieksz_liczbe(wsk); //przekazujemy wskaźnik (bez operatorów) cout << numerek << endl; zwieksz_liczbe(&numerek); //przekazujemy bezpośrednio adres zmiennej (operator &) cout << numerek << endl; return 0; } void zwieksz_liczbe (int *liczba) { *liczba+= 5; } Należy zwrócić uwagę, że do funkcji której argumentem jest wskaźnik, przekazujemy adres zmiennej za pomocą operatora pobrania adresu (&) a nie samą wartość. Wskaźniki i tablice Tablice są ściśle związane ze wskaźnikami. Nazwa tablicy to wskaźnik na jej pierwszy element. Oznacza to że możemy wyświetlić pierwszy element tablicy umieszczając operator wyłuskania przed jej nazwą: int tablica[5] = {1, 2, 3, 4, 5}; cout << *tablica << endl; Możemy także tworzyć wskaźniki do określonych elementów tablicy: int tablica[5] = {1, 2, 3, 4, 5}; int *wsk = &tablica[3]; cout << *wsk << endl; int tablica[5] = {1, 2, 3, 4, 5}; int *wsk = &tablica[3]; cout << *wsk << endl; Często spotykane są także tablice wskaźników. Ich tworzenie jest proste i analogiczne do innych typów zmiennych: #include <iostream> using namespace std; int main() { int liczba1 = 1, liczba2 = 2, liczba3 = 3; int *wsk[3]; wsk[0] = &liczba1; wsk[1] = &liczba2; wsk[2] = &liczba3; cout << *wsk[1] << endl; return 0; } Dynamiczna alokacja pamięci Często podczas działania programu pojawia się problem dynamicznego zarządzania pamięcią. Problem fajnie można zaprezentować na przykładzie tablicy o różnej ilości elementów. Z pomocą przychodzą wskaźniki oraz operatory new i delete. Najpierw tworzymy wskaźnik określonego typu a następnie zmienną o dowolnej wielkości. Potem przydzielamy wskaźnikowi adres nowej zmiennej utworzonej na stercie: #include <iostream> using namespace std; int main() { int *zmienna; zmienna = new int[3]; zmienna[0] = 111; cout << *zmienna << endl; delete zmienna; return 0; } Dynamiczne tworzenie zmiennych daje nam duże możliwości. Ważne jest aby usuwać je kiedy są niepotrzebne. W przeciwnym wypadku istnieje szansa wystąpienia błędu memory leak.