Pokazivač (programiranje)

Pokazivač (ponekad pointer, prema engl. pointer) predstavlja promjenljivu varijablu specijalnog tipa u nekim programskim jezicima poput C-a, C++-a, Paskala itd. Pokazivač ima svrhu da čuva adresu memorijske lokacije neke druge promjenljive, konstante ili pokazivača.

Osnovna svojstva pokazivača su:

  • pokazivač može mijenjati vrijednost (tj. može pokazivati na razne lokacije za vrijeme svog radnog vijeka)
  • preko njega se može dobiti vrijednost promjenljive čiju adresu čuva (na koju pokazuje)
  • preko njega se može mijenjati vrijednost promjenljive na koju pokazuje

Kada uzimamo ili mijenjamo vrijednost elementa na kojeg pokazivač pokazuje, kažemo da ga dereferenciramo.

Tipovi pokazivača uredi

U većini programskih jezika koji podržavaju pokazivače, pokazivači se dijele na tipske i bestipske.

Uz tipske pokazivače se veže informacija o tipu promjenljivih na koje će dati pokazivač moći pokazivati, odnosno čije će adrese moći čuvati. Kada deklarišemo tipski pokazivač, on do kraja svog vijeka ima isti tip.

Bestipski pokazivači nemaju određen tip promjenljive na koje će moći pokazivati, te mogu pokazivati na sve promjenljive podjednako. Na uštrb toga, međutim, postoje određena ograničenja vezana za ovu vrstu pokazivača:

  • nije dozvoljeno dereferenciranje (vidjeti poglavlje „Referenciranje i dereferenciranje“, niže)
  • nije dozvoljena pokazivačka aritmetika u jezicima u kojima je podržana za tipske pokazivače

Bestipski pokazivači se najčešće koriste u situacijama kada određena funkcija prihvata podatke promjenljivog tipa ili kada nam tip podatka nije unaprijed poznat.

Referenciranje i dereferenciranje uredi

Referenciranje je proces u kojem pokazivaču dodjeljujemo adresu određene memorijske lokacije. Nakon toga kažemo da pokazivač pokazuje na tu memorijsku lokaciju.

Dereferenciranje je proces u kojem preko pokazivača koji već pokazuje na neku memorijsku lokaciju pristupamo samoj lokaciji, bilo radi čitanja njene vrijednosti, bilo radi njenog mijenjanja. Zabranjeno je, i najčešće uzrokuje prekid rada programa, dereferenciranje pokazivača koji:

  • sadrži vrijednost koja služi za označavanje da pokazivač ni ne pokazuje ni na šta (najčešće vrijednost 0)
  • pokazuje na adresu koja se ne nalazi u memorijskom prostoru procesa koji izvršava program. To su najčešće pokazivači sa neinicijalizovanim vrijednostima, koji sadrže adresu koja se slučajno zatekla u prostoru zauzetom za dati pokazivač.

Pokazivačka aritmetika uredi

Pokazivačka aritmetika se odnosi na poseban način vršenja računskih operacija sabiranja i oduzimanja kada među operandima ima i pokazivača. Tada često ne važe uobičajena pravila pri računanju, tj. adrese koje pokazivači čuvaju neće se sabirati i oduzimati kao obični brojevi. Pokazivačka aritmetika za svrhu ima sofisticirano rukovanje pokazivačima kako bi se dobili određeni rezultati. Pomoću pokazivačke aritmetike, pokazivač se može kretati po određenim memorijskim lokacijama, mogu se mjeriti udaljenosti jedne memorijske lokacije od druge u zavisnosti od tipova pokazivača itd.

Pokazivači u programskom jeziku C uredi

Tipovi pokazivača uredi

U programskom jeziku C, pokazivači se dijele na tipske i bestipske.

Pored pokazivača na obične promjenljive, postoje i pokazivači na funkcije. Pokazivači na funkcije se mogu prosljeđivati drugim funkcijama kao argumenti i preko njih se mogu pozivati funkcije na koje pokazuju, što im je i primarna primjena.

Deklaracija uredi

Tipski pokazivači se deklarišu na sljedeći način:

tip_pokazivaca * ime_pokazivaca;

Prvo se navodi tip promjenljive na kakve će moći pokazivati pokazivač, zatim slijedi zvjezdica (*) i na kraju ime pokazivačke promjenljive terminirane tačka-zarezom. Za ime pokazivača važe uobičajena pravila imenovanja u p. j. C.

Bestipski pokazivači se deklarišu na sljedeći način:

void * ime_pokazivaca;

