Koncepcje programowania/Wskaźniki

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 rodzajem zmiennej, która przechowuje adres pamięci innej zmiennej. Wskaźniki służą do bezpośredniego dostępu i manipulowania danymi przechowywanymi w innych zmiennych.

Na przykład wyobraź sobie, że piszesz program, który musi przechowywać w pamięci dużą ilość danych. Możesz użyć wskaźnika do przechowywania adresu pamięci tych danych, a następnie możesz użyć wskaźnika do bezpośredniego dostępu do danych w pamięci i manipulowania nimi. Pozwoliłoby to na wydajniejsze i efektywniejsze wykorzystanie danych, ponieważ nie trzeba by kopiować danych z jednej części pamięci do drugiej.

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 są 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;
}

Python

edytuj

wskaźniki nie są powszechnie używane w Pythonie. W większości przypadków lepiej jest używać wbudowanych typów danych, takich jak listy i słowniki, zamiast bezpośrednio pracować ze wskaźnikami. Jeśli jednak naprawdę chcesz używać wskaźników w Pythonie, możesz to zrobić za pomocą modułu ctypes. Ten moduł umożliwia bezpośredni dostęp do wartości w pamięci, przy użyciu ich adresów pamięci.

Oto przykład wykorzystania modułu ctypes do tworzenia i używania wskaźnika w Pythonie:

import ctypes

# Zdefiniuj zmienną całkowitą
x = 10

# Pobierz adres pamięci zmiennej
x_address = id(x)

# Użyj modułu ctypes, aby utworzyć wskaźnik do zmiennej
x_pointer = ctypes.cast(x_address, ctypes.POINTER(ctypes.c_int))

# Użyj wskaźnika, aby uzyskać dostęp i zmodyfikować wartość zmiennej
x_pointer.contents.value = 20

print(x)  # Output: 20

JavaScript

edytuj

Aby używać wskaźników w JavaScript, musisz najpierw utworzyć zmienną i przypisać jej wartość. Ta wartość może być pierwotnym typem danych (takim jak liczba lub ciąg) lub może być obiektem.

Gdy masz już zmienną, możesz utworzyć wskaźnik do tej zmiennej za pomocą operatora &. Na przykład:

// Create a variable and assign it a value
let x = 10;

// Create a pointer to the variable x
let xPointer = &x;

Aby uzyskać dostęp do wartości zmiennej za pomocą wskaźnika, możesz użyć operatora *. Na przykład:

// Access the value of the variable x through the pointer
let y = *xPointer;

// The value of y is now 10

Możesz także użyć wskaźnika, aby zmodyfikować wartość oryginalnej zmiennej. Na przykład:

// Modify the value of the variable x through the pointer
*xPointer = 20;

// The value of x is now 20

Należy pamiętać, że wskaźniki w JavaScript nie są funkcją języka i nie są powszechnie używane w kodzie JavaScript. Wymieniono je tutaj dla kompletności i zrozumienia ogólnego działania wskaźników.

Emacs Lisp

edytuj

W Emacs Lisp wskaźnik jest odniesieniem do lokalizacji w pamięci. Innymi słowy, jest to sposób na dostęp do określonego fragmentu danych przechowywanych w pamięci. Wskaźniki są powszechnie używane w Emacs Lisp podczas pracy z obiektami, ponieważ umożliwiają bezpośredni dostęp do właściwości i metod obiektu, bez konieczności używania notacji kropkowej.

Aby utworzyć wskaźnik w Emacs Lisp, użyj funkcji make-pointer. Ta funkcja przyjmuje pojedynczy argument, który jest wartością, dla której chcesz utworzyć wskaźnik. Na przykład:

;; Create a pointer to the number 10
(setq num-pointer (make-pointer 10))

Aby uzyskać dostęp do wartości wskaźnika, użyj funkcji wartość wskaźnika. Ta funkcja przyjmuje pojedynczy argument, który jest wskaźnikiem, do którego chcesz uzyskać dostęp. Na przykład:

;; Access the value of the pointer
(pointer-value num-pointer)
;; Returns: 10

Aby zmodyfikować wartość wskaźnika, użyj funkcji set-pointer-value. Ta funkcja przyjmuje dwa argumenty: wskaźnik, który chcesz zmodyfikować, oraz nową wartość, którą chcesz przypisać do wskaźnika. Na przykład:

;; Modify the value of the pointer
(set-pointer-value num-pointer 20)

Należy pamiętać, że wskaźniki w Emacs Lisp nie są funkcją języka i nie są powszechnie używane w kodzie Emacs Lisp. Wymieniono je tutaj dla kompletności i zrozumienia ogólnego działania wskaźników.

Zadania

edytuj
  1. Napisz pogram, który deklaruje zmienną wskaźnika, inicjuje ją, aby wskazać zmienną liczbową, a następnie używa wskaźnika do przypisania nowej wartości do zmiennej liczby całkowitych.
  2. Napisz pogram, który deklaruje tablicę liczb całkowitych, a następnie używa wskaźnika do dostępu i modyfikacji wartości w tablicy.
  3. Napisz pogram, który deklaruje dwie zmienne całkowite, a następnie wykorzystuje wskaźnik do wymiany wartości dwóch zmiennych.
  4. Napisz pogram, który deklaruje zmienną ciągów, a następnie używa wskaźnika do wyświetlania każdego znaku w ciągu.
  5. Napisz pogram, który deklaruje wskaźnik do struktury, inicjuje strukturę z niektórymi wartościami, a następnie wykorzystuje wskaźnik do uzyskiwania dostępu i modyfikacji wartości w strukturze.
  6. Napisz pogram, który deklaruje wskaźnik do funkcji, a następnie używa wskaźnika do wywołania funkcji i przekazywania niektórych argumentów.
  7. Program napisz, który deklaruje wskaźnik do tablicy liczb całkowitych, a następnie używa wskaźnika do dostępu i modyfikacji wartości w tablicy.
  8. Napisz pogram, który deklaruje wskaźnik do wskaźnika, a następnie używa wskaźnika do dostępu i modyfikowania wartości zmiennej.
  9. Napisz pogram, który deklaruje zmienną liczbową, a następnie używa wskaźnika do alokacji pamięci dla tablicy liczb całkowitych i przypisania adresu tablicy do wskaźnika.
  10. Napisz pogram, który deklaruje wskaźnik do postaci, a następnie używa wskaźnika do odczytu i zapisu danych do pliku.