C/Wskaźniki

Z Wikibooks, biblioteki wolnych podręczników.
< C
Skocz do: nawigacji, wyszukiwania
Wikipedia, nasz siostrzany projekt, zawiera artykuł na temat wskaźników.

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?[edytuj]

Porada Dla ułatwienia przyjęto poniżej, że bajt ma 8 bitów, typ int składa się z dwóch bajtów (16 bitów), typ long składa się z czterech bajtów (32 bitów) oraz liczby zapisane są w formacie big endian (tzn. bardziej znaczący bajt na początku), co niekoniecznie musi być prawdą na Twoim komputerze.
Wskaźnik a wskazujący na zmienną b. Zauważmy, że b przechowuje liczbę, podczas gdy a przechowuje adres b w pamięci (1462)

Wskaźnik (ang. pointer) 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 [1]
symbol znaczenie użycie
* weź wartość x *x
* deklaracja wskaźnika do wartości int *x;
& weź adres &x

Operowanie na wskaźnikach[edytuj]

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 <stdio.h>
 
 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
Uwaga! Uwaga!

Należy mieć na uwadze, że gwiazdkę łączymy ze zmienną, nie z typem.

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[edytuj]

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 <stdio.h>
 
 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;
   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...[edytuj]

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 rozpatrzeć poniższy przykład:

 #include <stdio.h>
 
 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ść).

Uwaga! Uwaga!
Zwróćmy uwagę na wywołanie func_pointer(&z);. Należy pamiętać, by do funkcji oczekującej wskaźnika przekazać adres zmiennej, a nie samą zmienną. Jeśli byśmy napisali func_pointer(z); wówczas funkcja użyłaby liczby 3 jako adres i starałaby się zmienić komórkę pamięci o numerze 3. Kompilator powinien ostrzec w takim przypadku o konwersji z typu int do wskaźnika, ale jeśli zignorujemy ostrzeżenie, nasz program prawdopodobnie zamknie się z komunikatem o błędzie.

Pułapki wskaźników[edytuj]

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[edytuj]

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[edytuj]

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[edytuj]

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[edytuj]

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:

 int rozmiar;
 float *tablica;
 
 rozmiar = 3;
 tablica = (float*) malloc(rozmiar * sizeof *tablica);
 tablica[0] = 0.1;

Przeanalizujmy teraz po kolei, co dzieje się w powyższym fragmencie. Najpierw deklarujemy zmienne - rozmiar tablicy i wskaźnik, który będzie wskazywał obszar w pamięci, gdzie będzie trzymana tablica. Do zmiennej "rozmiar" możemy w trakcie działania programu przypisać cokolwiek - wczytać ją z pliku, z klawiatury, obliczyć, wylosować - nie jest to istotne. rozmiar * sizeof *tablica oblicza potrzebną wielkość tablicy. Dla każdej zmiennej float potrzebujemy tyle bajtów, ile zajmuje ten typ danych. Ponieważ może się to różnić na rozmaitych maszynach, istnieje operator sizeof, zwracający dla danego wyrażenia rozmiar jego typu w bajtach.

W wielu książkach (również K&Rv2) i w Internecie stosuje się inny schemat użycia funkcji malloc a mianowicie: tablica = (float*)malloc(rozmiar * sizeof(float)). Takie użycie należy traktować jako błędne, gdyż nie sprzyja ono poprawnemu wykrywaniu błędów.

Rozważmy sytuację, gdy programista zapomni dodać plik nagłówkowy stdlib.h, wówczas kompilator (z braku deklaracji funkcji malloc) przyjmie, że zwraca ona typ int, zatem do zmiennej tablica (która jest wskaźnikiem) będzie przypisywana liczba całkowita, co od razu spowoduje błąd kompilacji (a przynajmniej ostrzeżenie), dzięki czemu będzie można szybko poprawić kod programu. Rzutowanie jest konieczne tylko w języku C++, gdzie konwersja z void* na inne typy wskaźnikowe nie jest domyślna, ale język ten oferuje nowe sposoby alokacji pamięci.

Teraz rozważmy sytuację, gdy zdecydujemy się zwiększyć dokładność obliczeń i zamiast typu float użyć typu double. Będziemy musieli wyszukać wszystkie wywołania funkcji malloc, calloc i realloc odnoszące się do naszej tablicy i zmieniać wszędzie sizeof(float) na sizeof(double). Aby temu zapobiec lepiej od razu użyć sizeof *tablica (lub jeśli ktoś woli z nawiasami: sizeof(*tablica)), wówczas zmiana typu zmiennej tablica na double* zostanie od razu uwzględniona przy alokacji pamięci.

Dodatkowo, należy sprawdzić, czy funkcja malloc nie zwróciła wartości NULL - dzieje się tak, gdy zabrakło pamięci. Ale uwaga: może się tak stać również jeżeli jako argument funkcji podano zero.

Jeśli dany obszar pamięci nie będzie już nam więcej potrzebny powinniśmy go zwolnić, aby system operacyjny mógł go przydzielić innym potrzebującym procesom. Do zwolnienia obszaru pamięci używamy funkcji free(), która przyjmuje tylko jeden argument - wskaźnik, który otrzymaliśmy w wyniku działania funkcji malloc().

 free (tablica);
Uwaga! Uwaga!
Należy pamiętać o zwalnianiu pamięci - inaczej dojdzie do tzw. wycieku pamięci - program będzie rezerwował nową pamięć, ale nie zwracał jej z powrotem i w końcu pamięci może mu zabraknąć.