Umjesto tipa ovde se navodi ključna riječ void, nakon koje važe pravila kao za tipske pokazivače.

Pokazivači na funkcije se deklarišu na sljedeći način:

tip_rezultata_originalne_funkcije (*ime_pokazivaca)(tip_prvog_argumenta_funkcije, tip_drugog_argumenta_funkcije, ...);

Slijedi primjer za deklaraciju pokazivača na određenu funkciju:

/* deklaracija proizvoljne funkcije */
char * kopiraj(char * odrediste, char * izvoriste )
{
    /* blok funkcije */
}

/* deklaracija pokazivača na ovakvu funkciju (bez inicijalizacije) */
char * (*pkopiraj)(char * odrediste, char * izvoriste );

Referenciranje uredi

Da bismo dodijelili određenu vrijednost pokazivačkoj promjenljivoj, koristimo uobičajen operator dodjele:

p1 = <<adresa>>;

Pošto je svrha pokazivača da čuvaju adresu nekog drugog podatka, moramo da:

  • znamo unaprijed na kojoj tačno lokaciji se nalazi odgovarajuća promjenljiva
  • koristimo operator & da bismo dobili adresu date promjenljive
  • pozovemo neku od ugrađenih funkcija koje nam alociraju novu memoriju i vraćaju adresu alociranog prostora

Slijede primjeri za svaku od stavki, redom:

int * p1;
int a = 3;
p1 = 0x4CC7EC5C; /* izuzetno je opasno i ne preporučuje se dodjeljivati adrese direktno */
p1 = &a; /* koristimo operator & za dobijanje adrese od a da bismo je dodijelili promjenljivoj p1 */
p1 = malloc(sizeof(int) ); /* koristimo funkciju malloc da alociramo prostor veličine jedne cjelobrojne promjenljive. */
                               /* malloc vraća adresu novoalociranog prostora */

U slučajevima kada se želi pokazati da pokazivač ne čuva nikakvu adresu, tada mu dodjeljujemo nulu pomoću ugrađene konstante NULL:

int * p1 = NULL;

Kod pokazivača na funkcije važi nešto drugačija sintaksa referenciranja. Ako postoji funkcija f, i pokazivač pf deklarisan tako da može na nju da pokazuje, onda se referenciranje vrši jednostavnom dodjelom promjenljive f promjenljivoj pf, bez korišćenja običnih zagrada, kao kod samog poziva:

pf = f;

Dereferenciranje uredi

Dereferenciranje je proces u kojem preko pokazivača koji već pokazuje na neku memorijsku lokaciju pristupamo samoj lokaciji. Za ovu operaciju se koristi operator zvjezdice (*) ispred imena pokazivačke promjenljive:

int * p1;
int a;
p1 = &a; /* p1 neka pokazuje na a */
*p1 = 10; /* ono na šta p1 pokazuje (a), neka postane jednako 10 */

Pokazivači na funkcije se deferenciraju na nešto drugačiji način. Zapravo, kod njih se ne može govoriti o pravom dereferenciranju, nego radije samo o pozivanju funkcije preko pokazivača. Naime, onog trenutka kada je npr. pokazivač pf počeo da pokazuje na funkciju f, on je postao alias (lažno ime, drugo ime) za funkciju f, i koristi se ravnopravno, na isti način kao i sama funkcija. Slijedi primjer:

int saberi(int a, int b )
{
    return a + b;
}

int main()
{
    int (*psaberi)(int, int );
    int x = 3, y = 4;
    int z;
    psaberi = saberi;
    z = saberi(x, y );    /* poziv originalne funkcije */
    z = psaberi(x, y );   /* poziv funkcije preko pokazivača */
}

Slijedi primjer prosljeđivanja pokazivača na funkciju drugoj funkciji, koja je poziva preko tog pokazivača:

int saberi(int x, int y )
{
    return x + y;
}

int pozovi(int (*psaberi)(int, int), int x, int y )
{
    return psaberi(x, y );       /* pozivamo funkciju preko pokazivača, i prosljeđujemo joj preostala dva argumenta */
}

int main()
{
    int z;
    z = pozovi(saberi, 3, 4 );   /* prvi argument je samo ime funkcije, bez zagrada, a drugi su argumenti za sabiranje */
    return 0;
}

Pokazivačka aritmetika uredi

