Koncepcje programowania/Wskaźniki

Z Wikibooks, biblioteki wolnych podręczników.
Przejdź do nawigacji Przejdź do wyszukiwania

W językach programowania pozwalających na bezpośredni dostęp do pamięci (jak np. asembler, C, C++, Cyclone) pamięć jest reprezentowana jako jednowymiarowa tablica bajtów – wszystkie zmienne (statyczne i dynamiczne) są umieszczane w tej tablicy. Wskaźnik jest indeksem do tej tablicy – najczęściej ów indeks jest jednocześnie logicznym adresem.

Czym zatem jest wskaźnik? - jest to zmienna, która przechowuje w sobie adres, innej zmiennej. Na przykład gdzieś w kodzie zdefiniowali sobie taką zmienną:

int liczba = 144;

Gdzieś w pamięci została utworzona zmienna o tej nazwie i przechowuje wartość 144. Oczywiście komputer nie może oznaczać zmiennych w pamięci według nazw, wymyślonych przez programistów, bo nazwy zmiennych mogłyby się powtarzać w różnych programach, nie wspominając już o tym że komputer to maszyna zero-jedynkowa. Dlatego, każda komórka pamięci ma swój numer - bo jest jednoznaczny i unikalny, pozwala zachować porządek przy adresowaniu pamięci. Przyjmijmy że nasza komórka w pamięci ma numer 16250 - na marginesie, numer ma każdy bajt pamięci a pojedyncza zmienna typu int zajmuje 4 bajty, dlatego tak naprawdę nie jest to konkretny adres tej jednej zmiennej, tylko zakres 1650-1653. Ponieważ adresów bajtów jest tak wiele, bardzo często spotkasz się z sytuacją, gdzie one będą przechowywane w postaci szesnastkowej - czyli w tym przypadku 3F7A.

Istnieje taka zmienna w pamięci pod tym adresem. I co robi wskaźnik? Podajemy najpierw typ zmiennej, na jaką ma wskazywać wskaźnik czyli int. Potem jest gwiazdka (*) która bardzo często w wielu językach programowania jest znakiem zarezerwowanym dla wskaźników, tak jak nawiasy okrągłe dla funkcji, lub kwadratowe dla tablic (pomijając używanie jej do mnożenia liczb). Następnie podajemy nazwę wskaźnika w kodzie np. p i dalej chcemy zapisać adres zmiennej liczba. Wyglądałoby to tak:

int * p;
p = &liczba;

operator ampersand, jest operatorem uzyskiwania adresów w pamięci tego co znajduje się po jego prawej stronie.

Innymi słowy: Tworzymy wskaźnik * (gwiazdka) o nazwie x, wskazujący na int i do niego zapisujemy adres zmiennej liczba.

Informacja wskaźnik to zmienna mający swój ades, a w środku jest adres zmiennej liczba, czyli mająćy w sobie adres innej zmiennej. Wskaźnik wskazuje, gdzie w pamięci RAM znajduje się jakaś inna zmienna.

Teraz już być może rozumiesz, dlaczego niektóre języki programowania zawierają silne, statyczne typowanie. Być może pamiętasz z poprzedniego rozdziału, że kolejne zmienne w pamięci są zarezerwowane jedna obok drugiej. A skoro są to liczby całkowite typu int, zajmujące 4 bajty, to kolejne komórki będą miały adresy zwiększające się co 4. Dokładnie tak to zostanie zapisane w pamięci. Komputer zawsze tak to ułoży, a jeśli będzie trzeba, to przestawi inne zmienne w pamięci tak, by były ułożone w taki a nie inny sposób. Jeśli komputer wie, że wskaźnik wskazuje na int, to będzie skakał co 4 bajty w pamięci. Gdyby wskaźnik byłby na przykład typu double, to musiałby już skakać co 8 bajtów w kolejnych adresach, bo pojedyncza zmienna typu double, zajmuje nie 4 a o 8 bajtów. Wskaźnik jest ustawiony na pierwszej szufladce indeksu. Gdybym chciał ustawić wskaźnik o indeksie 6, to zapisałbym

int * p;
p += 5;

Jak dodam 5 do wskaźnika, to komputer do adresu w nim zapisanego, doda 5 * 4 + 20 bajtów = 3F92. Wyjdzie adres 6 numeru indeksu w tablicy. Podobnie gdy odejmę od wskaźnika 2

p -= 2;

to znajdę w nim adres mniejszy o 8 bitów czyli 4 szufladkę. Dlatego właśnie zmienne sa rozstawione kolejno w pamięci, po to by można było przestawiać wskaźniki na kolejne adresy. Gdyby język nie byłby silnie typowany, to działanie na wskaźnikach byłyby niemożliwe, bo jakbyś chciał się odwołać do wskaźnika w momencie gdy każda zmienna zawiera inne wartości? No nie dałoby się tego przewidzieć.

Dlaczego używa się wskaźników?

  • Dynamiczne rezerwowanie i alokowanie obszarów pamięci
  • Zwiększenie szybkości zapisu/odczytu komórek w tablicy
  • Dawanie funkcjom do pracy oryginalnych zmiennych z programu wywołującego
  • Możliwość współpracy z urządzeniami zewnętrznymi np. miernikiem
  • Polimorfizm i dziedziczenie - ponieważ są to pojęcia z zakresu programowania obiektowego, na razie pominiemy, na tym etapie jest jeszcze o nich za wcześnie.

Być może pamiętasz z poprzedniego rozdziału jak pracowaliśmy na tablicach. Pewnym jej ograniczeniem jest to, że niezależnie od tego ile wartości będzie potrzebował użytkownik, my i tak zawsze w programie rezerwowali sztywną, z góry określoną ilość elementów w tablicy. Jak się nad tym zastanowić, jest to straszne marnotrawstwo miejsca. Istnieje więc pewien mechanizm, który sprawia że nasze tablice będą jeszcze bardziej elastyczne, będziemy mogli zarezerwować dzięki nim tyle zmiennych, ile dokładnie chcemy w danym momencie, nie będziemy musieli za każdym razem wymyślać ile ich dokładnie potrzebujemy. Dodatkowo, po dokonaniu obliczeń przez nasz program, będziemy mogli z powrotem zwolnić całe zużyte miejsce, po prostu kasując całą tablicę. Przykład, tworzymy grę i gracz właśnie ukończył wszystkie cele misji na danej mapie. Oczywiste jest, że najlepiej byłoby przed załadowaniem kolejnej mapy, zwolnić całą użytą pamięć przed rozpoczęciem bieżącej misji. Po co trzymać dane z starej mapy skoro gracz i tak zostanie przeniesiony do nowej?. Taki proces nazywamy dynamicznym alokowaniem pamięci, tak jak Python pozwala dynamicznie zmieniać typ zmiennej, tak tutaj możemy dynamicznie bawić się tablicami.

Przykład w języku c++:

#include <iostream>
using namespace std;

int ile;

int main()
{
	cout << "Ile liczb w tablicy" << endl;
	cin>>ile; 

	int *tablica;
	tablica = new int [ile];
	return 0;
}