Escolar Documentos
Profissional Documentos
Cultura Documentos
Akademska 2014/2015
Enil Paji
OSNOVE RAUNARSTVA
Analiza tutorijala br. 9
(sa povrnim uvodom u dinamiku alokaciju memorije)
Pokazivake varijable nisu nita drugo nego varijable koje sade adresu neke druge varijable.
To je specijalna vrsta varijabli nad kojima moemo primijeniti odreen broj operatora, meu
njim i operator dereferenciranja kojeg emo kasnije objasniti. Pokazivaka varijabla (u
nastavku samo pokaziva) sadri adresu nekog drugog objekta, tada kaemo da ona pokazuje
na taj objekat. Ve smo spominjali na koji nain su varijable smjetene u memoriji i tom
prilikom smo objasnili da je svaka varijabla u memoriji smjetena na odreenoj (unikatnoj)
adresi. Pokazivai kao svoj sadraj (odnosno vrijednost) imaju adresu na kojoj je smjetena
neka varijabla. Zbog navedenog, mi moemo indirektno pristupiti toj varijabli, jer znamo
adresu na kojoj je ona smjetena. Pokazivai su jedini nain u jeziku C da ostvarimo
indirektni pristup drugim varijablama. Ta mogunost indirektnog pristupa predstavlja veliku
fleksibilnost u to emo se uvjeriti kasnije.
Moemo imati pokazivae na sve varijable koje moemo deklarirati u jeziku C, to znai da i
pokazivai na neki nain imaju svoj tip. Znai, moemo imati pokaziva na varijablu tipa
int, na varijablu tipa double, na niz (vidjeemo kasnije da je to pokaziva na prvi elemenat
niza, ali emo takoer vidjeti da moemo imati i pokaziva na niz kao cjelinu). Mogue je
imati pokaziva na varijablu koja je takoer pokaziva. Sve navedeno emo objasniti neto
kasnije.
Pokaimo sada kako bismo deklarirali pokaziva odnosno pokazivaku varijablu. Sintaksa je
slina deklaraciji obine varijable, navodimo tip pa ime. Razlika je da kod deklaracije
pokazivaa dodajemo znak zvijezda * prije imena varijable. Pokaimo generalnu sintaksu
deklaracija pokazivake varijable:
TIP *pokazivac;
Kao to vidimo, ovdje varijabla pokazivac nije tipa TIP nego pokaziva na TIP. Prikaimo
primjer konkretne deklaracije pokazivakih varijabli. Varijable pok1, pok2, pok3, pok4, pok5
redom predstavljaju pokazivae na tipove int, char, double, float, unsigned int:
int *pok1;
char *pok2;
double *pok3;
float *pok4;
unsigned int *pok5;
Naime, komapajler za jezik C e ovo dozvoliti, ali e nas upozoriti da takvo neto vjerovatno
predstavlja greku: C:\....\main.c|6|warning: initialization makes pointer from integer
without a cast [enabled by default]|. Dok kod kompajlera za jezik C++ ovakva dodjela
uopte nije mogua i bie javljena greka! Znai, nikada ne bismo trebali raditi ovakve
dodjele (osim eventualno dodjele NULL konstante ije emo znaenje objasniti kasnije).
Znai, pokaziva smije sadravati samo adrese i to adrese varijabli koje su deklarisane u
programu (eventualno i adresu dinamiki alocirane memorije to emo spomenuti na kraju
dokumenta). Pokazivau treba uvijek dodjeljivati adresu uz pomo adresnog operatora &
kojeg emo objasniti u nastavku.
Da bismo inicijalizirali (ili izvrili dodjelu) pokaziva moramo imati ili ve postojeu
varijablu koja se nalazi na nekoj adresi ili dinamiki zauzeti dio memorije to nije tema ove
analize ali emo na kraju i to spomenuti. Primjer inicijalizacije pokazivaa adresom ve
postojee varijable:
int var = 100;
int *pok = &var;
Ovjde moemo uoiti i prisustvo adresnog operatora (&) kojeg smo primijenili na varijablu
var. Adresni operator ima znaenje adresa od i njime mi dobivamo adresu neke varijable.
Ve smo se sa ovim operatorom susretali kod pozivanja scanf funkcije gdje smo prosljeivali
adrese varijabli (u cilju da ih scanf funkcija izmijeni, kasnije emo i mi pisati funkcije koje
mijenjaju varijable na proslijeenim adresama). Ovaj operator se jo naziva i operator
referenciranja sa kojim mi varijablu veemo za pokaziva. Adresni operator moemo
primijeniti jedino na varijable (odnosno l-vrijednosti koje smo spominjali), tako da je
besmisleno pisati &100 ili &(var + 7).
Nakon to smo povezali pokaziva i varijablu (tj. na to on pokazuje), mi sada moemo
indirektno pristupati varijabli. Ovo dvoje ispod je isto:
var = 250;
/* Isto kao i ovo: */
*pok = 250;
Naravno, ovo vrijedi samo za dereferencirani pokaziva (koji predstavlja varijablu), dok nema
smisla pisati npr. sljedee:
int broj = pok * 4 - 10;
Konstrukcija iznad se nee moi ni kompajlirati jer nije definisano mnoenje pokazivaa, to
emo vidjeti kasnije. Ipak, da smo dereferencirali pokaziva, izraz iznad bi bio legalan, npr.
da smo napisali sljedee: int broj = *pok * 4 - 10;.
Ovdje je zvjezdica * kod pokazivaa napisana podebljano u cilju isticanja da se radi o
unarnom operatoru dereferenciranja a ne binarnom operatoru mnoenja.
Treba napomenuti veliku razliku izmeu deklaracije pokazivaa i njegovog koritenja
(dereferenciranja). Naime, kada mi deklariramo pokaziva, npr. int *pok , tada se znak
zvijezda * koristi da bismo naglasili da se radi o pokazivakoj a ne obinoj varijabli. Prilikom
dereferenciranja, npr. *pok = 10, znak zvjezdica * ima posve drugo znaenje, i predstavlja
indirektan pristup pokazivanom (pointee) objektu. Vano je da studenti ne mijeaju ovo
dvoje.
Sada da se dotaknemo i stila pisanja deklaracij pokazivakih promjenljivih. Naime, kako
kompajler za jezik C (kao i C++) ignorira sve razmake koji ne predstavljaju osnovne razmake
(koji tu moraju biti, npr. prilikom pisanja deklariranja varijable: double b, razmak je
neophodan, jer ukoliko ga uklonimo dobili bismo rijei doubleb koja nee biti prepoznata) mi
moemo pisati, naizgled, razliite vrste konstrukcija. Pogledajmo sljedee:
int* pok1, pok2, pok3;
Kojeg tipa su varijable pok1, pok2 i pok3? Studenski odgovor na ovo pitanje je bio da su sve
varijable pokazivai. To nije istina, jer je ovdje samo varijabla pok1 pokaziva na tip int tj.
tipa int *, dok su varijable pok2 i pok3 tipa int. Isjeak iznad je ekvivalentan sljedeem:
int *pok1;
int pok2, pok3;
Zbog toga mi preporuujemo da se koristi stil pisanja kojeg smo do sada ve koristili.
Pogledajmo sada sljedeu konstrukciju.
int *pok1, pok2, pok3;
Varijable pok1, pok2 i pok4 su pokazivai (tj. tipa int *), dok su varijable pok3 i pok5 obine
varijable cjelobrojnog tipa (tj. tipa int).
Sve nekonstantne varijable za vrijeme svoga ivota mogu mijenjati svoj sadraj, pa tako i
pokazivai. Oni mogu prvo pokazivati na jednu varijablu pa kasnije na drugu. Pogledajmo
primjer:
int x = 10, y = 15, *pok;
pok = &x;
printf ("*pok = %i\n", *pok); /* Ispisuje 10 */
pok = &y;
printf ("*pok = %i\n", *pok); /* Ispisuje 15 */
Varijable x i y su tipa int, dok je varijabla pok tipa pokaziva na int, tj. tipa int *. Prvo smo
pokazivau dodijelili adresu varijable x pa smo ispisali njenu vrijednost preko *pok (to je
isto kao i x). Nakon toga smo pokazivau dodijelili adresu varijable y te ispisali njenu
vrijednost.
Ve smo ranije spominjali da u jeziku C postoji samo jedan nain da funkcija promijeni
vrijednost argumenta kojeg smo proslijedili. Taj nain je sljedei: funkciji emo kao
argumente proslijediti adresu varijable, te emo na taj nain indirektno pristupiti varijabli i
izmijeniti je.
Primjer 1: Napisati funkciju koja e vriti zamjenu proslijeenih joj realnih argumenata.
Testirati napisanu funkciju.
Napiimo sada funkciju Zamijeni koja e vriti zamjenu dvije varijable, tj. zamjenu njihovih
vrijednosti. Funkcija treba imati takve parametre da oni mogu prihvatiti adrese varijabli, a ve
smo ranije rekli da se adrese varijabli mogu smjestiti u pokazivae. Znai funkcija treba kao
parametre imati pokazivae na neki tip. Ona e imati parametre tipa double * jer je u postavci
reeno da se vri zamjena vrijednosti dvije realne varijable. Pri rjeavanju emo funkciju
napisati nakon main funkcije, kako bismo se ponovo podsjetili na pisanje prototipa:
5
Funkcija Zamijeni nije trebala da vraa nita, pa smo joj kao povratni tip stavili tip void.
Prilikom njenog pozivanja proslijedili smo adrese od varijabli koje trebamo zamijeniti. Unutar
funkcije smo koristili standardnu proceduru zamjene dvije varijable. Poto su parametri
funkcije pok_br1 i pok_br2 pokazivai, da bismo pristupili onome emu oni pokazuju morali
smo ih dereferencirati.
Podsjetimo se jo zato nismo navodili imena parametara u prototipu funkcije. Naime, kako
smo ve ranije naglasili, imena parametara u prototipu funkcije nam ne koriste pa ih moemo
izostaviti. Naravno, tipovi tih parametara se nisu smjeli izostaviti. Iz samog prototipa funkcije
moemo vidjeti da je double * tip, znai tip parametra nije double nego double *, tj. zvjezdica
je sastavni dio tipa. Ovo e biti vano kasnije kada budemo spominjali konverziju
pokazivaa.
Nakon kompajliranja i pokretanja programa, dobiti emo neto ovako:
Prikaimo sada i pogrean nain pisanja Zamijeni funkcije, kojeg napiu manje iskusni
studenti koji nisu u potpunosti razumjeli razliku izmeu pokazivaa i onoga na to on
pokazuje.
void Zamijeni (double *pok_br1, double *pok_br2)
{
double *temp = pok_br1; /* POGRESNO! */
pok_br1 = pok_br2;
Ukoliko, sa istom main funkcijom kao kod ispravnog primjera, ovo kompajliramo i
pokrenemo, dobiemo ovakav izlaz:
Program e kompajlirati, ali nee raditi ispravno. Napravljena je jedna podmukla greka gdje
smo mi u pok_br1 smjestili vrijednost pok_br2 (zaokruena linija), tj. manipulirali smo
vrijednostima pokazivaa a ne vrijednostima onoga na to oni pokazuju. Primjer bi radio da
smo u zakruenoj liniji pisali ovo: *pok_br1 = *pok_br2;.
Sada varijabla pok pokazuje na poetak niza (kae se pokazuje na niz) te sljedea konstrukcija
e izmijeniti prvi elemenat niza (tj. elemenat sa indeksom 0):
*pok = 10;
...
...
1Byte
1Byte
0x100
0x104
0x108
0x112
0x116
0x120
...
...
1Byte
Prvi elemenat neka je na fiktivnoj adresi 0x100, sljedei elemenat e biti na adresi 0x104
poto na veini kompajlera i platformi tip int zauzima 32 bita, odnosno 4 bajta. Znai, prvi
elemenat niza je smjeten na adresama 0x100, 0x101, 0x102 i 0x103.
Sa ove strane pokazivai su jako fleksibilni, jer pok + 1 e pokazivati na sljedei elemenat
niza (tj. na adresu 0x104) a ne na adresu 0x101. To pokaziva zna jer smo mu specificirali
tip (stavili smo da je pok pokaziva na tip int) i on e, kada ga uveamo, pokazivati na
sljedei elemenat niza. Gotovo nikada poveanje pokazivaa za 1 ne znai i poveanje adrese
za jedan, nego za onoliko bajta koliko zauzima tip pokazivaa.
Zbog toga je zabranjeno vriti dodjelu pokazivaa jednog tipa pokazivau drugog tipa, tj.
ukoliko imamo kd poput sljedeeg:
int x = 10;
double y = 20.4;
int *pint = &x;
double *pdouble = &y;
pint = pdouble; /* OVO NE SMIJEMO RADITI! */
Naravno , ovo treba razlikovati od dodjele *pint = *pdouble koja je potpuno legalna isto
kao i dodjela x = y.
Zapravo, indeksiranje niza (niz[i]) nije nita drugo nego pisanje *(niz + i). Poto je
sabiranje komutativno, *(niz + i) je isto to i *(i + niz), pa onda moemo pisati jako
udnu konstrukciju koja e raditi, ali je neitljiva i treba je izbjegavati: i[niz], konkretno za
pristup 4. elementu to je 3[niz]. Jedan mali primjer:
#include <stdio.h>
int main ()
{
int niz[] = {1, 5, 7};
printf ("2[niz] = %d", 2[niz]);
return 0;
}
Sada ove pokazivae moemo indeksirati, kao i obine nizove: pok2[1] = 100.
Kada smo deklarirali funkcije koje prihvataju nizove kao parametre, pisali smo neto poput
sljedeeg:
void Funkcija (int niz[], int vel);
Pri tome smo spomenuli da nismo slali kopiju niza funkciji, nego se svaka izmjena niza
unutar funkcije odraavala i na originalni niz. Sada moemo detaljnije objasniti zato se to
deavalo. Mi smo funkciji zapravo prosljeivali pokaziva na prvi elemenat niza te smo ga
indeksirali (vidjeli smo da je i to mogue) i direktno mijenjali njegov sadraj. Moda malo
zvui udno pokaziva na prvi elemenat niza a mi proslijedili int niz[], ali, to dvoje je
isto u parametrima funkcije. Iznad napisani prototip je potpuno ekvivalentan sljedeem:
void Funkcija (int *niz, int vel);
Sada stvari postaju jasnije, sada nam je jasno zato smo kao argument funkcije prilikom
njenog pozivanja prosljeivali samo ime niza, tj. pisali smo sljedee:
Funkcija (niz, 7);
Odnosno sljedee (znamo da je ime niza upotrijebljeno samo za sebe zapravo pokaziva na
prvi elemenat toga niza):
Funkcija (&niz[0], 7);
10
Primjer 3: Napisati funkciju Ispisi koja prima dva pokazivaa na tip int. Prvi pokaziva
predstavlja poetak bloka niza kojeg treba ispisivati, a drugi pokaziva pokazuje na kraj toga
bloka. Funkciju testirati tako to ete u kdu deklarirati niz od 16 elemenata te pozivom
funkcije:
1. Ispisati prvu njegovu polovinu
2. Ispisati zadnjih 5 elemenata toga niza
Vano je da analiziramo neke dijelove ovoga kda. Funkcija Ispisi prihvata dva pokazivaa
koji e predstavljati pokazivae na poetak i kraj nekog bloka niza. Unutar funkcije, u liniji
oznaenoj sa (1), imamo uslov while petlje poc <= kraj. Ovo ne znai da je ono na to
pokazuje poc manje ili jednako od onoga na to pokazuje kraj. Navedeno moemo itati
dokle god je adresa koju sadri poc manja ili jednaka adresi koju sadri kraj. Uslov e biti
taan dokle god je prvi pokaziva manji ili jednak drugom pokazivau (pok pokaziva
inkrementiramo unutar petlje). Znamo da su adrese u memoriji kontinualne i da se elementi
11
Iz primjera iznad moemo vidjeti da su varijable u memoriji smjetene jedna iza druge.
Varijabla pok bi mogla da pokazuje na varijablu var2, ali to nije garantovano i ne smijemo se
na to osloniti!
Takoer pokaimo jedan primjer viseeg pokazivaa koji e pokazivati na varijablu koja je
umrla, tj. prestala da postoji. Inae, primjer viseih pokazivaa emo objasniti kasnije.
int *pok;
{
int var = 10; /* Lokalna varijabla! */
pok = &var; /* U pok smjestamo adresu lokalne varijable... */
} /* <--- ovdje varijabla 'var' umire! */
/* Ovdje je pokazivac 'pok' viseci, tj. NIJE VALIDAN! */
int nesto = 1;
/* pok vjerovatno pokazuje na 'nesto' varijablu ali to nije garantovano! */
Zbog toga trebamo biti jako paljivi prilikom koritenja pokazivaa. Jo eventualnih problema
zbog neopreznog koritenja pokazivaa emo vidjeti kasnije.
U rjeenju iznad smo mogli primijetiti razliite naine prosljeivanja argumenata funkciji
Ispisi, pa emo sada rei neto i o njima. Poziv sa oznakom (2) je klasian, prosljeujemo
adresu prvog elementa i adresu 8. elementa (to je prva polovina niza). Oznaka (3) nam otkriva
jedan novi nain pozivanja, koji je u biti isti kao i prethodni. Kao to smo pisali pok + i,
ovdje piemo niz + i, prosljeujui pokaziva na i-ti elemenat.
Operator referenciranja, odnosno adresni operator (&) i operator dereferenciranja (*) su
meusobno suprotni, pa ukoliko imamo *&broj, dobivamo samo broj.
Nakon kompajliranja i pokretanja kda iznad, dobivamo sljedee:
12
Primjer 4: Napisati funkciju Ispisi koja prihvata pokaziva na prvi elemenat niza cjelobrojnih
elemenata i koja treba ispisati elemente toga niza unutar vitiastih zagrada, npr. {1, 2, 3}.
Zabranjeno je indeksiranje proslijeenog niza odnosno pokazivaa naredbama niz[i]
odnosno *(niz + i). Funkciju napisati nakon main funkcije, a njen prototip neka bude
lokalan za main funkciju.
#include <stdio.h>
int main ()
{
int niz[] = {1, 2, 3, 4, 5};
void Ispisi (int *, int); /* LOKALNI PROTOTIP */
Ispisi(niz, 5);
return 0;
}
void Ispisi (int *niz, int vel)
{
printf ("{");
int a = 0;
for (; a < vel - 1; ++a)
printf ("%i, ", *niz++); /* (1) */
if (vel > 0)
printf ("%i", *niz);
printf ("}");
}
Poto nam je bilo zabranjeno koristiti indeksiranje niza, morali smo koristiti pokazivaku
aritmetiku i dereferenciranje hodajueg pokazivaa. Ono to je zanimljivo jeste linija kda
koju smo oznaili sa (1), gdje postoji naredba *niz++. Naime, vidjeli smo (u dokumentu
Operatori u jeziku C, dio 1) da postfiksni operator inkrementiranja (p++) ima vei prioritet
od operatora dereferenciranja (*p), pa se taj izraz tretirao kao *(niz++). Kako znamo da
naredba x = y++ znai isto to sljedee dvije naredbe x = y; y++; tako je navedena
naredba znaila *niz; niz++, odnosno ispisivali smo ono na to niz pokazuje te nakon toga
inkrementirali pokaziva. Postoje jo tri naredbe koje kombiniraju operator dereferenciranja i
postfiksne odnosno prefiksne operatore inkrementiranja i dekremeniranja. Preostale
konstrukcije su (*p)++ (uveava postfiksno ono na ta p pokazuje), ++*p (uveava prefiksno
ono na ta p pokazuje) i *++p (dereferencira ve uveani pokaziva p). Naravno, isto vrijedi i
za operator dekrementiranja - npr. --*p itd. Njihovo znaenje je najlake odrediti ako
pogledamo prioritet i asocijativnost operatora. Vie o ovome e biti reeno u drugom dijelu
spomenutog dokumenta, tj. dokumenta Operatori u jeziku C (dio 2).
13
Kako su pokazivai zapravo tipovi u jeziku C, to ih funkcija moe i vraati tj. povratni tip
funkcije moe biti i pokaziva. Meutim, postoje odreene opasnosti kada funkcija vraa
pokaziva, na to emo ukazati kasnije, nakon primjera.
Primjer 5: Napisati funkciju Pomnozi koja kao parametre ima niz i broj. Parametar niz
predstavlja pokaziva na prvi elemenat niza cjelobrojnih elemenata, dok parametar broj
predstavlja broj sa kojim treba pomnoiti svaki elemenat niza. Iskoristiti napisanu Ispisi
funkciju za ispisivanje niza. Omoguiti ulanano povezivanje funkcija, tako da je mogue
pisati ovo: Ispisi (Pomnozi (niz, 10, 3), 10);
1. #include <stdio.h>
2. /* Funkcija vraca pokazivac na int */
3. int *Pomnozi (int *niz, int vel, int broj)
4.
{
5.
int *pocetak = niz, a; /* (1) */
6.
for (a = 0; a < vel; ++a, ++niz) /* (2) */
7.
*niz *= broj; /* Isto kao: *niz = *niz * broj; */
8.
return pocetak; /* (3) */
9.
}
10. int main ()
11.
{
12.
int niz[] = {1, 2, 3, 4, 5};
13.
void Ispisi (int *, int); /* LOKALNI PROTOTIP */
14.
Ispisi(Pomnozi (niz, 5, 100), 5); /* (4) */
15.
return 0;
16.
}
Analizirajmo iznad napisano rjeenje. Funkcija Ispisi ovdje nije prikazana radi utede
prostora, dok funkcija Pomnozi prihvata pokaziva na prvi elemenat niza, njegovu veliinu i
broj sa kojim treba pomnoiti svaki elemenat niza. Ona vraa pokaziva kako bismo mogli
ulanano pozivati funkcije (poziv kakav je napisan u 14. liniji gdje smo funkciji Ispisi
proslijedili kao argument rezultat funkcije Pomnozi). U liniji 5, oznaenoj sa (1), smo
deklarirali pokaziva pocetak i inicijalizirali ga sa vrijednosti koju je sadravao pokaziva niz.
Na ovaj nain smo u varijabli pocetak zapamtili adresu prvog elementa niza. Razlog za ovo je
bio to smo u nastavku funkcije mijenjali pokaziva niz (u zaglavlju for petlje) a funkcija je
trebala da vrati pokaziva na poetak niza. Isti efekat smo mogli postii da smo deklarirali
pomonu varijablu (npr. sa imenom pomocna) koju bismo koristili za hodanje kroz niz (npr.
14
15
Ukoliko sada pokuamo kompajlirati program, dobiti emo sljedee upozorenje (koje u
velikoj veini sluajeva predstavlja greku): C:\...main.c|6|warning: function returns
address of local variable [enabled by default]|. Ovdje je i kompajler bio u mogunosti
prepoznati vjerovatnu greku vraanja adrese lokalne varijable.
Sada da objasnimo jo jednu, naizgled udnu, konstrukciju prilikom indeksiranja pokazivaa
na neki elemenat niza: pok[-1]. Ovo moemo uraditi kada pokaziva pok pokazuje na neki
elemenat niza koji nije prvi. Npr. ako imamo sljedee:
int niz[] = {1, 2, 3, 4, 5, 6, 7};
/* 'pok' pokazuje na 4. elemenat niza, tj. broj 4 */
int *pok = niz + 3; /* Isto kao: int *pok = &niz[3] */
pok[-1] = 10; /* pok[-1] je isto to i niz[2] */
pok[-3] = 100; /* pok[-3] je isto to i niz[0] */
*(pok - 2) = 100; /* *(pok - 2) je isto to i niz[1] */
Naravno, ovo ima smisla jedino ukoliko pok[-indeks] ne izlazi izvan opsega niza.
Vidjeli smo takoer da ako deklariramo pokaziva koji pokazuje na poetak niza, taj
pokaziva kasnije moemo mijenjati:
int niz[] = {1, 2, 3, 4, 5};
int *pok = niz;
/* Mozemo pisati ovo: */
pok++;
/* Ali NE MOZEMO ovo: */
niz++;
Kao to vidimo, niz++ naredba e javiti greku. Ime niza, upotrijebljeno samo za sebe,
predstavlja pokaziva na prvi elemenat toga niza. Ali to nije obini, nego konstantni
pokaziva (tj. pokaziva iju vrijednost ne moemo mijenjati, ali moemo mijenjati vrijednost
onoga na to on pokazuje). Ako bolje razmislimo shvatiemo da je tako moralo biti, jer
ukoliko bismo imali mogunost pisanja naredbe niz++ (ili bilo kojeg drugog izraza koji bi
promijenio vrijednost pokazivaa niz) izgubili bismo trajno adresu poetka niza, to je
naravno nedopustivo.
U prethodnom paragrafu smo spomenuli pojam konstantnog pokazivaa. To je pokaziva iju
vrijednost ne moemo mijenjati (ali i dalje moemo mijenjati vrijednost onoga na to on
pokazuje). Takvi pokazivai mogu imati primjene gdje mi ne elimo da moemo mijenjati
njihove vrijednosti. Deklaracija takvog pokazivaa bi bila sljedea:
16
Ipak, zbog nekih tehnikih razloga, za nul-pokaziva je uvedena i makro konstanta NULL
koja ima vrijednost nula. Uz pomo spomenute makro konstante pisali bismo sljedee:
int *pok = NULL;
Konstanta NULL je vjerovatno definisana na sljedei nain: #define NULL (void *)0 i
njena definicija se moe nai u nekoliko standardnih biblioteka (ukljuujui i biblioteku
<stdlib.h>).
U paragrafu iznad smo mogli vidjeti dvije stvari, void pokazivae tj. pokazivae tipa void * i
pretvorbu odnosno konverziju (engl. cast) pokazivaa. Ovo emo objasniti pred kraj ovoga
dokumenta.
Spomenimo sada viestruke pokazivae. Ako pokaziva pokazuje na pokaziva, takav
pokaziva se naziva dvostruki pokaziva. Na isti nain moemo definisati i viestruke
pokazivae. Ako pokaziva pokazuje na pokaziva, da bismo izmijenili ono na to on
pokazuje (tj. pokaziva), potrebno je izvriti jednostruko dereferenciranje (npr. *pok).
Ukoliko pak elimo uz pomo dvostrukog pokazivaa izmijeniti ono na to pokazuje
pokaziva na kojeg on pokazuje, tada nam je potrebno dvostruko dereferenciranje (npr.
**pok). Pokaimo na primjeru:
int broj = 10;
int *p = &broj; /* Jednostruki pokazivac */
int **pp = &pbroj; /* Dvostruki pokazivac */
*p = 20; /* Postavlja vrijednost varijable 'broj' na 20 */
**pp = 50; /* Postavlja vrijednost varijable *p tj. 'broj' na 50 */
/*
*pp = 100;
Ovo bi postavilo vrijednost pokazivaca p na 100, tj. on vise ne bi
pokazivao na varijablu 'broj' nego na nesto sto se nalazi na adresi 100.
Ima isti efekat kao p = 100.
*/
17
Vidjeli smo nain pristupanja elementima matrice preko sljedee naredbe koja se nalazi u
zaokruenoj liniji *(*(mat + i) + j)). Podsjetimo se jo da e ostali elementi matrice
(oni koje nismo naveli prilikom inicijalizacije) biti nule.
Primjer 7: Prepraviti prethodni primjer tako da ispisuje elemente matrice, ali iskljuivo
koritenjem pokazivake aritmetike, dakle nije dozvoljeno indeksiranje niti uglastim
zagradama niti njegovim sinonimom *(pok + i).
#include <stdio.h>
void IspisiMatricu (int (*mat)[4], int n, int m) /* (1) */
{
int i, j;
for (i = 0; i < n; ++i)
{
int *pok = *mat; /* (2) */
for (j = 0; j < m; ++j)
printf ("%3i", *pok++);
printf ("\n");
mat++; /* Prelazimo u sljedeci red matrice (3) */
}
18
Analizirajmo ovo rjeenje. Stavili smo da funkcija prihvata pokaziva na niz kao cjelinu. Isti
efekat bismo postigli da smo stavili kao i u prethodnom rjeenju, tj. (int mat[][4]), ali
ovaj put smo stavili ovo drugo da znamo ta ono u funkcijskom parametru predstavlja. Isto
kao to znamo da smo funkciji za niz prosljeivali obini pokaziva (int *pok), znai
prosljeivali smo adresu prvog elementa niza, tako i za matricu prosljeujemo adresu prvog
njenog elementa a to je cijeli niz! Mi njoj trebamo proslijediti adresu cijelog niza odnosno
niza kao cjeline. Poto nismo smjeli koristiti nikakvo indeksiranje, ispisivanje smo radili samo
sa pokazivaima. U liniji sa oznakom (2) smo deklarirali obini pokaziva koji e pokazivati
na prvi elemenat reda matrice, inicijalizirali smo ga vrijednosti sa *mat. Ispisivali smo
vrijednost *pok i odmah ga postfiskno inkrementirali, kao i u jednom od ranijih primjera. Na
kraju smo poveavali pokaziva na niz kao cjelinu sa mat++. Isto kao to pok++ prelazi na
sljedei elemenat niza, tako i mat++ prelazi na sljedei elemenat matrice, tj. prelazi na novi
red. Prilikom pozivanja funkcije mogli smo doslovno proslijediti matrica varijablu, ali smo
namjerno deklarirali pokaziva na niz kao cjelinu da vidimo kako se i to radi. Morali smo
staviti ime pokazivaa unutar zagrada jer bi prosta konstrukcija int *pok[4] deklarirala niz
pokazivaa, tj. niz iji su elementi pokazivai na int. Funkciji nismo mogli proslijediti ni
dvostruki pokaziva, tj. int **mat, jer smo vidjeli da je prvi elemenat matrice niz kao
cjelina pa smo trebali na njega da proslijedimo pokaziva.
Za one koji ele znati vie slijedi nekoliko reenica o void pokazivaima te dinamikoj
alokaciji memorije u jeziku C; svakako se preporuuje da svi ovo barem proitaju a oni
zainteresirani i detaljno analiziraju.
Objasnimo void pokazivae (spomenute prilikom opisivanja naina na koji je vjerovatno
definisana makro naredba NULL). U prethodnoj analizi smo spomenuli da varijablu ne
moemo deklarirati da bude tipa void ali smo takoer spomenuli da je ipak mogue deklarirati
varijablu koja je pokaziva na tip void. Takav pokaziva se naziva generikim pokazivaom, i
on ima sljedee znaenje: ne zna se koji je tip na koji on pokazuje, ali moe pokazivati na bilo
koji tip. Takav pokaziva, tj. pokaziva tipa void * se ne moe dereferencirati, ne moe se nad
njim vriti nikakva aritmetika i on je neupotrebljiv sve dok se nad njim ne izvri konverzija
(koju emo opisati u nastavku).
Konverzija (engl. cast) je pretvaranje jednog tipa u drugi tip. Ve smo se susretali sa
konverzijama obinih tipova npr. (double)brojac i sl. Konverzija izmeu pokazivaa je
slina, pretvaramo pokaziva jednog tipa u pokaziva drugog tipa. ak pretvorba ne mora biti
19
Ono to se moe primijetiti jeste da smo vrili (neophodnu) konverziju pokazivaa. Iz prvog
pokazivaa (tipa void *) u pokaziva (tipa int *), jer nismo mogli samo napisati niz[a] jer
ne moemo dereferencirati pokaziva koji je tipa void, a znamo da je niz[a] isto to i
sljedea konstrukcija *(niz + a) koja predstavlja dereferenciranje.
20
Ovdje moemo, u funkciji AlocirajNiz, primijetiti da smo koristili i sizeof operator. Naime, taj
operator vraa veliinu, u bajtima, nekog objekta. Ve smo spomenuli da malloc alocira bajte
pa mi moramo alocirati dovoljno bajta. Ako tip int zauzima 4 bajta, onda moramo alocirati 40
bajta za 10 elemenata tipa int. Poto mi ne znamo (jer to ovisi od implementacije) koliko bajta
zauzima koji tip, koristimo sizeof operator koji e nam to otkriti. Ostalo smo ve objasnili u
uvodu. Vidjeli smo da funkcija AlocirajNiz nije vratila visei pokaziva nego pokaziva na
alocirani dio memorije (koji je potpuno validan). Razmislite zato vraeni pokaziva nije
visei.
Pokaimo sada dva primjera unosa razliitih brojeva, za koje e alokacija uspjeti i za koje
nee.
21
Ne moemo alocirati niz od -1 elemenata. Ovo smo svakako trebali provjeriti prije nego to i
pozovemo funkciju AlocirajNiz ali smo ovdje namjerno ostavili to da moemo pokazati da e
malloc funkcija vratiti nul-pokaziva u sluaju neuspjene alokacije.
Ovdje je zbog nedostatka prostora samo spomenuta dinamika alokacija i povrnu dotaknute
njene prednosti (zaista ih ima veliki broj) kao i njeni problemi prilikom neopreznog
koritenja.
Znai, ukoliko se odluimo da koristimo dinamiku alokaciju u programu, trebamo biti jako
oprezni i paziti na mnogo detalja!
22