C/Wskaźniki
Z Wikibooks, biblioteki wolnych podręcznikó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ługi pamięci dla zmiennych. W tym celu pomocne są wskaźniki.
[edytuj] Co to jest wskaźnik?
Wskaźnik (ang. pointer) to specjalny rodzaj zmiennej, w której zapisany jest adres w pamięci komputera, tzn. wskaźnik wskazuje miejsce, gdzie zapisana jest jakaś informacja. Oczywiście nic nie stoi na przeszkodzie aby wskazywaną daną był inny wskaźnik do kolejnego miejsca w pamięci.
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: 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, jeśli wpiszemy nieprawidłowy numer katalogowy to możemy dostać nie tą książkę, którą chcemy, albo też nie dostać nic.
Warto też poznać 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 (czyli np. znaku czy liczby) w pamięci komputera. Dokładniejszą definicję możesz znaleźć w Wikipedii.
[edytuj] 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 ma wskazywać). Skąd mamy znać ten adres? Wystarczy zapytać nasz komputer, jaki adres przydzielił zmiennej, którą np. wcześniej gdzieś stworzyliśmy. Robi się to za pomocą operatora & (operatora pobrania adresu). Przeanalizuj następujący kod[1]:
#include <stdio.h> int main (void) { int liczba = 80; printf("Zmienna znajduje sie pod adresem: %p, i przechowuje wartosc: %d\n", (void*)&liczba, liczba); return 0; }
Program ten wypisuje adres pamięci, pod którym znajduje się zmienna oraz wartość jaką kryje zmienna przechowywana pod owym adresem.
Aby móc zapisać gdzieś taki adres należy zadeklarować zmienną wskaźnikową. Robi się to poprzez dodanie * (gwiazdki) po typie na jaki zmienna ma wskazywać, np.:
int *wskaznik1; char *wskaznik2; float *wskaznik3;
Aby dobrać się do wartości wskazywanej przez zmienną należy użyć unarnego operatora * (gwiazdka), zwanego operatorem wyłuskania:
#include <stdio.h> int main (void) { int liczba = 80; int *wskaznik = &liczba; printf("Wartosc zmiennej: %d; jej adres: %p.\n", liczba, (void*)&liczba); printf("Adres zapisany we wskazniku: %p, wskazywana wartosc: %d.\n", (void*)wskaznik, *wskaznik); *wskaznik = 42; 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; }
[edytuj] O co chodzi z tym typem, na który ma wskazywać? Czemu to takie ważne?
Jest to ważne z kilku powodów.
Różne typy zajmują w pamięci różną wielkość. Przykładowo, jeżeli w zmiennej typu unsigned int zapiszemy liczbę 65 530, to w pamięci będzie istnieć jako:
+--------+--------+ |komórka1|komórka2| +--------+--------+ |11111111|11111010| = (unsigned int) 65530 +--------+--------+
Wskaźnik do takiej zmiennej (jak i do dowolnej innej) będzie wskazywać na pierwszą komórkę, w której ta zmienna ma swoją wartość.
Jeżeli teraz stworzymy drugi wskaźnik do tego adresu, tym razem typu unsigned char*, to wskaźnik przejmie ten adres prawidłowo[2], lecz gdy spróbujemy odczytać wartość na jaką wskazuje ten wskaźnik to zostanie odczytana tylko pierwsza komórka i wynik będzie równy 255:
+--------+ |komórka1| +--------+ |11111111| = (unsigned char) 255 +--------+
Gdybyśmy natomiast stworzyli inny wskaźnik do tego adresu tym razem typu unsigned long* to przy próbie odczytu odczytane zostaną dwa bajty z wartością zapisaną w zmiennej unsigned int oraz dodatkowe dwa bajty z niewiadomą zawartością i wówczas wynik będzie równy 65530 * 65536 + przypadkowa wartość :
+--------+--------+--------+--------+ |komórka1|komórka2|komórka3|komórka4| +--------+--------+--------+--------+ |11111111|11111010|????????|????????| +--------+--------+--------+--------+
Ponadto, zapis czy odczyt poza przydzielonym obszarem pamięci może prowadzić do nieprzyjemnych skutków takich jak zmiana wartości innych zmiennych czy wręcz natychmiastowe przerwanie programu. Jako przykład można podać ten (błędny) program[3]:
#include <stdio.h> int main(void) { unsigned char tab[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; unsigned short *ptr= (unsigned short*)&tab[2]; unsigned i; *ptr = 0xffff; for (i = 0; i < 10; ++i) { printf("%d ", tab[i]); } printf("%d\n", tab[10]); return 0; }
Nie można również zapominać, że na niektórych architekturach dane wielobajtowe muszą być odpowiednio wyrównane w pamięci. Np. zmienna dwubajtowa może się znajdować jedynie pod parzystymi adresami. Wówczas, gdybyśmy chcieli adres zmiennej jednobajtowej przypisać wskaźnikowi na zmienną dwubajtową mogłoby dojść do nieprzewidzianych błędów wynikających z próby odczytu niewyrównanej danej.
Zaskakujące może się okazać, że różne wskaźniki mogą mieć różny rozmiar. Np. wskaźnik na char może być większy od wskaźnika na int, ale również na odwrót. Co więcej, wskaźniki różnych typów mogą się różnić reprezentacją adresów. Dla przykładu wskaźnik na char może przechowywać adres do bajtu natomiast wskaźnik na int ten adres podzielony przez 2.
Podsumowując, różne wskaźniki to różne typy i nie należy beztrosko rzutować wyrażeń pomiędzy różnymi typami wskaźnikowymi, bo grozi to nieprzewidywalnymi błędami.
[edytuj] Do czego służy typ void*?
Czasami zdarza się, że nie wiemy, na jaki typ wskazuje dany wskaźnik. W takich przypadkach stosujemy typ void*. Sam void nie znaczy nic, natomiast void* oznacza "wskaźnik na obiekt w pamięci niewiadomego typu". Taki wskaźnik możemy potem odnieść do konkretnego typu danych (w języku C++ wymagana jest do tego operacja rzutowania). Na przykład, funkcja malloc zwraca właśnie wskaźnik za pomocą void*.
[edytuj] Arytmetyka wskaźników
W języku C do wskaźników można dodawać lub odejmować liczby całkowite. Istotne jest jednak, że dodanie do wskaźnika liczby 2 nie spowoduje przesunięcia się w pamięci komputera o dwa bajty. Tak naprawdę przesuniemy się o 2*rozmiar zmiennej. Jest to bardzo ważna informacja! Początkujący programiści popełniają często dużo błędów, związanych z nieprawidłową arytmetyką wskaźników.
Zobaczmy na przykład:
int *ptr; int a[] = {1, 2, 3, 5, 7}; ptr = &a[0];
Otrzymujemy następującą sytuację:
Gdy wykonamy:
ptr += 2;
wskaźnik ustawi się na trzecim elemencie tablicy.
Wskaźniki można również od siebie odejmować, czego wynikiem jest odległość dwóch wskazywanych wartości. Odległość zwracana jest jako liczba obiektów danego typu, a nie liczba bajtów. Np.:
int a[] = {1, 2, 3, 5, 7}; int *ptr = &a[2]; int diff = ptr - a; /* diff ma wartość 2 (a nie 2*sizeof(int)) */
Wynikiem może być oczywiście liczba ujemna. Operacja jest przydatna do obliczania wielkości tablicy (długości łańcucha znaków) jeżeli mamy wskaźnik na jej pierwszy i ostatni element.
Operacje arytmetyczne na wskaźnikach mają pewne ograniczenia. Przede wszystkim nie można (tzn. standard tego nie definiuje) skonstruować wskaźnika wskazującego gdzieś poza zadeklarowaną tablicę, chyba, że jest to obiekt zaraz za ostatnim (one past last), np.:
int a[] = {1, 2, 3, 5, 7}; int *ptr; ptr = a + 10; /* niezdefiniowane */ ptr = a - 10; /* niezdefiniowane */ ptr = a + 5; /* zdefiniowane (element za ostatnim) */ *ptr = 10; /* to już nie! */
Nie można[4] również odejmować od siebie wskaźników wskazujących na obiekty znajdujące się w różnych tablicach, np.:
int a[] = {1, 2, 3}, b[] = {5, 7}; int *ptr1 = a, *ptr2 = b; int diff = a - b; /* niezdefiniowane */
[edytuj] Tablice a wskaźniki
Trzeba wiedzieć, że tablice to też rodzaj zmiennej wskaźnikowej. Taki wskaźnik wskazuje na miejsce w pamięci, gdzie przechowywany jest jej pierwszy element. Następne elementy znajdują się bezpośrednio w następnych komórkach pamięci, w odstępie zgodnym z wielkością odpowiedniego typu zmiennej.
Na przykład tablica:
int tab[] = {100,200,300};
występuje w pamięci w sześciu komórkach [5]:
+--------+--------+--------+--------+--------+--------+ |wartosc1| |wartosc2| |wartosc3| | +--------+--------+--------+--------+--------+--------+ |00000000|01100100|00000000|11001000|00000001|00101100| +--------+--------+--------+--------+--------+--------+
Stąd do trzeciej wartości można się dostać tak (komórki w tablicy numeruje się od zera):
zmienna = tab[2];
albo wykorzystując metodę wskaźnikową:
zmienna = *(tab + 2);
Z definicji obie te metody są równoważne.
Z definicji (z wyjątkiem użycia operatora sizeof) wartością zmiennej lub wyrażenia typu tablicowego jest wskaźnik na jej pierwszy element (tab == &tab[0]).
Co więcej, można pójść w drugą stronę i potraktować wskaźnik jak tablicę:
int *wskaznik; wskaznik = &tab[1]; /* lub wskaznik = tab + 1; */ zmienna = wskaznik[1]; /* przypisze 300 */
Jako ciekawostkę podamy, iż w języku C można odnosić się do elementów tablicy jeszcze w inny sposób:
printf ("%d\n", 1[tab]);
Skąd ta dziwna notacja? Uzasadnienie jest proste:
tab[1] = *(tab + 1) = *(1 + tab) = 1[tab]
Podobną składnię stosuje m.in. asembler GNU.
[edytuj] Gdy argument jest wskaźnikiem...
Czasami zdarza się, że argumentem (lub argumentami) funkcji są wskaźniki. W przypadku "normalnych" zmiennych nasza funkcja działa tylko na lokalnych kopiach tychże argumentów, natomiast nie zmienia zmiennych, które zostały podane jako argument. Natomiast w przypadku wskaźnika, każda operacja na wartości wskazywanej powoduje zmianę wartości zmiennej zewnętrznej. Spróbujmy rozpatrzeć poniższy przykład:
#include <stdio.h>
void func (int *zmienna)
{
*zmienna = 5;
}
int main ()
{
int z=3;
printf ("z=%d\n", z); /* wypisze 3 */
func(&z);
printf ("z=%d\n", z); /* wypisze 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ść).
Nie gra roli czy przy deklaracji funkcji jako argument funkcji podamy wskaźnik czy tablicę (z podanym rozmiarem lub nie), np. poniższe deklaracje są identyczne:
void func(int ptr[]); void func(int *ptr);
Można przyjąć konwencję, że deklaracja określa czy funkcji przekazujemy wskaźnik do pojedynczego argumentu czy do sekwencji, ale równie dobrze można za każdym razem stosować gwiazdkę.
[edytuj] 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 lub niezainicjowany wskaźnik! Przykładem nieprawidłowego kodu, może być np.:
int *wsk;
printf ("zawartosc komorki: %d\n", *(wsk)); /* Błąd */
wsk = 0; /* 0 w kontekście wskaźników oznacza wskaźnik NULL */
printf ("zawartosc komorki: %d\n", *(wsk)); /* Błąd */
Należy również uważać, aby nie odwoływać się do komórek poza przydzieloną pamięcią, np.:
int tab[] = { 0, 1, 2 };
tab[3] = 3; /* Błąd */
Pamiętaj też, że możesz być rozczarowany używając operatora sizeof, podając zmienną wskaźnikową. Uzyskana wielkość będzie wielkością wskaźnika, a nie wielkością 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 a = sizeof zmienna; /* a wynosi np. 4, tj. sizeof(char*) */
a = sizeof(char*); /* robimy to samo, co wyżej */
a = sizeof *zmienna; /* zmienna a ma teraz przypisany rozmiar
pojedynczego znaku, tj. 1 */
a = sizeof(char); /* robimy to samo, co wyżej */
[edytuj] Na co wskazuje NULL?
Analizując kody źródłowe programów często można spotkać taki oto zapis:
void *wskaznik = NULL; /* lub = 0 */
Wiesz już, że nie możemy odwołać się pod komórkę pamięci wskazywaną przez wskaźnik NULL. Po co zatem przypisywać wskaźnikowi 0? Odpowiedź może być zaskakująca: właśnie po to, aby uniknąć błędów! Wydaje się to zabawne, ale większość (jeśli nie wszystkie) funkcje, które zwracają wskaźnik w przypadku błędu zwrócą właśnie NULL, czyli zero. Tutaj rodzi się kolejna wskazówka: jeśli w danej zmiennej przechowujemy wskaźnik, zwrócony wcześniej przez jakąś funkcję zawsze sprawdzajmy, czy nie jest on równy 0 (NULL). Wtedy mamy pewność, że funkcja zadziałała poprawnie.
Dokładniej, NULL nie jest słowem kluczowym, lecz stałą (makrem) zadeklarowaną przez dyrektywy preprocesora. Deklaracja taka może być albo wartością 0 albo też wartością 0 zrzutowaną na void* (((void *)0)), ale też jakimś słowem kluczowym deklarowanym przez kompilator.
Warto zauważyć, że pomimo przypisywania wskaźnikowi zera, nie oznacza to, że wskaźnik NULL jest reprezentowany przez same zerowe bity. Co więcej, wskaźniki NULL różnych typów mogą mieć różną wartość! Z tego powodu poniższy kod jest niepoprawny:
int **tablica_wskaznikow = calloc(100, sizeof *tablica_wskaznikow);
Zakłada on, że w reprezentacji wskaźnika NULL występują same zera. Poprawnym zainicjowaniem dynamicznej tablicy wskaźników wartościami NULL jest (pomijamy sprawdzanie wartości zwróconej przez malloc()):
int **tablica_wskaznikow = malloc(100 * sizeof *tablica_wskaznikow); int i = 0; while (i<100) tablica_wskaznikow[i++] = 0;
| Do zrobienia: * przykład jest niezrozumiały
|
[edytuj] Stałe wskaźniki
Tak, jak istnieją 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; /* lub równoważnie */ int const *a;
oraz stałe wskaźniki:
int * const b;
Pierwszy to wskaźnik, którym nie można zmienić wskazywanej wartości. Drugi to wskaźnik, którego nie można przestawić na inny adres. Dodatkowo, można zadeklarować 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; /* alternatywnie */ int const * const c;
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 */
[edytuj] Dynamiczna alokacja pamięci
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.
[edytuj] 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.
[edytuj] 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:
int rozmiar; float *tablica; rozmiar = 3; tablica = 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 (addr);
| 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).
[edytuj] Tablice wielowymiarowe
W rozdziale Tablice pokazaliśmy, jak tworzyć tablice wielowymiarowe, gdy ich rozmiar jest znany w czasie kompilacji. Teraz zaprezentujemy, jak to wykonać za pomocą wskaźników i to w sytuacji, gdy rozmiar może się zmieniać. Załóżmy, że chcemy stworzyć tabliczkę mnożenia:
int rozmiar; int i; int **tabliczka; printf("Podaj rozmiar tabliczki mnozenia: "); scanf("%i", &rozmiar); /* dla prostoty nie będziemy sprawdzali, czy użytkownik wpisał sensowną wartość */ tabliczka = malloc(rozmiar * sizeof *tabliczka); /* 1 */ for (i = 0; i<rozmiar; ++i) { /* 2 */ tabliczka[i] = malloc(rozmiar * sizeof **tabliczka); /* 3 */ } /* 4 */ for (i = 0; i<rozmiar; ++i) { int j; for (j = 0; j<rozmiar; ++j) { tabliczka[i][j] = (i+1)*(j+1); } }
Najpierw musimy przydzielić pamięć - najpierw dla "tablicy tablic" (1) a potem dla każdej z podtablic osobno (2-4). Ponieważ tablica jest typu int* to nasza tablica tablic będzie wskaźnikiem na int* czyli int**. Podobnie osobno, ale w odwrotnej kolejności będziemy zwalniać tablicę wielowymiarową:
for (i = 0; i<rozmiar; ++i) { free(tabliczka[i]); } free(tabliczka);
Należy nie pomylić kolejności: po wykonaniu free(tabliczka) nie będziemy mieli prawa odwoływać się do tabliczka[i] (bo wcześniej dokonaliśmy zwolnienia tego obszaru pamięci).
Można także zastosować bardziej oszczędny sposób alokowania tablicy wielowymiarowej, a mianowicie:
#define ROZMIAR 10 int i; int **tabliczka = malloc(ROZMIAR * sizeof *tabliczka); *tabliczka = malloc(ROZMIAR * ROZMIAR * sizeof **tabliczka); for (i = 1; i<ROZMIAR; ++i) { tabliczka[i] = tabliczka[0] + (i * ROZMIAR); } for (i = 0; i<ROZMIAR; ++i) { int j; for (j = 0; j<ROZMIAR; ++j) { tabliczka[i][j] = (i+1)*(j+1); } } free(*tabliczka); free(tabliczka);
Powyższy kod działa w ten sposób, że zamiast dla poszczególnych wierszy alokować osobno pamięć alokuje pamięć dla wszystkich elementów tablicy i dopiero później przypisuje wskazania poszczególnych wskaźników-wierszy na kolejne bloki po ROZMIAR elementów.
Sposób ten jest bardziej oszczędny z dwóch powodów: Po pierwsze wykonywanych jest mniej operacji przydzielania pamięci (bo tylko dwie). Po drugie za każdym razem, gdy alokuje się pamięć trochę miejsca się marnuje, gdyż funkcja malloc musi w stogu przechowywać różne dodatkowe informacje na temat każdej zaalokowanej przestrzeni. Ponadto, czasami alokacja odbywa się blokami i gdy zażąda się niepełny blok to reszta bloku jest tracona.
Zauważmy, że w ten sposób możemy uzyskać nie tylko normalną, "kwadratową" tablicę (dla dwóch wymiarów). Możliwe jest np. uzyskanie tablicy trójkątnej:
0123 012 01 0
lub tablicy o dowolnym innym rozkładzie długości wierszy, np.:
const size_t wymiary[] = { 2, 4, 6, 8, 1, 3, 5, 7, 9 }; int i; int **tablica = malloc((sizeof wymiary / sizeof *wymiary) * sizeof *tablica); for (i = 0; i<10; ++i) { tablica[i] = malloc(wymiary[i] * sizeof **tablica); }
Gdy nabierzesz wprawy w używaniu wskaźników oraz innych funkcji malloc i realloc nauczysz się wykonywać różne inne operacje takie jak dodawanie kolejnych wierszy, usuwanie wierszy, zmiana rozmiaru wierszy, zamiana wierszy miejscami itp.
[edytuj] Wskaźniki na funkcje
Dotychczas zajmowaliśmy się sytuacją, gdy wskaźnik wskazywał na jakąś zmienną. Jednak nie tylko zmienna ma swój adres w pamięci. Oprócz zmiennej także i funkcja musi mieć swoje określone miejsce w pamięci. A ponieważ funkcja ma swój adres[6], to nie ma przeszkód, aby i na nią wskazywał jakiś wskaźnik.
[edytuj] Deklaracja wskaźnika na funkcję
Tak naprawdę kod maszynowy utworzony po skompilowaniu programu odnosi się właśnie do adresu funkcji. Wskaźnik na funkcję różni się od innych rodzajów wskaźników. Jedną z głównych różnic jest jego deklaracja. Zwykle wygląda ona tak:
typ_zwracanej_wartości (*nazwa_wskaźnika)(typ1 parametr1, typ2 parametr2);
Oczywiście parametrów może być więcej (albo też w ogóle może ich nie być). Oto przykład wykorzystania wskaźnika na funkcję:
#include <stdio.h>
int suma (int a, int b)
{
return a+b;
}
int main ()
{
int (*wsk_suma)(int a, int b);
wsk_suma = suma;
printf("4+5=%d\n", wsk_suma(4,5));
return 0;
}
Zwróćmy uwagę na dwie rzeczy:
- przypisując nazwę funkcji bez nawiasów do wskaźnika automatycznie informujemy kompilator, że chodzi nam o adres funkcji
- wskaźnika używamy tak, jak normalnej funkcji, na którą on wskazuje
[edytuj] Do czego można użyć wskaźników na funkcje?
Język C jest językiem strukturalnym, jednak dzięki wskaźnikom istnieje w nim możliwość "zaszczepienia" pewnych obiektowych właściwości. Wskaźnik na funkcję może być np. elementem struktury - wtedy mamy bardzo prymitywną namiastkę klasy, którą dobrze znają programiści, piszący w języku C++. Ponadto dzięki wskaźnikom możemy tworzyć mechanizmy działające na zasadzie funkcji zwrotnej[7]. Dobrym przykładem może być np. tworzenie sterowników, gdzie musimy poinformować różne podsystemy, jakie funkcje w naszym kodzie służą do wykonywania określonych czynności. Przykład:
struct urzadzenie {
int (*otworz)(void);
void (*zamknij)(void);
};
int moje_urzadzenie_otworz (void)
{
/* kod...*/
}
void moje_urzadzenie_zamknij (void)
{
/* kod... */
}
int rejestruj_urzadzenie(struct urzadzenie &u) {
/* kod... */
}
int init (void)
{
struct urzadzenie moje_urzadzenie;
moje_urzadzenie.otworz = moje_urzadzenie_otworz;
moje_urzadzenie.zamknij = moje_urzadzenie_zamknij;
rejestruj_urzadzenie(&moje_urzadzenie);
}
W ten sposób w pamięci każda klasa musi przechowywać wszystkie wskaźniki do wszystkich metod. Innym rozwiązaniem może być stworzenie statycznej struktury ze wskaźnikami do funkcji i wówczas w strukturze będzie przechowywany jedynie wskaźnik do tej struktury, np.:
struct urzadzenie_metody {
int (*otworz)(void);
void (*zamknij)(void);
};
struct urzadzenie {
const struct urzadzenie_metody *m;
};
int moje_urzadzenie_otworz (void)
{
/* kod...*/
}
void moje_urzadzenie_zamknij (void)
{
/* kod... */
}
static const struct urzadzenie_metody
moje_urzadzenie_metody = {
moje_urzadzenie_otworz,
moje_urzadzenie_zamknij
};
int rejestruj_urzadzenie(struct urzadzenie *u) {
/* kod... */
}
int init (void)
{
struct urzadzenie moje_urzadzenie;
moje_urzadzenie.m = &moje_urzadzenie_metody;
rejestruj_urzadzenie(&moje_urzadzenie);
}
[edytuj] Możliwe deklaracje wskaźników
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 typ int */ int (*apa[])[];/* tablica wskaźników 'apa' wskazujących wskaźniki na typ 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 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 */
[edytuj] Popularne błędy
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ą:
- odwołania do adresów pamięci, które są poza obszarem przydzielonym funkcją malloc()
- brak sprawdzania, czy dany wskaźnik nie ma wartości NULL
- wycieki pamięci, czyli niezwalnianie całej, przydzielonej wcześniej pamięci
[edytuj] Ciekawostki
- w rozdziale Zmienne pisaliśmy o stałych. Normalnie nie mamy możliwości zmiany ich wartości, ale z użyciem wskaźników staje się to możliwe:
const int CONST = 0; int *c = &CONST; *c = 1; printf("%i\n", CONST); /* wypisuje 1 */
Konstrukcja taka może jednak wywołać ostrzeżenie kompilatora bądź nawet jego błąd - wtedy może pomóc jawne rzutowanie z const int* na int*.
- język C++ oferuje mechanizm podobny do wskaźników, ale nieco wygodniejszy – referencje
- język C++ dostarcza też innego sposobu dynamicznej alokacji i zwalniania pamięci - przez operatory new i delete
- w rozdziale Typy złożone znajduje się opis implementacji listy za pomocą wskaźników. Przykład ten może być bardzo przydatny przy zrozumieniu, po co istnieją wskaźniki, jak się nimi posługiwać oraz jak dobrze zarządzać pamięcią.
Przypisy
- ↑ Warto zwrócić uwagę na rzutowanie do typu wskaźnik na void. Rzutowanie to jest wymagane przez funkcję printf, gdyż ta oczekuje, że argumentem dla formatu %p będzie właśnie wskaźnik na void, gdy tymczasem w naszym przykładzie wyrażenie &liczba jest typu wskaźnik na int.
- ↑ Tak naprawdę nie zawsze można przypisywać wartości jednych wskaźników do innych. Standard C gwarantuje jedynie, że można przypisać wskaźnikowi typu void* wartość dowolnego wskaźnika, a następnie przypisać tą wartość do wskaźnika pierwotnego typu oraz, że dowolny wskaźnik można przypisać do wskaźnika typu char*.
- ↑ Może się okazać, że błąd nie będzie widoczny na Twoim komputerze.
- ↑ To znaczy standard nie definiuje co się wtedy stanie, aczkolwiek na większości architektur odejmowanie dowolnych dwóch wskaźników ma zdefiniowane zachowanie. Pisząc przenośne programy nie można jednak na tym polegać, zwłaszcza, że odejmowanie wskaźników wskazujących na elementy różnych tablic zazwyczaj nie ma sensu.
- ↑ Ponownie przyjmując, że bajt ma 8 bitów, int dwa bajty i liczby zapisywane są w formacie little endian
- ↑ Tak naprawdę kod maszynowy utworzony po skompilowaniu programu odnosi się właśnie do adresu funkcji.
- ↑ Funkcje zwrotne znalazły zastosowanie głównie w programowaniu GUI