U pokazivačkoj aritmetici programskog jezika C ne važe uobičajena pravila pri računanju, tj. adrese koje pokazivači čuvaju neće se sabirati i oduzimati kao obični brojevi. Način računanja će zapravo zavisiti od toga koji je tip promjenljive na koju pokazivači pokazuju, odnosno koji je tip samih pokazivača. Sabiranje i oduzimanje pokazivača sa cijelim brojem se vrši po sljedećoj formuli:

p1 ± n = p1 ± (n * sizeof(*p1));

Pogledajmo sljedeći primjer:

double x = 3.14; /* pretpostavimo da se x nalazi na adresi 10000 */
double * px = &x; /* px dobija vrijednost 10000 */
printf("%d\n", px + 5 ); /* štampa se broj 10000 + 5*sizeof(double) = npr. 10000 + 5*8 = 10040 (ne 10005) */

Pokazivači se mogu i međusobno oduzimati, kada važe slična pravila kao kod pokazivača i cijelog broja.

Važe sljedeća restriktivna pravila vezana za pokazivače:

  • pokazivači se ne smiju množiti niti dijeliti cijelim brojem
  • pokazivači se ne smiju međusobno sabirati, množiti niti dijeliti

Primjena uredi

U programskom jeziku C, najvažnije primjene pokazivača su sljedeće:

  • prenos argumenata funkciji po referenci
  • implementacija nizova
  • implementacija dinamičkih struktura

Prenos argumenata funkciji po referenci uredi

U programskom jeziku C, funkcije su takve da dobijaju uvijek kopije argumenata koje joj prosljeđujemo, a nikad originalne argumente. Na ovaj način funkcija ne može imati spoljašnje efekte jer sve što dobija od funkcije-pozivaoca su kopije originalnih argumenata koji ostaju netaknuti. Ovakav način prenosa argumenata se popularno naziva prenos argumenata po vrijednosti, jer funkcija dobija praktično samo vrijednosti originalnih argumenata ali nikad i direktno njih same.

Problemi se javljaju, međutim, u slučajevima kada želimo da funkcija ipak izmijeni određene promjenljive koje joj proslijedimo kao argumente. Ta se potreba javlja npr. kada želimo funkciju koja će imati više rezultata, jer pomoću uobičajene ključne riječi return ona može vratiti samo jedan rezultat. Tada koristimo tzv. prenos argumenata po referenci koji u stvari predstavlja praksu da umjesto originalnog elementa proslijedimo njegovu adresu kao argument. Ova adresa predstavlja pokazivač na originalni element, pa iako se opet poštuje pravilo da funkcija dobija kopiju argumenta koji joj se proslijedi, ovaj put ona dobija kopiju adrese, što je ipak sasvim dovoljno da se originalnom argumentu priđe direktno, dereferenciranjem.

Pogledajmo sljedeći primjer:

void povecaj(int c )
{
    c = c+1;
}

int main()
{
    int x = 1;
    povecaj(x );
    printf("%d", x );
    return 0;
}

Iz prethodnog paragrafa zaključujemo da funkcija povecaj neće izvršiti svoju ulogu jer na mjesto promjenljive c dobija kopiju od x. Na taj način kopija od x biva povećana za 1, ali ne i originalni element x u funkciji main. Da bismo popravili kôd, moraćemo izvršiti par izmjena:

  • funkcija povecaj treba da se prilagodi tako da prihvata adresu cjelobrojne promjenljive, a ne nju samu
  • kada dobije adresu, dereferenciraće je i povećati ono što se nalazi na toj adresi za 1
  • funkcija main treba da pošalje adresu od x, a ne sāmo x

Pogledajmo kako izgleda rezultat izmjena:

void povecaj(int * pc )
{
    *pc = *pc + 1;     /* *pc predstavlja ono na šta pc pokazuje, tj. ono što se nalazi na adresi pc */
}

int main()
{
    int x = 1;
    povecaj(&x );      /* &x predstavlja adresu od x */
    printf("%d", x );
    return 0;
}

Koristeći prenos argumenata po referenci, programer takođe štedi na kopiranju argumenata pri pozivanju jedne funkcije iz druge. Ako je argument koji prenosimo velik podatak, npr. struktura od više elemenata čija ukupna veličina prevazilazi veličinu pokazivačke promjenljive na datom sistemu, dobro ga je proslijediti po referenci jer na taj način kopiramo manje podataka.

Implementacija nizova uredi

U programskom jeziku C ne postoje nizovi kao ugrađeni tipovi. Naprotiv, oni se implementiraju preko ostalih ugrađenih tipova i pokazivača. Ovo važi i za jednodimenzionalne i višedimenzionalne nizove (matrice, kocke itd.). Svaki niz i C-u se sastoji od dva fizička dijela - elemenata koji sačinjavaju niz i jednog pokazivača koji pokazuje na početak tog niza, tj. njegov prvi element (vidjeti sliku desno). Dakle, kada predstavimo niz na sljedeći način:

