Licencja Creative Commons

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, do punktu 5.9 włącznie).

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 (np. 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 przydzielenie 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 (zwróć uwagę na ostatni element) 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:

#include<stdio.h>

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...

#include<stdio.h>

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 frowning smiley .

Na koniec przykład hard core ...

#include<stdio.h>

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ł cool smiley


Licencja Creative Commons