Autorem poniższego opracowania jest dr Piotr A. Dybczyński z Instytutu Obserwatorium Astronomiczne UAM w Poznaniu.
Wskaźniki (pointery) i tablice
Koniecznie proszę przeczytać odpowiedni rozdział w podręczniku (K&R, Rozdział 5).
Skrótowe i subiektywne podsumowanie najważniejszych faktów dotyczących pointerów i tablic w języku C:
- Operator jednoargumentowy & , zwany czasami operatorem referencji lub adresowania, umieszczony przed zmienną tworzy wyrażenie, którego wartość jest "adresem" tej zmiennej, wskazaniem na tę zmienną, pointerem do tej zmiennej itd.
- Użycie słowa adres w odniesieniu do pointerów i operacji na nich jest całkowicie uzasadnione, pod warunkiem, że uświadomimy sobie, iż taki "adres" to wartość nie nadająca się do oglądania, interpretacji, całkowicie zależna od architektury systemu komputerowego i systemu operacyjnego. Jest to adres tylko w tym sensie, że jednoznacznie identyfikuje położenie wskazywanego obiektu w pamięci widocznej dla naszego procesu.
- Operatorem odwrotnym do & jest również jednoargumentowy operator *. Zwany bywa operatorem wyłuskania, odniesienia itp. Umieszczony przed wyrażeniem w1 tworzy nowe wyrażenie (w2 = *(w1)), którego wartością jest obiekt wskazywany przez wartość wyrażenia w1. Oczywiście w tym przykładzie w1 i w2 są zupełnie różnych typów.
- Wskaźniki (pointery) deklarujemy podobnie jak zmienne, umieszczając jednak przed nazwą zmiennej operator *. Sama nazwa zmiennej funkcjonuje wtedy w programie jako wskaźnik a poprzedzona * oznacza obiekt wskazywany przez ten wskaźnik,
- Poza sytuacjami specjalnymi kompilator (a potem program czy proces) musi w każdym momencie wiedzieć na obiekt jakiego typu wskazuje dany pointer.
- Specjalnym typem wskaźników są stałe łańcuchowe (typu: "Ala ma kota"). Ponieważ w języku C nie występuje twór w postaci łańcucha znaków (ciągu znaków, napisu) więc tego typu stała traktowana jest Z DEFINICJI jako wskaźnik na pierwszy znak łańcucha (wskaźnik typu: char *). Każde użycie takiej stałej w kodzie źródłowym programu powoduje umieszczenie zawartego miedzy cudzysłowami ciągu znaków w pamięci, dopisanie na końcu znaku o kodzie równym zero (będącego w języku C znakiem końca łańcucha) i zastąpienie stałej wskaźnikiem do pierwszego znaku tego ciągu.
- Tablice jednowymiarowe deklarujemy w postaci:
typ_elementu_tablicy nazwa_tablicy[ ilość elementów ]; czy np. double vect[3];
oznacza to przedzielenie pamięci na trzy obiekty typu double i może być dodatkowo wzbogacone inicjalizacją w postaci np.:
double vect[3]={1.23,3.07,-12.253434}; - Sama nazwa tablicy pojawiająca się w programie jest interpretowana jako wskaźnik do pierwszego jej elementu, tak więc po powyższej deklaracji tablicy vect sama nazwa vect to to samo co &vect[0].
- Wskaźnikom nadajemy wartość albo wykorzystując operator & albo korzystając ze związków pointerów z tablicami. Można też korzystać z metod inicjowania wartości w czasie deklaracji, co jest często wykorzystywane w odniesieniu do łańcuchów znaków, np.:
char *p="Ala ma kota", w[]="a kot ma ogon";
Po tej deklaracji p jest pointerem typu char * wskazującym na literę 'A' w pamięci, natomiast w jest CZTERNASTOELEMENTOWĄ tablicą elementów typu char, którą równie dobrze można by zainicjować deklaracją:
char w[13]={'a',' ','k','o','t',' ','m','a',' ','o','g','o','n','\0'}; - Arytmetyka wskaźników oparta jest na fakcie znajomości rozmiaru obiektu wskazywanego. Jeżeli w jest typu double *, czyli wskazuje na obiekt typu double (współcześnie prawie zawsze ośmiobajtowy) to w+1 wskazuje NA NASTĘPNE OSIEM BAJTÓW , a w-1 na poprzednie osiem bajtów!
- Arytmetyka na wskaźnikach jest praktycznie tym samym co indeksowanie tablic, Z DEFINICJI każdy napis typu nazwa[liczba] jest zastępowany wyrażeniem *(nazwa+liczba). W praktyce oznacza to, że do drugiego elementu tablicy vect (pamiętamy, że w C tablice indeksujemy od zera) można odwołać się albo pisząc vect[1] albo *(vect+1). To drugie jest nawet szybsze bo nie wymaga konwersji.
- Tablica dwuwymiarowa JEST JEDNOWYMIAROWĄ TABLICĄ TABLIC JEDNOWYMIAROWYCH. Tablica trójwymiarowa jest jednowymiarową tablicą tablic dwuwymiarowych, itd. Z wszystkimi tego logicznymi konsekwencjami przy stosowaniu zasad z poprzednich punktów. Np. po deklaracji double z[2][4]={ {11.0,12.0,13.0,14.0},{21.0,22.0,23.0,24.0} }, napis z[1] jest nazwą drugiego wiersza tablicy z, jest więc interpretowany jako pointer wskazujący na pojedynczy obiekt typu double (pierwszy element tego wiersza - tablicy jednowymiarowej), czyli wskazuje na wartość 21.0 .
- W tablicach dwuwymiarowych dane są przechowywane w pamięci wierszami, pierwszy indeks mówi o który wiersz chodzi a drugi indeks wskazuje, o który element wiersza (którą kolumnę).
Podstawowe reguły przechowywania danych w tablicach jednowymiarowych ilustruje następujący przykład:
int main(void)
{
/* Poniższa instrukcja deklaruje i inicjuje pięcioelementową tablicę */
char a[5] = { 'a','b','c','d','e' };
/* niżej deklarujemy p jako wskaźnik do obiektu typu char */
char *p;
/* W zasięgu tych deklaracji poprawne są konstrukcje: */
p = a; /* p wskazuje na pierwszy element tablicy a[] */
/* nie wiemy ile wynosi p (zależy od systemu adresowania i kodowania adresów
danego komputera, ale prawdziwe będą wszystkie poniższe warunki: */
if( p == a ) puts("Test 1: true!");
if( p == &a[0]) puts("Test 2: true!");
if( *p == a[0]) puts("Test 3: true!");
if( *p == 'a') puts("Test 4: true!");
if( *p == *(&a[0]) ) puts("Test 5: true!");
if( *p == *a ) puts("Test 6: true!");
if( *(p+3) == a[3] ) puts("Test 7: true!");
if( *(p+3) == *(a+3) ) puts("Test 8: true!");
if( *(p+3) == 'd') puts("Test 9: true!");
return 0;
}
/* Jak ktoś nie wierzy proszę skopiować ten kod do pliku, skompilować i wykonać. */
A (całkowicie legalna lecz nieodpowiedzialna) próba wykonania po tych deklaracjach instrukcji
puts(p);
spowoduje wypisanie najpierw: abcde a za nimi bliżej nieokreślonej liczby dowolnych "krzaczków" aż procesor napotka w pamięci bajt o wartości zero lub wkroczy na cudzy obszar pamięci (wtedy "Naruszenie ochrony pamięci").
Jak ktoś nie zrozumiał czegoś z tekstu powyżej, proszę wrócić do podręcznika a potem przeczytać tę stronę ponownie od początku. Mam nadzieję, że to polecenie w niczyim przypadku nie wygeneruje pętli nieskończonej, ale dopóki trwa semestr można pytać, pytać, pytać...
A teraz przykład (soft core) dla tablicy dwuwymiarowej...
int main(void)
{
/* Poniższa linia to deklaracja i inicjalizacja dwuwymiarowej tablicy,
składającej się z dwóch wierszy po pięć elementów (tu liczb typu double) */
double b[2][5] = { {11.0,12.0,13.0,14.0,15.0 }, {21.0,22.0,23.0,24.0,25.0} };
/* niżej deklarujemy p jako wskaźnik do obiektu typu double */
double *p;
/* W zasięgu tych deklaracji poprawne są konstrukcje: */
p = b[1]; /* b[1] jest nazwą drugiego wiersza tablicy b
i jest zamieniane na wskaźnik do pierwszego
elementu tego wiersza, czyli p wskazuje na 21.0 */
/* nie wiemy ile numerycznie wynosi p (zależy od systemu adresowania i kodowania adresów
danego komputera, ale prawdziwe będą wszystkie poniższe warunki: */
if( p == &b[1][0] ) puts("Test 01: true!");
if( p == b[0]+5 ) puts("Test 02: true!");
if( *p == b[1][0]) puts("Test 03: true!");
if( *p == 21.0) puts("Test 04: true!");
if( *p == *(&b[1][0]) ) puts("Test 05: true!");
if( *p == *(b[1]) ) puts("Test 06: true!");
if( *(p-1) == b[0][4] ) puts("Test 07: true!");
if( *(p-1) == *(b[0]+4) ) puts("Test 08: true!");
if( p[3] == b[1][3] ) puts("Test 09: true!");
if( p[3] == 24.0) puts("Test 10: true!");
if( *(p+3) == b[1][3] ) puts("Test 11: true!");
if( *(p-3) == b[0][2] ) puts("Test 12: true!");
if( *p == *(b[1]) ) puts("Test 13: true!");
if( *(p-1) == *(*b+4)) puts("Test 14: true!");
return 0;
}
/* Jak ktoś nie wierzy proszę skopiować ten kod do pliku, skompilować i wykonać. */
Jak ktoś nie zrozumiał czegoś napisanego wyżej, proszę wrócić do podręcznika a potem przeczytać tę stronę od początku. Reszta jak wyżej .
Na koniec przykład hard core ...
int main(void)
{
int c[5] = {1,2,3,4,5};
/* skoro odwołanie c[2] jest Z DEFINICJI zamieniane na *(c+2)
a dodawanie jest przemienne... */
if( c[2] == 2[c]) puts("To prawda!");
return 0;
}
/* Jak ktoś nie wierzy proszę skopiować ten kod do pliku, skompilować i wykonać. */
Jak ktoś nie zrozumiał tego ostatniego przykładu, to nie szkodzi, mnie też kiedyś zaszokował