int a[10];

tada nastaje 10 elemenata niza (koji nijedan nema svoje pravo ime), i jedna pokazivačka promjenljiva a preko koje se pristupa svim elementima niza koristeći sintaksu a[0] (prvi element, jer u C-u indeksiranje kreće od nule, a ne od jedinice), a[1], a[2], itd.

Nizovi se u C-u implementiraju preko pokazivača zahvaljujući specifičnoj pokazivačkoj aritmetici. Ako je a pokazivač na prvi element niza, onda *a predstavlja sam element. Dalje, pošto pokazivačka aritmetika diktira da a+1 bude pokazivač na sljedeći element niza, onda će *(a+1) predstavljati sam drugi element niza. Idući dalje, primjećujemo da je a[n] ekvivalentno sa *(a+n). To je upravo ono što kompajler vidi kada putem operatora indeksa referenciramo određeni element niza.

Ako idemo dalje, i predstavimo matricu realnih brojeva M dimenzija m x n, tada je stanje u memoriji sljedeće:

  • M predstavlja pokazivač na prvi element niza od m pokazivača.
  • svaki od m pokazivača pokazuje na po prvi element niza od n realnih brojeva (vidjeti sliku desno).

Kada koristimo statičke deklaracije nizova, ovakva stanja memorije se formiraju automatski, ali je takođe moguće formirati ih ručno, koristeći običnu deklaraciju pokazivača i neku od ugrađenih C-ovih funkcija za alociranje memorije. Slijedi primjer:

char ** alocirajMatricu(int m, int n )
{
    char ** M;
    int i;
    M = malloc(m * sizeof(char *) );        /* alociramo memoriju za niz pokazivača */
    for (i = 0; i < m; i++ )                /* za svaki pokazivač u tom nizu alociramo po još jedan niz */
         M[i] = malloc(n * sizeof(char) );  /* karaktera da bismo formirali matricu. */
    return M;                                /* vraćamo pokazivač „na matricu“. */
}

int main()
{
    char ** M;
    int m, n;
    int i, j;
    scanf("%d %d", &m, &n );         /* pitamo korisnika kolika matrica je potrebna */
    M = alocirajMatricu(m, n );      /* pozivamo funkciju da nam alocira prostor te veličine */
    for (i = 0; i < m; i++ )         /* nadalje se ponašamo kao prema običnoj matrici */
         for (j = 0; j < n; j++ )
               M[i][j] = 'x';

    // ovako alociranu matricu moramo obrisati ručno
    for (i = 0; i < m; i++ )
         free(M[i] );
    free(M );
    return 0;
}

Implementacija dinamičkih struktura uredi

Poseban članak: Dinamičke strukture podataka

Uopšteno, dinamičke strukture podataka predstavljaju tip strukture koja zauzima tačno onoliko memorije koliko joj je neophodno, i može se širiti i smanjivati po potrebi. Najčešće spominjane dinamičke strukture su binarno stablo (ili binarno drvo), lista, stek, graf, red, i drugi. U programskom jeziku C one se implementiraju koristeći ceovske strukture (struct), pokazivače i dinamičko alociranje memorije pomoću neke od ugrađenih funkcija C-a.

Pokazivači u programskom jeziku Paskal uredi

Tipovi pokazivača uredi

U programskom jeziku Paskal, pokazivači se dijele na tipske i bestipske.

Paskal ne podržava pokazivače na funkcije.

Deklaracija uredi

Tipski pokazivači se deklarišu na sljedeći način:

ime_pokazivaca: ^tip_pokazivaca;

Prvo se navodi ime pokazivačke promjenljive i dvotačka (kao kod deklaracije ostalih elementarnih tipova promjenljivih u Paskalu), zatim slijedi kapica (^) i na kraju tip promjenljive na kakve će pokazivač moći pokazivati. Za ime pokazivača važe uobičajena pravila imenovanja u p. j. Paskal.

Bestipski pokazivači se deklarišu na sljedeći način:

ime_pokazivaca: pointer;

Umjesto tipa i kapice, ovde se navodi ključna riječ pointer.

Referenciranje uredi

Referenciranje je proces u kojem pokazivaču dodjeljujemo adresu određene memorijske lokacije. Da bismo dodijelili određenu vrijednost pokazivačkoj promjenljivoj, koristimo jedan od sledeća dva načina:

  • uobičajen operator dodjele
  • komandu new radi alokacije memorije i istovremene dodjele adrese te memorije pokazivaču