Należy też uważać, by nie zwalniać dwa razy tego samego miejsca. Po wywołaniu free wskaźnik nie zmienia wartości, pamięć wskazywana przez niego może też nie od razu ulec zmianie. Czasem możemy więc korzystać ze wskaźnika (zwłaszcza czytać) po wywołaniu free nie orientując się, że robimy coś źle - i w pewnym momencie dostać komunikat o nieprawidłowym dostępie do pamięci. Z tego powodu zaraz po wywołaniu funkcji free można przypisać wskaźnikowi wartość 0.

Czasami możemy potrzebować zmienić rozmiar już przydzielonego bloku pamięci. Tu z pomocą przychodzi funkcja realloc:

 tablica = realloc(tablica, 2*rozmiar*sizeof *tablica);

Funkcja ta zwraca wskaźnik do bloku pamięci o pożądanej wielkości (lub NULL gdy zabrakło pamięci). Uwaga - może to być inny wskaźnik. Jeśli zażądamy zwiększenia rozmiaru a za zaalokowanym aktualnie obszarem nie będzie wystarczająco dużo wolnego miejsca, funkcja znajdzie nowe miejsce i przekopiuje tam starą zawartość. Jak widać, wywołanie tej funkcji może być więc kosztowne pod względem czasu.

Ostatnią funkcją jest funkcja calloc(). Przyjmuje ona dwa argumenty: liczbę elementów tablicy oraz wielkość pojedynczego elementu. Podstawową różnicą pomiędzy funkcjami malloc() i calloc() jest to, że ta druga zeruje wartość przydzielonej pamięci (do wszystkich bajtów wpisuje wartość 0).

Przykład tworzenia tablicy typu float przy użyciu calloc() zamiast malloc():

 int rozmiar;
 float *tablica;
 
 rozmiar = 3;
 tablica = (float*) calloc(rozmiar, sizeof *tablica);
 tablica[0] = 0.1;

Możliwe deklaracje wskaźników[edytuj]

Tutaj znajduje się krótkie kompendium jak definiować wskaźniki oraz co oznaczają poszczególne definicje:

int i;         /* zmienna całkowita (typu int) 'i' */
int *p;        /* wskaźnik 'p' wskazujący na zmienną całkowitą */
int a[];       /* tablica 'a' liczb całkowitych typu int */
int f();       /* funkcja 'f' zwracająca liczbę całkowitą typu int */
int **pp;      /* wskaźnik 'pp' wskazujący na wskaźnik wskazujący na liczbę całkowitą typu int */
int (*pa)[];   /* wskaźnik 'pa' wskazujący na tablicę liczb całkowitych typu int */
int (*pf)();   /* wskaźnik 'pf" wskazujący na funkcję zwracającą liczbę całkowitą typu int */
int *ap[];     /* tablica 'ap' wskaźników na liczby całkowite typu int */
int *fp();     /* funkcja 'fp', która zwraca wskaźnik na zmienną typu int */
int ***ppp;    /* wskaźnik 'ppp' wskazujący na wskaźnik wskazujący na wskaźnik wskazujący na liczbę  typu int */
int (**ppa)[]; /* wskaźnik 'ppa' na wskaźnik wskazujący na tablicę liczb całkowitych typu int */
int (**ppf)(); /* wskaźnik 'ppf' wskazujący na wskaźnik funkcji zwracającej dane typu int */
int *(*pap)[]; /* wskaźnik 'pap' wskazujący na tablicę wskaźników na typ int */
int *(*pfp)(); /* wskaźnik 'pfp' na funkcję zwracającą wskaźnik na typ int*/
int **app[];   /* tablica wskaźników 'app' wskazujących na wskaźniki wskazujące na typ int */
int (*apa[])[];/* tablica wskaźników 'apa' wskazujących na tablicę liczb całkowitych typu int */
int (*apf[])();/* tablica wskaźników 'apf' na funkcje, które zwracają typ int */
int **fpp();   /* funkcja 'fpp', która zwraca wskaźnik na wskaźnik, który wskazuje typ int */
int (*fpa())[];/* funkcja 'fpa', która zwraca wskaźnik na tablicę liczb typu int */
int (*fpf())();/* funkcja 'fpf', która zwraca wskaźnik na funkcję, która zwraca dane typu int */

Popularne błędy[edytuj]

Jednym z najczęstszych błędów, oprócz prób wykonania operacji na wskaźniku NULL, są odwołania się do obszaru pamięci po jego zwolnieniu. Po wykonaniu funkcji free() nie możemy już wykonywać żadnych odwołań do zwolnionego obszaru. Innym rodzajem błędów są:

  1. odwołania do adresów pamięci, które są poza obszarem przydzielonym funkcją malloc()
  2. brak sprawdzania, czy dany wskaźnik nie ma wartości NULL
  3. wycieki pamięci, czyli gubienie wskaźników do zaalokowanej pamięci powstające w tym wypadku przez niezwalnianie całej, przydzielonej wcześniej pamięci[2]

Wycieki pamięci[edytuj]

Przykład funkcji powodującej wyciek pamięci ( tworzy wskaźnik, przydziela pamięć i nie zwalnia pamięci po zakończeniu funkcji) : [3]

/* 
 Function with memory leak 
 http://www.geeksforgeeks.org/what-is-memory-leak-how-can-we-avoid/
*/
#include <stdlib.h>
 
void f()
{
   int *ptr = (int *) malloc(sizeof(int));
 
   /* Do some work */
 
   return; /* Return without freeing ptr*/
}



Powinno być :

/* 
Function without memory leak
http://www.geeksforgeeks.org/what-is-memory-leak-how-can-we-avoid/
 */
#include <stdlib.h>;
 
void f()
{
   int *ptr = (int *) malloc(sizeof(int));
 
   /* Do some work */
 
   free(ptr);
   return;
}


Inne przykłady[4] Do znajdowania wycieków pamięci możemy użyć programów :

Zobacz też[edytuj]


Przypisy