Zmienne w komputerze są przechowywane w pamięci. To wie każdy programista, a dobry programista potrafi kontrolować zachowanie komputera w przydzielaniu i obsłudze pamięci dla zmiennych. W tym celu pomocne są wskaźniki.
Co to jest wskaźnik?
Wskaźnik (ang. pointer)Everything you need to know about pointers in C Version 1.3. Copyright 2005–2010 Peter Hosey. to specjalny rodzaj zmiennej, w której zapisany jest adres w pamięci komputera. Oznacza to, że wskaźnik wskazuje miejsce, gdzie zapisana jest jakaś informacja (np. zmienna typu liczbowego czy struktura).
Obrazowo możemy wyobrazić sobie pamięć komputera jako bibliotekę a zmienne jako książki. Zamiast brać książkę z półki samemu (analogicznie do korzystania wprost ze zwykłych zmiennych), możemy podać bibliotekarzowi wypisany rewers z numerem katalogowym książki a on znajdzie ją za nas. Analogia ta nie jest doskonała, ale pozwala wyobrazić sobie niektóre cechy wskaźników: numer na rewersie identyfikuje pewną książkę, kilka rewersów może dotyczyć tej samej książki, numer w rewersie możemy skreślić i użyć go do zamówienia innej książki, a jeśli wpiszemy nieprawidłowy numer, to możemy dostać nie tę książkę, którą chcemy, albo też nie dostać nic.
Warto też przytoczyć w tym miejscu definicję adresu pamięci. Możemy powiedzieć, że adres to pewna liczba całkowita, jednoznacznie definiująca położenie pewnego obiektu w pamięci komputera. Tymi obiektami mogą być np. zmienne, elementy tablic czy nawet funkcje. Dokładniejszą definicję możesz znaleźć w Wikipedii.
+ Podstawy wskaźników The Adventures of Malloc and New by Eunsuk Kang and Jean Y ang symbol znaczenie użycie * weź wartość x *x * deklaracja wskaźnika do wartości int *x;& weź adres &x
Kiedy korzystać ze wskaźników ? listy
Jak dokumentować użycie wskaźników ?
Grafika Unicode stackoverflow question: i-dont-understand-how-this-code-replaces-the-head-of-a-linked-list-or-how-to ? ┌─head─┐ ┌──value──┐ ┌──value──┐ │ ├───────►│ 4 │ │ 0 │ └──────┘ ├───next──┤ ├───next──┤ │ ├───────────►│ NULL │ └─────────┘ └─────────┘
Operowanie na wskaźnikach By stworzyć wskaźnik do zmiennej i móc się nim posługiwać, należy przypisać mu odpowiednią wartość - adres obiektu, na jaki chcieliśmy aby wskazywał. Skąd mamy znać ten adres? W języku C możemy "zapytać się" o adres za pomocą operatora & (operatora pobrania adresu). Przeanalizuj następujący kod:
#include
int main (void) { int liczba = 80; printf("Wartość zmiennej liczba: %d\n", liczba ); printf("Adres zmiennej liczba: %p\n", &liczba ); return 0; } Program ten wypisuje adres pamięci, pod którym znajduje się zmienna oraz wartość jaką kryje zmienna przechowywana pod owym adresem. Przykładowy wynik: Wartość zmiennej liczba: 80 Adres zmiennej liczba: 0022FF74
Aby móc przechowywać taki adres, zadeklarujemy zmienną wskaźnikową. Ważną informacją, oprócz samego adresu wskazywanej zmiennej, jest typ wskazywanej zmiennej. Mimo że wskaźnik jest zawsze typu adresowego, kompilator wymaga od nas, abyśmy przy deklaracji podali typ zmiennej, na którą wskaźnik będzie wskazywał. Robi się to poprzez dodanie * (gwiazdki) przed nazwą wskaźnika, np.:
int *wskaznik1; // zmienna wskaźnikowa na obiekt typu liczba całkowita char *wskaznik2; // zmienna wskaźnikowa na obiekt typu znak float *wskaznik3; // zmienna wskaźnikowa na obiekt typu liczba zmiennoprzecinkowa
Niektórzy programiści mogą nieco błędnie interpretować wskaźnik do typu jako nowy typ i uważać, że jeśli napiszą: int * a,b,c; to otrzymają trzy wskaźniki do liczby całkowitej. W rzeczywistości uzyskamy jednak tylko jeden wskaźnik a, oraz dwie liczby całkowite b i c (tak jakbyśmy napisali int *a; int b, int c). W tym przypadku trzy wskaźniki otrzymamy pisząc: int *a,*b,*c; Aby uniknąć pomyłek, lepiej jest pisać gwiazdkę tuż przy zmiennej, albo jeszcze lepiej - nie mieszać deklaracji wskaźników i zmiennych: int *a; int b,c;
Dostęp do wskazywanego obiektu Aby dobrać się do wartości wskazywanej przez wskaźnik, należy użyć unarnego operatora * (gwiazdka), zwanego operatorem wyłuskania. Mimo, że kolejny raz używamy gwiazdki, oznacza ona teraz coś zupełnie innego. Jest tak, ponieważ używamy jej w zupełnie innym miejscu: nie przy deklaracji zmiennej (gdzie gwiazdka oznacza deklarowanie wskaźnika), a przy wykorzystaniu zmiennej, gdzie odgrywa rolę operatora, podobnie jak operator & (pobrania adresu obiektu). Program ilustrujący: #include
int main (void) { int liczba = 80; int *wskaznik = &liczba; // wskaznik przechowuje adres, ktory pobieramy od zmiennej liczba
printf("Wartosc zmiennej: %d, jej adres: %p.\n", liczba, &liczba); printf("Adres przechowywany we wskazniku: %p, wskazywana wartosc: %d.\n", wskaznik, *wskaznik);
*wskaznik = 42; // zapisanie liczby 42 do obiektu, na który wskazuje wskaznik printf("Wartosc zmiennej: %d, wartosc wskazywana przez wskaznik: %d\n", liczba, *wskaznik);
liczba = 0x42; // liczba podana w systemie szesnastkowym, podana po prefiksie 0x printf("Wartosc zmiennej: %d, wartosc wskazywana przez wskaznik: %d\n", liczba, *wskaznik);
return 0;
Przykładowy wynik programu: Wartosc zmiennej: 80, jej adres: 0022FF74. Adres przechowywany we wskazniku: 0022FF74, wskazywana wartosc: 80. Wartosc zmiennej: 42, wartosc wskazywana przez wskaznik: 42 Wartosc zmiennej: 66, wartosc wskazywana przez wskaznik: 66
Gdy argument jest wskaźnikiem... Czasami zdarza się, że argumentami funkcji są wskaźniki. W przypadku zwykłych zmiennych, nasza funkcja otrzymuje jedynie lokalne kopie argumentów, które zostały jej podane. Wszelkie zmiany dokonują się lokalnie i nie są widziane poza funkcją. Przekazując do funkcji wskaźnik, również zostaje stworzona kopia... wskaźnika, na którym możemy operować. Tu jednak kopiowanie i niewidoczne lokalne zmiany się kończą. Obiekt, na który wskazuje ten wskaźnik, znajduje się gdzieś w pamięci i możemy na nim działać (czyli na oryginale), tak więc zmiany te są widoczne po wyjściu z funkcji. Spróbujmy rozpatrzyć poniższy przykład: #include
void func_var (int zmienna) { zmienna = 4; } void func_pointer (int *zmienna) { (*zmienna) = 5;
int main (void) { int z=3; printf ("z= %d\n", z);
func_var (z); printf ("z= %d\n", z);
func_pointer (&z); printf ("z= %d\n", z);
return 0;
Wynikiem będzie: z= 3 z= 3 z= 5
Widzimy, że funkcje w języku C nie tylko potrafią zwracać określoną wartość, lecz także zmieniać dane, podane im jako argumenty. Ten sposób przekazywania argumentów do funkcji jest nazywany przekazywaniem przez wskaźnik (w przeciwieństwie do normalnego przekazywania przez wartość).
Pułapki wskaźników Ważne jest, aby przy posługiwaniu się wskaźnikami nigdy nie próbować odwoływać się do komórki wskazywanej przez wskaźnik o wartości NULL ani nie używać niezainicjowanego wskaźnika! Przykładem nieprawidłowego kodu może być np.: int *wsk; printf ("zawartosc komorki: %d\n", *(wsk)); /* Błąd */ wsk = NULL; printf ("zawartosc komorki: %d\n", *(wsk)); /* Błąd */
Pamiętaj też, że możesz być rozczarowany używając operatora sizeof, podając zmienną wskaźnikową. Uzyskana wielkość będzie oznaczała rozmiar adresu, a nie rozmiar typu użytego podczas deklarowania naszego wskaźnika. Wielkość ta będzie zawsze miała taki sam rozmiar dla każdego wskaźnika, w zależności od kompilatora, a także docelowej platformy. Zamiast tego używaj: sizeof(*wskaźnik). Przykład: char *zmienna; int z = sizeof zmienna; /* z może być równe 4 (rozmiar adresu na maszynie 32bit) */ z = sizeof(char*); /* robimy to samo, co wyżej */ z = sizeof *zmienna; /* tym razem z= rozmiar znaku, tj. 1 */ z = sizeof(char); /* robimy to samo, co wyżej */
Stałe wskaźniki Podobnie jak możemy deklarować zwykłe stałe, tak samo możemy mieć stałe wskaźniki - jednak są ich dwa rodzaje. Wskaźniki na stałą wartość: const int *a; int const * a; /* równoważnie */
oraz stałe wskaźniki: int * const b;
Słówko const przed typem działa jak w przypadku zwykłych stałych, tzn. nie możemy zmienić wartości wskazywanej przy pomocy wskaźnika.
W drugim przypadku słowo const jest tuż za gwiazdką oznaczającą typ wskaźnikowy, co skutkuje stworzeniem stałego wskaźnika, czyli takiego którego nie można przestawić na inny adres.
Obie opcje można połączyć, deklarując stały wskaźnik, którym nie można zmienić wartości wskazywanej zmiennej, i również można zrobić to na dwa sposoby:
const int * const c; int const * const c; /* równoważnie */
int i=0; const int *a=&i; int * const b=&i; int const * const c=&i; *a = 1; /* kompilator zaprotestuje */ *b = 2; /* ok */ *c = 3; /* kompilator zaprotestuje */ a = b; /* ok */ b = a; /* kompilator zaprotestuje */ c = a; /* kompilator zaprotestuje */
Wskaźniki na stałą wartość są przydatne między innymi w sytuacji gdy mamy duży obiekt (na przykład strukturę z kilkoma polami). Jeśli przypiszemy taką zmienną do innej zmiennej, kopiowanie może potrwać dużo czasu, a oprócz tego zostanie zajęte dużo pamięci. Przekazanie takiej struktury do funkcji albo zwrócenie jej jako wartość funkcji wiąże się z takim samym narzutem. W takim wypadku dobrze jest użyć wskaźnika na stałą wartość. void funkcja(const duza_struktura *ds) { /* czytamy z ds i wykonujemy obliczenia */ } .... funkcja(&dane); /* mamy pewność, że zmienna dane nie zostanie zmieniona */
Dynamiczna alokacja pamięci - tablice dynamiczne Mając styczność z tablicami można się zastanowić, czy nie dałoby się mieć tablic, których rozmiar dostosowuje się do naszych potrzeb a nie jest na stałe zaszyty w kodzie programu. Chcąc pomieścić więcej danych możemy po prostu zwiększyć rozmiar tablicy - ale gdy do przechowania będzie mniej elementów okaże się, że marnujemy pamięć. Język C umożliwia dzięki wskaźnikom i dynamicznej alokacji pamięci tworzenie tablic takiej wielkości, jakiej akurat potrzebujemy.
O co chodzi
Czym jest dynamiczna alokacja pamięci? Normalnie zmienne programu przechowywane są na tzw. stosie (ang. stack) - powstają, gdy program wchodzi do bloku, w którym zmienne są zadeklarowane a zwalniane w momencie, kiedy program opuszcza ten blok. Jeśli deklarujemy tak tablice, to ich rozmiar musi być znany w momencie kompilacji - żeby kompilator wygenerował kod rezerwujący odpowiednią ilość pamięci.
Dostępny jest jednak drugi rodzaj rezerwacji (czyli alokacji) pamięci. Jest to alokacja na stercie (ang. heap). Sterta to obszar pamięci wspólny dla całego programu, przechowywane są w nim zmienne, których czas życia nie jest związany z poszczególnymi blokami. Musimy sami rezerwować dla nich miejsce i to miejsce zwalniać, ale dzięki temu możemy to zrobić w dowolnym momencie działania programu.
Należy pamiętać, że rezerwowanie i zwalnianie pamięci na stercie zajmuje więcej czasu niż analogiczne działania na stosie. Dodatkowo, zmienna zajmuje na stercie więcej miejsca niż na stosie - sterta utrzymuje specjalną strukturę, w której trzymane są wolne partie (może to być np. lista). Tak więc używajmy dynamicznej alokacji tam, gdzie jest potrzebna - dla danych, których rozmiaru nie jesteśmy w stanie przewidzieć na etapie kompilacji lub ich żywotność ma być niezwiązana z blokiem, w którym zostały zaalokowane.
Obsługa pamięci Podstawową funkcją do rezerwacji pamięci jest funkcja malloc. Jest to niezbyt skomplikowana funkcja - podając jej rozmiar (w bajtach) potrzebnej pamięci, dostajemy wskaźnik do zaalokowanego obszaru.
Załóżmy, że chcemy stworzyć tablicę liczb typu float: //Pamietaj aby dodac na poczatku biblioteke stdlib.h!
int rozmiar; float *tablica;
rozmiar = 3; tablica = (float*) malloc(rozmiar * sizeof(*tablica)); //pierwsza gwiazdka (*) w funkcji malloc() to operator mnozenia tablica = 0.1;