Slijede primjeri za obje stavke, redom:

var
p1: ^integer;
a: integer;
begin
p1 := @a; (* koristimo operator @ za dobijanje adrese od a da bismo je dodijelili promjenljivoj p1 *)
new(p1) (* koristimo komandu new da alociramo prostor veličine jedne cjelobrojne promjenljive (u skladu sa deklaracijom pokazivača) *)
end.

U slučajevima kada se želi pokazati da pokazivač ne čuva nikakvu adresu, tada mu dodjeljujemo nulu pomoću ugrađene konstante nil:

p := nil;

Dereferenciranje uredi

Za ovu operaciju se koristi operator kapice (^) iza imena pokazivačke promjenljive:

var
p1: ^integer;
a: integer;
begin
p1 := @a; (* p1 neka pokazuje na a *)
p1^ := 10; (* ono na šta p1 pokazuje (a), neka postane jednako 10 *)
end.

Pokazivačka aritmetika uredi

Pokazivačka aritmetika nije podržana u standardnom Paskalu. Postoje pojedine ekstenzije Paskala koje dozvoljavaju pokazivačku aritmetiku, kao što je GPC (Gnu-ova ekstenzija Paskala). Pokazivačka aritmetika Paskala je i tada jednostavna i ograničena. Kod nje važe uobičajena pravila pri računanju, tj. adrese koje pokazivači čuvaju će se sabirati i oduzimati kao obični brojevi, na taj način dodajući i oduzimajući tačno zadati broj bajtova od adrese koju pokazivač čuva.

Pogledajmo sljedeći primjer:

var
x: real; (* pretpostavimo da se x nalazi na adresi 10000 *)
px: ^real;
{$X+} (* korišćenje ekstenzije koja dozvoljava pokazivačku aritmetiku *)
begin
x := 3.14;
px := @x; (* px dobija vrijednost 10000 *)
px := px+5; (* px dobija vrednost 10005 *)
end.

Komande inc i dec služe da povećaju odnosno smanje vrijednost pokazivača za onoliko bajtova koliko zauzima promjenljiva na koju pokazuje. Dakle, komande inc i dec se vrše po sledećim formulama:

inc(p1) <=> p1 := p1 + sizeof(^p1);
dec(p1) <=> p1 := p1 - sizeof(^p1);

Ovo obezbjeđuje npr. iteriranje po nizu služeći se jednim pokazivačem.

Primjena uredi

U programskom jeziku Paskal, pokazivači nemaju preveliku primjenu, za razliku od programskog jezika C. Paskal sadrži niz kao ugrađeni tip (koristeći ključne riječi array ... of), prenošenje argumenta funkciji po referenci je obezbijeđeno sintaksom jezika (koristeći ključnu riječ var), te je jedina primjena pokazivača u Paskalu najčešće samo implementacija dinamičkih struktura podataka.

Pokazivači u ostalim programskim jezicima uredi

Ada uredi

Ada podržava samo tipske pokazivače i konverzija između pokazivača različitih tipova je podržana samo u određenom broju slučajeva. Svi pokazivači se podrazumijevano inicijaliziju na nulu (null), i dereferenciranje pokazivača sa tom vrijednošću izaziva izuzetak. U programskom jeziku Ada, pokazivači se nazivaju pristupni tipovi, i ne podržavaju pokazivačku aritmetiku, bez korišćenja ekstenzija (System.Storage_Elements)

C++ uredi

C++, kao jezik nastao na osnovu C-a, podržava cjelokupnu sintaksu C-a vezanu za pokazivače.

C# uredi

U programskom jeziku C#, pokazivači su podržani ali samo pod određenim uslovima: bilo koji blok koda koji sadrži pokazivače mora biti označen kao „nesiguran“, koristeći ključnu riječ unsafe. Programi ili moduli koji sadrže takve blokove obično zahtijevaju više nivoe dozvola od strane korisnika da bi se pokrenuli. Sintaksa je jako slična sintaksi programskog jezika C.

Fortran uredi

Fortran podržava pokazivače od verzije 90. Pokazivači u Fortranu, međutim, predstavljaju složene tipove podataka, koji pored adrese ciljne promjenljive takođe sadrže i donju i gornju granicu niza (ukoliko pokazivač služi niz), podatke o sekcijama niza i druge pomoćne podatke.

Vidi još uredi

Spoljašnje veze uredi