Download as pdf or txt
Download as pdf or txt
You are on page 1of 27

Dr.

eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

Dodatak predavanjima: Sve to treba znati o pokazivaima


Bez obzira na injenicu da je C++ uveo novu vrstu objekata nazvanu reference, koja umnogome rjeava
brojne probleme vezane za nerazumijevanje i pogrenu upotrebu pokazivaa, pokazivai se u jeziku C++ jo
uvijek intenzivno koriste, s obzirom da se reference u jeziku C++ nisu mogle izvesti na nain koji bi u
potpunosti uklonio potrebu za pokazivaima, kao to je to uinjeno u istim objektno orijentiranim jezicima
(npr. u Javi). Naime, injenica da se od jezika C++ zahtijevala visoka kompatibilnost sa jezikom C, dovela je
do toga da je bijeg od pokazivaa u jeziku C++ ipak nemogu. S obzirom da je nerazumjevanje koncepta
pokazivaa est razlog za brojne probleme u razumijevanju naprednijih koncepata jezika C++, svrha ovog
dodatka je da prui pregled onih znanja o pokazivaima koje je neophodno posjedovati za ispravno
razumijevanje predavanja koja kasnije slijede. Veinu tih znanja studenti su trebali stei kroz prethodne
kurseve programiranja, ali pitanje je da li su ih zaista stekli, zbog dosta sloene materije. To je i glavni razlog
zbog koje je ovaj dodatak uope pisan.
Pokazivai su jedna od dvije vrste objekata u jeziku C++ pomou kojih se moe vriti indirektni pristup
drugim objektima (druga vrsta su reference). Pokazivai su u C++ direktno naslijeeni iz jezika C, u kojem
predstavljaju jedine objekte preko kojih je mogu indirektni pristup drugim objektima. U sutini, pokazivai nisu
nita drugo nego promjenljive koje kao svoj sadraj sadre adresu nekog drugog objekta (kae se pokazuju na
neki objekat), podrane odgovarajuim operatorima koji omoguavaju da se pristupi objektu na koji pokazuju.
Pokazivai, dakle, nisu nita komplicirano. Najvei problem kod poentika je to se esto pobrka sam pokaziva
(engl. pointer), sa onim na ta on u tom trenutku pokazuje (engl. pointee). Dodatni problem (i to ne samo kod
poetnika) je to to pokazivai pruaju izuzetno veliku fleksibilnost, koju je veoma lako zloupotrebiti. Zbog
tog razloga se programerima u jeziku C++ savjetuje da dobro proue reference, i da sve probleme koji se
mogu rjeavati pomou referenci zaista i rjeavaju pomou referenci, a ne pomou pokazivaa. Bez obzira na
to, u jeziku C++ i dalje postoji mnogo stvari koje je mogue uraditi samo pomou pokazivaa, a koje je
nemogue realizirati niti pomou referenci, niti na bilo koji drugi nain (koji ne koristi pokazivae).
Pokazivae nije mogue objasniti i razumjeti bez neto detaljnijeg objanjenja o tome kako se promjenljive
uvaju u memoriji raunara (izmeu ostalog i zbog toga to kod pokazivaa moemo direktno pristupiti
informaciji o tome gdje se u memoriji uva objekat na koji pokaziva pokazuje). Zbog toga emo prvo u
osnovnim crtama razmotriti nain smjetanja promjenljivih u raunarskoj memoriji. Poznato je da svaka
promjenljiva koja se deklarira u programu tokom svog ivota zauzima odreeni prostor u memoriji. Mjesto u
memoriji koje je dodijeljeno nekoj promjenljivoj odreeno je njenom adresom. Adresa promjenljive je
zapravo neka informacija koja na jednoznaan nain odreuje mjesto u memoriji gdje se ta promjenljiva
nalazi, i ona je fiksna sve dok ta promjenljiva postoji. Ta informacija je kod veine raunarskih arhitektura
brojane prirode (tj. adrese su obino neki brojevi), mada ne mora nuno da bude. Recimo, kod najveeg
broja raunarskih arhitektura memorija je organizovana kao jednodimenzionalni niz memorijskih elija
(registara), i tada je adresa prosto redni broj (indeks) odgovarajue memorijske elije u kojoj se uva sadraj
promjenljive (ili prve od vie takvih elija u sluaju da sadraj promjenljive ne moe stati u jednu memorijsku
eliju). Meutim, sasvim su mogue (i postoje) drugaije memorijske arhitekture kod kojih adresa nije broj.
Recimo, postoje memorijski modeli koji nisu organizirani kao jednodimenzionalni ve kao dvodimenzionalni
niz (matrica) memorijskih elija, tako da je za odreivanje pozicije neke memorijske elije potrebno poznavati
red i kolonu u kojoj se ta elija nalazi, tako da kod takvih memorijskih modela adresa nije broj nego par
brojeva (red i kolona). Na primjer, takav memorijski model javlja se kod prvih generacija PC raunara sa
starijim modelima Intel-ovih procesora poput 8086, kod kojih su adrese zaista predstavljeni kao parovi
brojeva (koji se redom nazivaju segment i offset). tavie, postoje i koncepti asocijativnih memorija kod kojih
je svakoj memorijskoj eliji pridruen neki jednoznani identifikator (tag), koji uope ne mora biti numerike
prirode, i koji onda preuzima ulogu adrese te elije. ak i ako se ograniimo iskljuivo na raunarske
arhitekture kod kojih adrese memorijskih elija zaista jesu brojevi, prilikom rada sa pokazivaima mogu
nastati velike greke u reznonovanju ukoliko o adresama razmiljamo kao obinim brojevima. Zbog toga
adrese ne treba posmatrati kao brojeve, nego iskljuivo kao informacije koje jednoznano odreuju lokaciju
nekog objekta u memoriji, pri emu je tana priroda te informacije manje-vie posve nebitna.
Pretpostavimo, na primjer, da imamo deklaraciju neke klasine (recimo cjelobrojne) promjenljive poput
int broj;

Prilikom nailaska na ovu deklaraciju, rezervira se odgovarajui prostor u memoriji za potrebe smjetanja
sadraja promjenljive broj, koji naravno ima svoju adresu (koju emo nazvati prosto adresa promjenljive
broj). To emo predstaviti sljedeom memorijskom slikom:

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

broj

???
Rezervirani memorijski prostor smo predstavili pravougaonikom, dok upitnici oznaavaju da sadraj
rezerviranog memorijskog prostora nije definiran, odnosno u njemu se nalazi isti sadraj koji se u tom dijelu
memorije nalazio od ranije, tj. prije izvrene rezervacije (zbog toga je i poetna vrijednst promjenljive broj
nedefinirana). Ukoliko nakon ove deklaracije izvrimo dodjelu neke vrijednosti promjenljivoj broj, npr.
dodjelom poput
broj = 56;

efekat ove dodjele bie smjetanje neke kodirane reprezentacije broja 56 u rezervirani memorijski prostor (za
reprezentaciju cijelih brojeva najee se koristi prosti binarni kd, ali sam nain reprezentacije nije bitan za
izlaganja koja slijede). To emo predstaviti sljedeom slikom:
broj

56
Da smo prilikom deklaracije odmah izvrili i inicijalizaciju promjenljive, npr. deklaracijom poput
int broj(56);

// ili int broj = 56; u C stilu

ovakvu memorijsku sliku bismo imali odmah po stvaranju promjenljive, odnosno istovremeno sa rezervacijom
memorijskog prostora za pamenje sadraja promjenljive broj bilo bi izvreno upisivanje odgovarajueg
sadraja u rezervirani prostor.
Razumije se da za pristup promjenljivoj broj nije uope potrebno poznavati na kojoj se adresi u
memoriji ona nalazi, jer njenom sadraju uvijek moemo pristupiti koristei njeno simboliko ime broj (kao
to smo uvijek i inili). Meutim, pokazivai su specijalne vrste promjenljivih koje kao svoj sadraj imaju
adresu neke druge promjenljive (ili openito, neke druge l-vrijednosti) i u takve promjenljive se ne moe
upisivati nita drugo osim adrese. Pokazivake promjenljive se deklariraju tako da se ispred njihovog imena
stavi znak * (zvjezdica). Na primjer, sljedea deklaracija deklarira promjenljivu pok koja nije tipa cijeli
broj (tj. int), nego pokaziva na cijeli broj (ovaj tip se oznaava kao int *):
int *pok;

Kao i svaka druga promjenljiva, i pokazivaka promjenljiva zauzima odreeni prostor u memoriji (te i
ona ima svoju adresu). Pretpostavimo li da su na prethodno opisani nain deklarirane cjelobrojna promjenljiva
broj i pokazivaka promjenljiva pok, to moemo predstaviti sljedeom memorijskom slikom:
broj

56

pok

???

Upitnici unutar promjenljive pok govore da prethodna deklaracija nije postavila nikakav inicijalni
sadraj u prostor rezerviran za potrebe te promjenljive, nego je on onakav kakav se prosto zatekao odranije u
tom prostoru. Tako e ostati sve dok ovoj promjenljivoj ne dodijelimo neku vrijednost. Meutim, pokazivake
promjenljive namijenjene su da pamte iskljuivo adrese, i njjma se ne smiju neposredno dodjeljivati brojane
vrijednosti (razlozi za to su brojni, a jedan od njih, mada ne i najbitniji, je i to to adrese ne moraju nuno biti
brojevi). Stoga kompajler nee dozvoliti dodjelu poput sljedee:
pok = 4325;

// OVO NIJE DOZVOLJENO!

Umjesto toga, pokazivakim promjenljivim se mogu dodjeljivati samo vrijednosti pokazivake prirode (tj.
vrijednosti koje garantirano predstavljaju adrese nekih drugih objekata). Na primjer, pokazivau je mogue
dodjeliti adresu nekog objekta (npr. promjenljive) uz pomo unarnog prefiksnog operatora &, koji moemo
itati adresa od. Ovaj operator (koji se jo naziva i operator referenciranja) moe se primjenjivati samo na
operande koji imaju adrese (tj. na l-vrijednosti), kao to su recimo promjenljive, ali ne i na recimo brojeve ili
proizvoljne izraze (tako je, na primjer, konstrukcija poput &broj smislena, ali su konstrukcije poput &15
ili &(broj + 2) besmislene). Stoga, ukoliko izvrimo dodjelu poput

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

pok = &broj;

u promjenljvu pok e se smjestiti adresa promjenljive broj (nain kako je ta adresa zaista predstavljena
posve je nebitan). Situacija u memoriji sada odgovara sljedeoj slici:
broj

pok

56
Na ovoj slici, da bismo izbjegli sve eventualne pekulacije o tome kako je adresa zaista predstavljena u
pokazivakoj promjenljivoj, tu adresu prosto prikazali kao krui od kojeg vodi strelica ka objektu koji
odgovara toj adresi. Kae se da sada pokazivaka promjenljiva (engl. pointer) pok pokazuje (engl. points to)
na promjenljivu broj, za koju emo rei da je pokazani objekat (engl. pointee) u nedostatku boljeg prevoda
za ovu englesku rije.
Koritenje pokazivakih promjenljivih u aritmetikim izrazima je prilino ogranieno (npr. izraz poput
3 * pok + 2 nije dozvoljen), a ak i u kontekstima u kojima je dozvoljeno njihovo koritenje u aritmetikim
izrazima, pravila izvoenja raunskih operacija (aritmetike) sa pokazivaima razlikuju se od klasinih pravila
aritmetike sa brojevima (jo jedan razlog da nije dobro doivljavati adrese kao brojeve), o emu e kasnije
biti detaljno govora. U svakom sluaju, svaki direktni pristup promjenljivoj pok odnosi se na adresu koja je
u njoj pohranjena, a ne na promjenljivu na koju ona pokazuje. Po tome se pokazivai bitno razlikuju od
referenci, s obzirom da se svaki pristup referenci automatski preusmjerava na objekat na koji referenca ukazuje
(odnosno, kod pokazivaa ovog automatskog preusmjeravanja nema).
Sam sadraj pokazivake promjenljive nam obino nije od neposredne koristi, nego nam je vanije da
pristupimo objektu na koji ona pokazuje. Za tu svrhu koristi se posebna operacija, koja se obino naziva
dereferenciranje, a postie se uz pomo unarnog operatora *. Ovaj operator primjenjuje se na pokazivake
promjenljive, ili openitije na pokazivake izraze (tj. izraze iji su rezultati adrese, o kojima emo govoriti
neto kasnije), a moemo ga itati kao ono na ta pokazuje .... Recimo, ako je pok neka pokazivaka
promjenljiva, izraz *pok moemo itati kao ono na ta pokazuje pok. Tako e, ukoliko se prethodno
izvre naredbe iz gore napisanih primjera, naredba
std::cout << *pok;

// Ispisuje sadraj onoga na ta pok pokazuje

ispisae vrijednost 56, jer promjenljiva pok trenutno pokazuje na promjenljivu broj, tako da je u ovom
trenutku *pok isto to i promjenljiva broj.
Dereferencirani pokaziva moe se koristiti u bilo kojem kontekstu u kojem bi se mogao koristiti bilo
koji izraz iji tip odgovara tipu na koji pokaziva pokazuje. Stoga je sljedea naredba potpuno legalna, i
ispisuje brojeve 57 i 112:
std::cout << *pok + 1 << std::endl << 2 * *pok;

U ovoj naredbi trebamo obratiti panju na dva detalja. Prvo, operator dereferenciranja ima vei prioritet
od aritmetikih operatora (sabiranja, mnoenja, itd.) tako da se izraz poput *pok + 1 ne interpretira kao
*(pok + 1) ve kao (*pok) + 1 (kasnije emo vidjeti kakvo bi tano znaenje imala prva interpretacija,
koja je takoer smislena). Drugo, u izrazu 2 * *pok prva zvjezdica predstavlja operator mnoenja, dok
druga zvjezdica predstavlja operator dereferenciranja (odnosno, postoje dva razliita operatora istog imena
*, pri emu je jedan binarni a drugi unarni). Ova injenica moe dovesti do zbrke ukoliko prilikom pisanja
izraza u kojima se javljaju dereferencirani pokazivai izostavimo razmake, odnosno ukoliko prethodnu
naredbu napiemo na sljedei nain:
std::cout<<*pok+1<<std::endl<<2**pok;

// LEGALNO, ALI NEMOJTE PISATI OVAKO!!!

Meutim, na ovaj nain je veoma nejasno ta koja zvjezdica predstavlja, pa ovakvo pisanje treba izbjegavati.
Vjerovatno najjasnije znaenje dobijamo ukoliko upotrijebimo i dodatne zagrade:
std::cout << *pok + 1 << std::endl << 2 * (*pok);

Veoma vana injenica je da dereferencirani pokaziva ima status promjenljive, tanije status l-vrijednosti,
tako da se moe nai i sa lijeve strane znaka jednakosti. tavie, na dereferencirane pokazivae mogu se
primjenjivati i sve druge operacije koje se normalno mogu primjenjivati samo nad promjenljivim (i drugim
l-vrijednostima) kao to su recimo ++ i , tako da su sasvim dozvoljene naredbe poput sljedeih:
3

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

*pok = 100;
(*pok)++;

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

// Smjeta 100 u ono na ta pokazuje pok


// Uveava sadraj onoga na ta pokazuje pok za 1

Prva naredba postavlja sadraj memorijske lokacije na koju ukazuje pokazivaka promjenljiva pok na
vrijednost 100, dok druga naredba uveava sadraj memorijske lokacije na koju ukazuje pokazivaka
promjenljiva pok za jedinicu. U konkretnom primjeru, kada pokazivaka promjenljiva pok pokazuje na
promjenljivu broj, efekat ovih naredbi je isti kao da smo neposredno pisali
broj = 100;
broj++;

// *pok je ovdje faktiki promjenljiva "broj"

Drugim rijeima, dereferencirani pokaziva koji pokazuje na neku promjenljivu ponaa se identino kao i
promjenljiva na koju pokazuje. Treba jo napomenuti da su u izrazu (*pok)++ zagrade obavezne. Naime,
operator ++ ima vei prioritet u odnosu na operator dereferenciranja. Stoga bi se izraz *pok++ interpretirao
kao *(pok++), koji je takoer smislen. Uskoro emo razjasniti i sadraj ove druge interpretacije.
Operatori referenciranja & i dereferenciranja * su meusobno inverzni. Ako je x objekat tipa
NekiTip, tada izraz &x ima tip pokaziva na tip NekiTip, a izraz *&x svodi se prosto na x. S
druge strane, ukoliko je pok pokaziva tipa pokaziva na tip NekiTip, tada je tip izraza *pok prosto
NekiTip, a izraz &*pok svodi se na pok.
Veoma je bitno istai da znak * upotrijebljen ispred imena promjenljive ima potpuno drugaije znaenje
prlikom deklaracije promjenljive i prilikom njene upotrebe. Nerazumijevanje ove injenice uzrok je velikih
zbrki kod poetnika. Zaista, prilikom deklaracije promjenljive, zvjezdica ispred njenog imena (npr. u deklaraciji
poput int *pok;) oznaava da se radi o pokazivakoj a ne o obinoj promjenljivoj, dok prilikom upotrebe
(pokazivake) promjenljive zvjezdica ispred njenog imena (npr. u izrazu poput std::cout << *pok)
oznaava da ne elimo pristupiti toj (pokazivakoj) promjenljivoj nego onome na ta ona pokazuje. Ovo su
oito dva posve razliita znaenja (da zbrka bude jo vea, znamo da u nekim kontekstima zvjezdica moe
oznaavati i mnoenje). Ipak, postoji jedan nain kako se deklaracije pokazivakih promjenljivih mogu itati
tako da se ukloni razlika izmeu gore pomenuta dva znaenja znaka *. Naime, umjesto da deklaraciju oblika
int *pok;

itamo kao pok je tipa pokaziva na cijeli broj, moemo je itati kao *pok je tipa cijeli broj, odnosno ono
na ta pokazuje pok je tipa cijeli broj. Uz ovakvu interpretaciju, tumaenje znaka * upotrijebljenog unutar
deklaracija i unutar proizvoljnih izraza postaje konzistentnije, odnosno fraza poput *pok moe se uvijek
itati kao ono na ta pokazuje pok, bez obzira to se u deklaracijama pokazivaka promjenljiva (pok u
ovom primjeru) tek uvodi. Nije na odmet napomenuti ni da znak & nema isto znaenje u svim kontekstima.
Upotrijebljen u proizvoljnim izrazima ispred imena promjenljive, on oznaava adresu te promjenljive. S druge
strane, prilkom deklaracije promjenljive, isti znak upotrijebljen njenog imena oznaava da se ne radi o
obinoj promjenljivoj, nego o tzv. referenci (to je posve drugo znaenje). Konano, u nekim kontekstima isti
znak moe oznaavati i operaciju konjunkcije po bitima (ukoliko se upotrijebi kao binarna operacija izmeu
dva cjelobrojna operanda).
Kao to svaka promjenljiva (osim ako je oznaena kvalifikatorom const) moe tokom svog ivota
mijenjati svoj sadraj, tako i pokazivake promjenljive mogu mijenjati svoj sadraj, to zapravo znai da
tokom ivota mogu pokazivati na razliite objekte. Ovo ilustrira sljedei primjer:
int broj1(5), broj2(8);
int *pok;
pok = &broj1;
std::cout << *pok << std::endl;
pok = &broj2;
std::cout << *pok << std::endl;

//
//
//
//

*pok je sada isto to i broj1


Ispisuje "5"
*pok je sada isto to i broj2
Ispisuje "8"

Jednom pokazivau se moe dodijeliti drugi pokaziva samo ukoliko oba pokazuju na posve isti tip (uz
neke iznimke vezane za tzv. nasljeivanje, na koje emo ukazati kada se za to ukae potreba). Na primjer,
ukoliko imamo deklaraciju
int *pok1, *pok2;

tada je dodjela pok1 = pok2 posve legalna, i nakon nje pok1 i pok2 pokazuju na istu lokaciju u memoriji,
i to onu na koju je prije dodjele pokazivao pokaziva pok2 (da bi ova dodjela imala smisla, prethodno je

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

potrebno pokazivau pok2 dodijeliti adresu nekog drugog objekta). Drugim rijeima, nakon ove dodjele,
*pok1 i *pok2 ne samo da imaju iste vrijednosti, nego su oni jedan te isti objekat (inae, pojava da se
istom objektu moe pristupiti na vie razliitih naina u programiranju se naziva aliasing). Slino vrijedi za
dodjelu pok2 = pok1. Sljedei primjer ilustrira pojavu aliasinga:
int broj;
int *pok1, *pok2;
pok1 = &broj1;
pok2 = pok1;
*pok1 = 8;
std::cout << *pok1 << *pok2;
*pok1 = 5;
std::cout << *pok1 << *pok2;

//
//
//
//
//
//

*pok1 je sada isto to i broj


*pok2 je sada takoer isto to i broj
U promjenljivu broj se smjeta 8
Ispisuje dvije osmice...
U promjenljivu broj se smjeta 5
Ispisuje dvije petice...

Kao to je ve reeno gore, pokazivai koji ne pokazuju na objekte istih tipova ne mogu se meusobno
dodjeljivati. Recimo, ukoliko imamo deklaracije
int *pok1;
double *pok2;

tada nije dozvoljena niti dodjela pok1 = pok2 niti dodjela pok2 = pok1. Meutim, bitno je razlikovati
ove dodjele od dodjela *pok1 = *pok2 i *pok2 = *pok1 koje su potpuno legalne, s obzirom da se
dereferencirani pokazivai pok1 i pok2 ponaaju kao cjelobrojna odnosno realna promjenljiva, a
meusobne dodjele izmeu cjelobrojnih i realnih promjenljivih su dozvoljene (uz automatsku konverziju tipa).
S obzirom da se u jeziku C++ razmaci u tekstu programu mogu po volji ubacivati i izbacivati sve dok to
ne dovede do promjene smisla programa (npr. ne smijemo izbaciti razmak izmeu int i a u konstrukciji
int a, jer bismo u suprotnom dobili rije inta koja nema nikakvog smisla u C++-u), pokazivake
promjenljive se mogu stilski deklarirati na nekoliko razliitih naina sa aspekta gdje pisati razmake, pro emu do
danas ne postoji opeprihvaeni konsenzus koji je stil najbolji. Recimo, neki stilovi deklaracije promjenljive
pok koja je tipa pokaziva na cijeli broj su sljedei:
int*pok;
int* pok;
int *pok;
int * pok;

//
//
//
//

Nepregledan stil...
Mnogi danas preporuuju ovaj stil...
Meutim, izgleda da je ovaj klasini stil bolji...
Ovaj stil nesretno asocira na mnoenje...

Od ova etiri stila, rijetko ko e zagovarati prvi ili etvrti stil. S druge strane, intenzivne su prepirke oko
toga da li je bolji drugi ili trei stil. U literaturi (pogotovo onoj posveenoj C++-u a ne C-u) se esto zagovara
drugi stil, uz objanjenje da u deklaracijama oznaka * bolje pristaje uz oznaku tipa, odnosno treba je tumaiti
kao sastavni dio oznake tipa (u skladu sa tumaenjem pok je tipa pokaziva na cijeli broj). Mada ovakvo
tumaenje nije bez osnova, ono ima jedan ozbiljan nedostatak. Naime, pogledajmo sljedeu deklaraciju:
int* a, b;

// Kojeg su tipa "a" i "b"?

Uz prethodno tumaenje, moglo bi se pomisliti da su u ovoj deklaraciji i a i b pokazivake promjenljive


(tj. da su a i b tipa pokaziva na cijeli broj). Meutim, istina je da je samo a pokazivaka promjenljiva,
dok je b obina cjelobrojna promjenljiva (tipa int). Autor ovih materijala smatra da je manje zbunjujue
ukoliko se ista deklaracija napie kao
int *a, b;

// Ovdje se jasnije vidi da je samo "a" pokaziva...

i ista interpretira kao *a i b su tipa cijeli broj. Zbog toga e se u ovim materijalima koristiti ovaj (stariji) stil
pisanja deklaracija pokazivakih promjenljivih, bez obzira to se u posljednje vrijeme intenzivno propagira
drugi stil.
Prije nego to se upoznamo sa primjenama pokazivaa, bitno je napomenuti da bez obzira to pokazivai
i dalje imaju veliku primjenu u jeziku C++, oni se mnogo manje koriste nego to se koriste u jeziku C. Naime,
u jeziku C su pokazivai jedini nain da se ostvari indirektni pristup nekoj promjenljivoj, dok se ta indirekcija
u jeziku C++ esto moe ostvariti znatno jednostavnije putem tzv. referenci. Recimo, u jeziku C, ne postoji
nikakav nain da funkcija promijeni vrijednosti svojih stvarnih parametara. Tako, ukoliko na primjer elimo
napisati funkciju Razmijeni koja razmjenjuje sadraje promjenljivih koje joj se prenose kao argumenti, u
C-u ne postoji nikakav nain da to izvedemo tako da poziv funkcije izgleda poput sljedeeg:
Razmijeni(a, b);

// Ovo je u C-u nemogue postii

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

Naime, kako god bila napisana funkcija Razmijeni, ona uvijek radi sa kopijom stvarnih argumenata
a i b, tako da ih ona ne moe izmijeniti (napomenimo da je, zahvaljujui referencama, u jeziku C++
mogue postii da se koristi upravo ovakav poziv). U jeziku C jedino to moemo uraditi je da funkciji
Razmijeni eksplicitno prenesemo adrese gdje se nalaze odgovarajui stvarni parametri, odnosno u pozivu
funkcije moramo eksplicitno uzeti adrese stvarnih parametara pomou adresnog operatora &. To zahtijeva
da poziv funkcije izgleda poput sljedeeg:
Razmijeni(&a, &b);

Da bi ovakav poziv bio sintaksno ispravan, potrebno je funkciju Razmijeni napisati tako da umjesto
cijelih brojeva kao parametre prihvata pokazivae na cijele brojeve. Tada je mogue upotrijebiti operator
dereferenciranja da se pomou njega indirektno pristupi sadraju memorijskih lokacija gdje se nalaze stvarni
parametri i da se na taj nain izvri razmjena. Na taj nain funkcija Razmijeni, napisana u duhu jezika C,
mogla bi izgledati ovako:
void Razmijeni(int *pok1, int *pok2) {
int pomocna(*pok1);
*pok1 = *pok2;
*pok2 = pomocna;
}

Mada napisana funkcija izgleda dosta jednostavno, poetniku nije posve lako shvatiti kako ona radi.
Neka su, na primjer, vrijednosti promjenljivih a i b respektivno 5 i 8, i neka je funkcija Razmijeni
pozvana na opisani nain (tj. uz eksplicitno navoenje operatora referenciranja & pri pozivu funkcije).
Sljedea slika ilustrira situaciju nakon izvravanja svake od naredbi u funkciji Razmijeni:
Razmijeni(&a, &b);
a

pok1

pok2

pok1

pok2

int pomocna(*pok1);
a

pomocna

*pok1 = *pok2;
a

pok1

pok2

pomocna

*pok2 = pomocna;
a

pok1

pok2

pomocna

Vidimo da su promjenljive a i b zaista razmijenile svoje vrijednosti. S druge strane, u jeziku C++
isti problem moe se rijeiti na mnogo jednostavniji i jasniji nain, koritenjem prenosa parametara po
referenci umjesto pokazivaa. Bez obzira na to, ovaj primjer veoma ilustrativno demonstrira na koji nain
djeluju pokazivai, a ujedno i demonstrira kako se pokazivai koriste kao parametri funkcija. Interesantno je
napomenuti da se potpuno ista situacija u memoriji poput situacije prikazane na prethodnim slikama deava i
u rjeenju u kojem se umjesto pokazivaa koristi prenos parametara po referenci, samo to toga programer
nije svjestan. U sutini, reference uvedene u jezik C++ se, na izvjestan nain, ponaaju kao veoma dobro
prerueni pokazivai.

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

Prethodni primjer je takoer veoma pouan, jer emo kod njega razmotriti i neke od tipinih greaka
koje mogu nastupiti pri radu sa pokazivaima. Naime, poetnici koji esto brkaju razliku izmeu pokazivaa i
pokazanog objekta esto nisu sigurni kad treba staviti zvjezdicu a kada ne. Razmotriemo sada neke od
tipinih greaka koje u prethodnom primjeru mogu nastati ukoliko se ne stavi zvjezdica tamo gdje bi trebala,
ili se stavi tamo gdje ne bi trebala. Idealna situacija je ukoliko nam kompajler pri tome prijavi greku, jer
onda smo barem sigurni da neto nije u redu. Mnogo gora situacija je ukoliko pri tome ne doe do sintaksne
greke, ali funkcija ne radi ispravno (ili jo gore, izgleda da radi ispravno, a pravi neku tetu sa strane koja
nije odmah vidljiva). Krenimo prvo sa najmanje problematinim grekama, tj. onima koje e dovesti do
prijave sintaksne greke. Slijedi spisak ta bismo sve mogli pogrijeiti u prethodnoj funkciji po pitanju
upotrebe zvjezdice, a da to dovede do prijave greke:
int *pomocna(*pok1);
int pomocna(pok1);
pok1 = *pok2;
*pok1 = pok2;
pok2 = pomocna;
pok2 = *pomocna;
*pok2 = *pomocna;

//
//
//
//
//
//
//

Greka
Greka
Greka
Greka
Greka
Greka
Greka

(1)
(2)
(3)
(4)
(5)
(6)
(7)

Konstrukcija (1) je sintaksno neispravna jer se u njoj promjenljiva pomocna deklarira kao pokaziva, a
pokuava se inicijalizirati onim na ta pokazuje pokaziva pok, to je cijeli broj (a pokaziva ne moemo
inicijalizirati cijelim brojem). U konstrukciji (2) pokuavamo inicijalizicati cjelobrojnu promjenljivu
pokazivaem, to takoer nije dozvoljeno. U konstrukcijama (3) i (5) pokuavamo dodijeliti pokazivau cijeli
broj, dok u konstrukciji (4) pokuavamo cijelom broju dodijeliti pokaziva. Konano, u konstrukcijama (6) i
(7) pokuavamo dereferencirati promjenljivu pomocna koja nije pokazivakog tipa, to je naravno
nedozvoljeno, osim ako nismo istovremeno napravili jo jednu greku i ovu promjenljivu takoer deklarirali
kao pokazivaku (o posljedicama takve greke govoriemo neto nie u nastavku teksta).
Sada emo prei na malo podmuklije greke, s obzirom na injenicu da one ostaju nedetektirane od
strane kompajlera. Pretpostavimo da smo grekom umjesto dodjele *pok1 = *pok2 napisali dodjelu
pok1 = pok2 (koja je u naelu sasvim legalna), odnosno da smo napisali sljedeu funkciju:
void Razmijeni(int *pok1, int *pok2) {
int pomocna(*pok1);
pok1 = pok2;
*pok2 = pomocna;
}

// OVA FUNKCIJA NE RADI ISPRAVNO...

Ova funkcija nee raditi ispravno, jer umjesto da kopiramo ono na ta pokazuje pokaziva pok2 tamo gdje
pokazuje pokaziva pok1, mi zapravo kopiramo same pokazivae, odnosno adrese koje su u njima
sadrane, nakon e doi do aliasinga (oba pokazivaa e pokazivati na istu promjenljivu). Sljedea analiza
pokazuje ta e se dogoditi uz iste poetne pretpostavke kao u prethodnoj analizi:
Razmijeni(&a, &b);
a

pok1

pok2

pok1

pok2

int pomocna(*pok1);
a

pomocna

pok1 = pok2;
a

pok1

pok2

pomocna

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

*pok2 = pomocna;
a

pok1

pok2

pomocna

U svakom sluaju, vidimo da sadraji promjenljivih a i b nisu razmijenjeni (nego je zapravo sadraj
promjenljive a samo iskopiran u promjenljivu b). Ova greka i nije toliko opasna, jer emo u fazi testiranja
barem vidjeti da funkcija ne radi (gore su greke koje se ak ni testiranjem ne mogu uvijek uoiti, a uskoro
emo vidjeti da su naalost i takve greke mogue).
Druga greka koja bi se mogla napraviti je sljedea, u kojoj se promjenljiva pomocna grekom deklarira
kao pokazivaka promjenljiva, a ona to ne treba da bude. Naravno, da bi takva funkcija prola sintaksnu
provjeru, treba napraviti jo poneku grekicu, recimo staviti zvjezdicu ispred promjenljive pomocna u
dodjeli *pok2 = pomocna. Sve u svemu, pretpostavimo da smo grekom napisali ovakvu, sintaksno ispravnu
ali logiki nekorektnu funkciju:
void Razmijeni(int *pok1, int *pok2) {
int *pomocna(pok1);
*pok1 = *pok2;
*pok2 = *pomocna;
}

// NI OVA FUNKCIJA NE RADI ISPRAVNO...

Kod ove funkcije ve na poetku imamo aliasing uzrokovan injenicom da pokazivai pomocna i
pok2 pokazuju na isti objekat. Sljedea analiza pokazuje ta se deava prilikom izvravanja ove funkcije:
Razmijeni(&a, &b);
a

pok1

pok2

pok1

pok2

pomocna

pok1

pok2

pomocna

pok1

pok2

pomocna

int *pomocna(pok1);
a

*pok1 = *pok2;
a

*pok2 = *pomocna;
a

Kao to vidimo, sadraji promjenljivih a i b ni ovdje nisu ispravno razmijenjeni. Varijacijom ove
neispravne funkcije moe se dobiti jo nekoliko neispravnih (mada sintaksno korektnih) varijanti. Recimo,
umjesto dodjele *pok1 = *pok2 neko bi mogao napisati pogrenu dodjelu pok1 = pok2. Isto tako, neko bi
umjesto dodjele *pok2 = *pomocna mogao napisati dodjelu pok2 = pomocna (koja bi sad sintaksno prola,
jer je promjenljiva pomocna deklarirana kao pokaziva). I naravno, mogle bi se istovremeno napraviti
obje spomenute greke. Kao korisnu vjebu, analizirajte ta bi se desilo u svakom od gore opisanih scenarija.

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

Uglavnom, ni u jednom od gore opisanih scenarija funkcija Razmijeni ne bi radila ono to treba da radi,
ali bi njeno testiranje jasno pokazalo da traeni cilj (razmjena sadraja promjenljivih a i b) nije postignut.
Preimo sada na opis vjerovatno najpodmuklije greke koju bismo mogli napraviti prilikom pisanja ove
funkcije. Pretpostavimo da neko nije navikao da vri inicijalizaciju promjenljivih odmah prilikom njihove
deklaracije, nego da je vie sklon da umjesto konstrukcije
int pomocna(*pok1);

obavi prvo deklaraciju pa dodjelu vrijednosti promjenljivoj pok, odnosno da napie


int pomocna;
pomocna = *pok1;

Na taj nain dobijamo sljedeu izvedbu funkcije Razmijeni:


void Razmijeni(int *pok1, int *pok2) {
int pomocna;
pomocna = *pok1;
*pok1 = *pok2;
*pok2 = pomocna;
}

// Ova funkcija je korektna...

Jasno je da je ova funkcija korektna, jer deklaracija promjenljive uz inicijalizaciju ima u konanici isti
efekat kao deklaracija promjenljive bez inicijalizacije iza koje odmah slijedi dodjela eljene poetne
vrijednosti. Meutim, pretpostavimo da je neko napisao verziju gornje funkcije pri emu je stavio zvjezdice i
gdje treba i gdje ne treba, odnosno da je napisao sljedeu (sintaksno ispravnu) funkciju:
void Razmijeni(int *pok1, int *pok2) {
int *pomocna;
*pomocna = *pok1;
*pok1 = *pok2;
*pok2 = *pomocna;
}

// OVA FUNKCIJA IMA FATALNU GREKU


//
IAKO PRILIKOM TESTIRANJA MOE
//
IZGLEDATI KAO DA RADI ISPRAVNO!!!

Ova funkcija je neispravna, ali to je najgore, to vrlo vjerovatno nee biti otkriveno prilikom njenog
testiranja. Analizirajmo izvravanje ove funkcije (obratite panju da na poetku pokaziva pomocna nije
inicijaliziran tako da pokazuje na sluajnu lokaciju u memoriji, koja je oznaena crticama):
Razmijeni(&a, &b);
a

pok1

pok2

pok1

pok2

pomocna

???

pok1

pok2

pomocna

???

int *pomocna;
a

*pomocna = *pok1;
a

*pok1 = *pok2;
a

pok1

pok2

pomocna

???

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

*pok2 = pomocna;
a

pok1

pok2

pomocna

???

Spolja gledano, testiranje funkcije bi moglo pokazati da funkcija radi ispravno, odnoso ona zaista
razmjenjuje sadraj promjenljivih a i b! U emu je onda problem? Glavni problem je upravo u pristupanju
nepoznatom mjestu u memoriji koje nastaje dereferenciranjem neinicijaliziranog pokazivaa, za koji ne znamo
gdje pokazuje. Dodjelom *pomocna = *pok1 mi sadraj onoga na ta pokazuje pokaziva pok1 (a to je
promjenljiva a) kopiramo tamo gdje pokazuje pokaziva pomocna, ali problem u tome je to mi nemamo
nikakve ideje o tome koje je to mjesto u memoriji. Desi li se sluajno da se na tom mjestu u memoriji ne nalazi
nita bitno, nee biti nikakvih tetnih posljedica, i izgledae nam da je sve ispravno. Meutim, moe se dogoditi
da se u tom dijelu memorije na koji pokaziva pomocna nalaze recimo neki podaci bitni za funkcioniranje
operativnog sistema, ili openito neki podaci zabranjeni za pristup. U tom sluaju e vjerovatno operativni
sistem prekinuti rad programa, uz obavjetenje da program radi nedozvoljene stvari. Ni to nije najgori
scenario, jer tad bar znamo da neto nije u redu. Najgore je ako se na tom mjestu u memoriji na koje sluajno
pokaziva pomocna u tom trenutku pokazuje nalazi recimo neka druga promjenljiva istog programa. Ova
funkcija e tada poremetiti vrijednost te promjenljive, a da niko toga nee biti svjestan. Posljedice tog napada
bie vidljive tek moda mnogo kasnije, kada programu zatreba vrijednost te promjenljve koju je ova funkcija
unitila! Ovo je najgora vrsta greaka, jer se teko otkrivaju testiranjem, s obzirom da se njihove mogue
posljedice tipino manifestiraju mnogo kasnije u odnosu na trenutak kada je do greke zaista dolo.
Treba napomenuti da do ove greke ne bi moglo doi da smo inicijalizaciju vrili odmah prilikom
deklaracije. Naime, treba primijetiti da u prethodnoj konstrukciji
int *pomocna;
*pomocna = *pok1;

// Problematina konstrukcija...

smisao zvjezdice ispred promjenljive pomocna u prvom i drugom redu nije isti. Stoga ova konstrukcija nije
ekvivalentna konstrukciji
int *pomocna(*pok1);

// A ova nije ni sintaksno ispravna...

koja usput nije ni sintaksno ispravna, jer se pokazivaka promjenljiva pomocna ne moe inicijalizirati
cjelobrojnom promjenljivom, a *pok1 se upravo tretira kao cjelobrojna promjenljiva. Sve prethodne (ispravne
i neispravne) primjere treba dobro prouiti, jer je njihovo prouavanje najbolji nain da se ispravno shvati
smisao pokazivakih promjenljivih i izbjegnu greke pri njihovoj upotrebi.
Sada emo prei na opis pokazivake aritmetike. Jezici C i C++ pokazivake promjenljive tretiraju
pokazivake promjenljive posve drugaije nego obine brojane promjenljive, ak i u situacijama kada se
adrese koje su u njima pohranjene zaista mogu zamisliti kao obini brojevi (to je zapravo sluaj kod veine
raunarskih arhitektura). Jedan od razloga za to je i injenica da nije dobro pretpostavljati nita o tome kako je
memorija zaista organizirana, s obzirom da bi smisao jezika trebala da to manje ovisi od svojstava hardvera
na kojem se program izvrava. Posljedica ovoga je da mnoge aritmetike operacije nad pokazivakim
promjenljivim nisu dozvoljene, a i one koje jesu dozvoljene interpretiraju se bitno drugaije nego sa obinim
brojanim promjenljivim (na nain koji ne ovisi od same arhitekture raunara). Stoga je neophodno pokazivake
tipove treba shvatiti kao sasvim posebne tipove, bitno razliite od cjelobrojnih tipova (bez obzira to su veini
sluajeva oni interno realizirani upravo kao cijeli brojevi). Na primjer, nije dozvoljeno sabrati, pomnoiti ili
podijeliti dva pokazivaa. Razlog za ovu zabranu je injenica da se ne vidi na ta bi smisleno mogao
pokazivati pokaziva koji bi se dobio recimo mnoenjem adresa dva druga objekta, ak i uz pretpostavku da
su te adrese obini brojevi. Slino, nije dozvoljeno pomnoiti ili podijeliti pokaziva sa brojem, ili obrnuto.
Takoer, mada se interna vrijednost pokazivaa moe ispisati na izlazni tok (pri emu se kao rezultat ispisa
dobija interna reprezentacija adrese pohranjene u pokazivau u vidu heksadekadnog broja, to za veinu
korisnika nije osobito interesantna informacija), nije dozvoljeno njihovo unoenje preko ulaznog toka (npr.
tastature). Meutim, dozvoljeno je sabrati pokaziva i cijeli broj (i obratno), zatim oduzeti cijeli broj od
pokazivaa, kao i oduzeti jedan pokaziva od drugog. U nastavku emo razmotriti smisao ovih operacija.
Smisao pokazivake aritmetike oitava se u sluaju kada pokaziva pokazuje na neki element niza (kako
operator referenciranja & kao svoj argument prihvata bilo kakvu l-vrijednost, njegov argument moe biti i
indeksirani element niza, za razliku od recimo izraza poput &2 ili &(a + 2) koji su besmisleni). Da bismo
to ilustrirali, pretpostavimo da imamo sljedee deklaracije:
10

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

double niz[5] = {4.23, 7, 3.441, 12.9, 6.03};


double *p(&niz[2]);

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

// Ili bez "=" u C++11

Ovdje smo inicijalizirali pokaziva p tako da pokazuje na element niza niz[2]. Nakon ovih
deklaracija, stanje u memoriji moemo predstaviti ovako:
niz

4.23

3.441 12.9

6.03

Pokaziva p pokazuje na element niz[2], tako da je *p upravo niz[2]. Razmotrimo sada izraz
p + 1. Ovaj izraz ponovo predstavlja pokaziva (tako da se i on moe dereferencirati poput svih drugih
pokazivaa, odnosno izraz poput *(p + 1) je legalan) ali koji pokazuje na element niz[3]. Dakle,
dodavanje jedinice na neki pokaziva koji pokazje na neki element niza daje kao rezultat novi pokaziva koji
pokazuje na sljedei element niza u odnosu na element na koji pokazuje izvorni pokaziva. Meutim, treba
naglasiti da ak i u sluajevima kada adrese moemo interpretirati kao brojeve, brojana vrijednost adrese
koju predstavlja pokaziva p + 1 gotovo sigurno nije jednaka brojanoj vrijednosti adrese koju predstavlja
pokaziva p uveanoj za 1! Naime, kod veine raunarskih arhitektura memorija se adresira po bajtima,
odnosno uzima se da jedna memorijska elija zauzima tano jedan bajt. S druge strane, promjenljive tipa
double sasvim sigurno ne mogu stati u jednu jednobajtnu memorijsku lokaciju (najei nain kodiranja
promjenljivih tipa double koji se danas koristi propisan je IEEE 754 standardom, prema kojem se jedna
promjenljiva tipa double kodira u 64 bita, odnosno u 8 bajta, to znai da promjenljive tipa double
prema tom standardu zauzimaju 8 jednobajtnih memorijskih lokacija). Stoga, ukoliko je brojana vrijednost
adrese pohranjene u pokazivau p recimo 1000, tada pokazivau p + 1 ne odgovara adresa 1001 nego
recimo 1008, s obzirom da element niza niz[2] ne zauzima samo memorijsku eliju sa adresom 1000,
nego 8 memorijskih elija sa adresama od 1000 do 1007 ukljuivo!
Ukoliko pretpostavimo da je memorijski model linearan (jednodimenzionalan) tako da su adrese interno
reprezentirane kao obini brojevi, tada e stvarna adresa koju predstavlja pokaziva p + 1 biti vea od adrese
koju predstavlja pokaziva p za broj memorijskih lokacija (tipino broj bajta) koje zauzima objekat na koji
pokaziva p pokazuje. Ova konvencija je izuzetno praktina, jer programer ne mora da vodi brigu o tome
koliko zaista neki element niza zauzima prostora: ako p pokazuje na neki element niza, p + 1 pokazuje
na sljedei element istog niza, ma gdje da se on nalazio. to je jo znaajnije, to vrijedi bez obzira kako je
memorija organizirana (tj. ak i u sluaju da ona nije organizirana na linearan nain). Izraz p + 1 uvijek
pokazuje na element niza koji neposredno slijedi elementu na koji pokazuje pokaziva p, ma kako da je
memorija organizirana, i ma kakav da je interni sadraj pokazivaa p. To i jeste osnovni smisao pokazivake
aritmetike. Analogno tome, izraz p 1 predstavlja pokaziva koji pokazuje na prethodni element niza u
odnosu na element na koji pokazuje pokaziva p (u navedenom primjeru na element niz[1]). Generalno,
izraz oblika p + n odnosno p n gdje je p pokaziva a n cijeli broj (odnosno ma kakav cjelobrojni
izraz) predstavlja novi pokaziva koji pokazuje na element niza iji je indeks vei odnosno manji za n u
odnosu na indeks elementa niza na koji pokazuje pokaziva p. Stoga e sljedea naredba, za niz iz
razmatranog primjera, ispisati vrijednosti 3.441, 12.9 i 6.03:
for(int i = 0; i <= 2; i++) std::cout << *(p + i) << " ";

Uoite razliku izmeu izraza *(p + i) i izraza *p + i (odnosno (*p) + i) koji, kao to smo ranije
vidjeli, imaju posve drugaije znaenje. Takoer, kako je svaki dereferencirani pokaziva l-vrijednost, to su
izrazi poput izraza
*(p 1) = 8.5

sasvim smisleni (ovaj izraz e postaviti sadraj elementa niza niz[1] na vrijednost 8.5).
Bitno je napomenuti da je smisao pokazivake aritmetike preciziran standardima jezika C i C++ samo za
sluaj kada pokaziva pokazuje na neki element nekog niza. Za sluaj kada pokaziva pokazuje na neki drugi
objekat (npr. na obinu promjenljivu), smisao pokazivake aritmetike nije definiran. Na primjer, za ranije
definiran pokaziva pok koji je pokazivao na cjelobrojnu promjenljivu broj, smisao izraza pok + 1 i
pok 1 nije preciziran standardom (tj. oni su sintaksno ispravni, ali nije precizirano ta im je vrijednost,
stoga se ne smiju koristiti). Za sluaj linearnih memorijskih modela, ovi izrazi bi se gotovo sigurno
interpretirali kao pokazivai koji pokazuju na mjesta u memoriji ije su adrese vee odnosno manje od adrese

11

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

zapisane u pokazivau pok za onoliko memorijskih lokacija koliko zauzima promjenljiva broj. Meutim,
ako memorijski model nije linearan, interpretacije zaista mogu biti svakakve. Stoga se trebamo pridravati
standarda koji strogo naglaava da pokazivaku aritmetiku treba koristiti samo ukoliko pokaziva pokazuje na
neki element niza. Ovog pravila se zaista moramo pridravati strogo: u nekim sluajevima operativni sistem
ima pravo da prekine izvravanje programa u kojem je pokaziva odlutao zbog pogrene primjene
pokazivake aritmetike, ak i ako se zalutali pokaziva uope ne dereferencira (ovo se deava iznimno
rijetko, ali se deava)!
Pokazivaka aritmetika nije uvijek definirana ak ni za sluaj pokazivaa koji pokazuju na elemente
niza. Postoji jo pravilo koje kae da su izrazi oblika p + n odnosno p n gdje je p pokaziva koji
pokazuje na neki element niza a n cjelobrojni izraz definirani samo ukoliko novodobijeni pokaziva ponovo
pokazuje na neki element niza koji zaista postoji u skladu sa specificiranom dimenzijom niza, ili eventualno
na mjesto koje se nalazi neposredno iza posljednjeg elementa niza. Drugim rijeima, vrijednost izraza p n
postaje nedefinirana ukoliko novodobijeni pokaziva odluta ispred prvog elementa niza, a vrijednost izraza
p + n postaje nedefinirana ukoliko novodobijeni pokaziva odluta daleko iza posljednjeg elementa niza.
Za konkretan primjer niza niz iz maloprije navedenog primjera koji sadri 5 elemenata i pokazivaa p
koji je postavljen da pokazuje na element niz[2], izrazi p + 1, p + 2, p + 3, p 1 i p 2 su
precizno definirani, za razliku od izraza p + 4 ili p 3 koji nisu, jer su odlutali u nepoznate vode
(njihov sadraj je nepredvidljiv, posebno u sluajevima kada memorijski model nije linearan).
Nad pokazivakim promjenljivim se mogu primjenjivati i operatori +=, =, ++ i ije je
znaenje analogno kao za obine promjenljive, samo to se koristi pokazivaka aritmetika. Na primjer, izraz
p += 2 e promijeniti sadraj pokazivake promjenljive p tako da pokazuje na element niza koji se nalazi
dvije pozicije navie u odnosu na element niza na koji p trenutno pokazuje, dok e izraz p promijeniti
sadraj pokazivake promjenljve p tako da pokazuje na prethodni element niza u odnosu na element na koji
p trenutno pokazuje (u oba sluaja, p postaje nedefiniran u sluaju da odlutamo izvan prostora koji niz
zauzima; eventualno se smijemo pozicionirati tik iza kraja niza). Na taj nain mogue je koristiti pokazivae
ija vrijednost tokom izvravanja programa pokazuje na razne elemente niza, kao da se pokaziva kree
odnosno klizi kroz elemente niza. Ovakve pokazivae obino nazivamo klizei pokazivai (engl. sliding
pointers), kao to je ilustrirano u sljedeem primjeru (u kojem pretpostavljamo da su niz i p deklarirani
kao u ranijim primjerima):
p = &niz[0];
for(int i = 0; i < 5; i++) {
std::cout << *p << " ";
p++;
}

Dereferenciranje i inkrementiranje (odnosno dekrementiranje) pokazivaa se esto obavlja u istom izrazu,


to je ilustrirano u sljedeem primjeru (koji je ekvivalentan prethodnom):
p = &niz[0];
for(int i = 0; i < 5; i++) std::cout << *p++ << " ";

U posljednjem primjeru, izraz *p++ interpretira se kao izraz *(p++) a ne kao izraz (*p)++ koji ima
drugaije, ranije objanjeno znaenje.
Oba navedena primjera su posve legalna i korektna. Meutim, sljedei primjer, koji bi trebao da ispie
elemente niza u obrnutom poretku, nije legalan (iako je sintaksno ispravan), bez obzira to e u veini sluajeva
raditi ispravno:
p = &niz[4];
for(int i = 0; i < 5; i++) {
std::cout << *p << " ";
p--;
}

// NELEGALNO: Na kraju petlje,


//
pokaziva p e "odlutati"
//
u nedozvoljeno podruje!!!

Isto vrijedi i za saetu varijantu ovog primjera:


p = &niz[4];
for(int i = 0; i < 5; i++) std::cout << *p-- << " ";

// NELEGALNO!

U emu je problem? Potekoa lei u injenici da nakon zavretka ovih sekvenci, pokaziva p pokazuje
ispred prvog elementa niza, to po standardu nije legalno. Operativni sistem ima pravo da prekine program u
12

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

kojem se generira takav pokaziva ukoliko mu njegovo postojanje ugroava rad (to se vrlo vjerovatno nee
desiti, ali se moe desiti). Rjeenje ovog problema je posve jednostavno nemojmo generirati takve pokazivae.
Na primjer, moemo p na poetku postaviti da pokazuje tano iza posljednjeg elementa niza, tj. na nepostojei
element niza niz[5] (to je, zaudo, legalno razloge za to emo shvatiti kasnije) i umanjivati pokaziva
prije nego to ga dereferenciramo, kao u sljedeem (legalnom) primjeru:
p = &niz[5];
for(int i = 0; i < 5; i++) {
p--;
std::cout << *p << " ";
}

ili u njegovoj kompaktnijoj varijanti:


p = &niz[5];
for(int i = 0; i < 5; i++) std::cout << *--p << " ";

Pokazivai se mogu porediti pomou operatora <, >, <=, >=, == i != pod uvjetom da
pokazuju na isti tip. Dva pokazivaa na razliite tipove (npr. pokazivai na tipove int i double) ne
mogu se porediti, ak i ukoliko su tipovi na koje pokazivai pokazuju uporedivi. Pokuaj poreenja
pokazivaa koji pokazuju na razliite tipove prijavljuje se kao sintaksna greka od strane kompajlera. Zbog
toga, pretpostavimo da imamo dva pokazivaa koji pokazuju na objekte istog tipa. Rezultat poreenja
primjenom operatora == i != je uvijek jasno definiran: dva pokazivaa su jednaka ako i samo ako
pokazuju na isti objekat. Meutim, rezultat poreenja dva pokazivaa primjenom ostalih relacionih operatora
je precizno definiran standardnom samo za sluaj kada oba pokazivaa pokazuju na elemente jednog te istog
niza. Na primjer, neka su p1 i p2 dva pokazivaa koji pokazuju na neke elemente istog niza (eventualno,
oni smiju pokazivati i na fiktivni prostor iza posljednjeg elementa niza). Pokaziva p1 je manji od pokazivaa
p2 ukoliko p1 pokazuje na element niza sa manjim indeksom u odnosu na element na koji pokazuje p2,
a vei ukoliko pokazuje na element niza sa veim indeksom u odnosu na element na koji pokazuje p2. S
druge strane, ukoliko pokazivai p1 i p2 ne pokazuju na dva elementa istog niza, pojmovi vei i manji
nisu definirani, tako da rezultat poreenja zavisi od implementacije. Stoga se takva poreenja ne smiju koristiti.
Ukoliko je memorijski model linearan, sva poreenja pokazivaa se obino interno realiziraju prostim
poreenjem brojanih vrijednosti adresa pohranjenih u pokazivau, tako da su rezultati poreenja praktino
uvijek u skladu sa intuicijom, ak i ukoliko krimo pravila kada je poreenje pokazivaa legalno (npr. kod
linearnih modela uvijek je manji onaj pokaziva koji pokazuje na niu adresu). Meutim, kod memorijskih
modela koji nisu linearni, svakakva iznenaenja su mogua. Da biste sprijeili mogua iznenaenja, samo
potujte pravilo: nemojte porediti po veliini pokazivae koji ne pokazuju na elemente istog niza. Takoer,
bitno je uoiti razliku izmeu poreenja poput p1 == p2 koje poredi dva pokazivaa (koji mogu biti razliiti
ak i ukoliko su sadraji lokacija na koje oni pokazuju isti) i poreenja poput *p1 == *p2 koje poredi
sadraje lokacija na koje pokazuju dva pokazivaa (koji mogu biti isti ak i ukoliko su pokazivai razliiti).
Mada nije mogue sabrati, pomnoiti ili podijeliti dva pokazivaa, mogue je oduzeti jedan pokaziva od
drugog, pod uvjetom da su oba pokazivaa istog tipa. Pri tome, rezultat oduzimanja je, slino kao za sluaj
poreenja, precizno definiran samo za sluaj kada oba pokazivaa pokazuju na elemente istog niza. Po
definiciji, rezultat oduzimanja takva dva pokazivaa je cijeli broj (a ne pokaziva) koji je jednak razlici indeksa
elemenata niza na koje pokazivai pokazuju. Na primjer, ukoliko pokaziva p1 pokazuje na element niza
niz[4] a pokaziva p2 na element niza niz[1], tada je vrijednost izraza p1 p2 cijeli broj 3 (tj.
4 1). Ovakva definicija obezbjeuje da vrijedi pravilo da je vrijednost izraza p1 + (p2 p1) uvijek jednaka
vrijednosti p2, to je u skladu sa intuicijom.
Zahvaljujui mogunosti poreenja pokazivaa, prethodni primjeri za ispis elemenata niza uz koritenje
klizeih pokazivaa mogu se napisati tako da se uope ne koristi pomona promjenljiva i, ve samo klizei
pokaziva. Na primjer, elemente niza u standardnom poretku moemo ispisati ovako:
for(double *p = &niz[0]; p < &niz[5]; p++)
std::cout << *p << " ";

Pri ispisu unazad ponovo treba biti oprezniji, zbog mogunosti da pokaziva odluta ispred prvog
elementa niza. Na primjer, sljedei primjer nije korektan (u skladu sa standardom):
for(double *p = &niz[4]; p >= &niz[0]; p--)
std::cout << *p << " ";

13

// NELEGALNO!!

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

Naime, nakon to p dostigne prvi element niza, vrijednost izraza p postaje nedefinirana, pa je pogotovo
nedefinirano ta je rezultat poreenja p >= &niz[0]. Stoga je veoma upitno kada e napisana petlja uope
zavriti (i da li e uope zavriti). Ovo nije samo prazno filozofiranje: napisani primjer provjereno nee raditi
sa C i C++ kompajlerima za MS DOS operativni sistem (kod kojeg memorijski model nije linearan) u
sluajevima kada je niz niz deklariran kao statiki ili globalni. Sigurno je mogue navesti jo mnotvo
situacija u kojima ovaj primjer ne radi kako treba. Rjeenje ovog problema je isto kao u ranijim primjerima:
ne smijemo dozvoliti da pokazivaka aritmetika dovede do izlaska pokazivaa izvan dozvoljenog opsega.
Sljedea varijanta je, na primjer, korektna (sjetimo se da se u for petlji bilo koji od njena tri argumenta
mogu izostaviti u ovom primjeru izostavljen je trei argument):
for(double *p = &niz[5]; p > &niz[0];)
std::cout << *--p << " ";

Treba naglasiti da su veoma rijetke knjige o programskim jezicima C i C++ u kojima se upozorava na opisanu
opasnost pokazivake aritmetike, koja posebno dolazi do izraaja kada klizee pokazivae koristimo za
kretanje kroz niz nanie.
Pokazivai su tijesno povezani sa nizovima. Naime, kad god se ime niza upotrijebi samo za sebe, bez
navoenja indeksa u uglastim zagradama, ono se automatski konvertira u pokaziva (odgovarajueg tipa) koji
pokazuje na prvi element niza (preciznije, u konstantni pokaziva, to emo precizirati malo kasnije). Drugim
rijeima, ukoliko je niz neki niz, tada je izraz niz potpuno ekvivalentan izrazu &niz[0]. Zbog
automatske konverzije nizova u pokazivae, slijedi da se pokazivaka aritmetika moe primijeniti i na nizove.
Razmotrimo, na primjer, izraz niz + 2. Ovaj izraz je, na prvi pogled, besmislen. Meutim, on je ekvivalentan
izrazu &niz[0] + 2 koji je, zahvaljujui pokazivakoj aritmetici (ne zaboravimo da je izraz &niz[0]
pokaziva), ekvivalentan izrazu &niz[2], to je lako provjeriti. Dakle, izrazi niz + 2 i &niz[2] su
ekvivalentni. Slijedi da elemente niza niz moemo ispisati i ovako:
for(double *p = niz; p < niz + 5; p++) std::cout << *p << " ";

Naravno, iz istog razloga, sasvim je legalna i sljedea konstrukcija:


for(int i = 0; i < 5; i++) std::cout << *(niz + i) << " ";

Iz ovoga vidimo da su, zahvaljujui pokazivakoj aritmetici i automatskoj konverziji nizova u pokazivae,
izrazi niz[i] i *(niz + i), gdje je niz ma kakva nizovna promjenljiva, zapravo sinonimi. Ovim je u
jezicima C i C++ uspostavljena veoma tijesna veza izmeu pokazivaa i nizova. Ovi jezici su pomenutu vezu
uinili jo tijesnijom (na nain kakav ne postoji niti u jednom drugom programskom jeziku) uvoenjem
konvencije da izrazi oblika a[n] i *(a + n) uvijek budu potpuni sinonimi, bez obzira da li je a ime
niza ili pokaziva. Drugim rijeima, indeksiranje se moe primijeniti i na pokazivae! Jednostavno, izraz
oblika a[n] u sluaju kada a nije ime niza, po definiciji se interpretira kao *(a + n), bez obzira kojeg
je tipa a. Stoga je izraz a[n] ispravan kad god se zbir a + n moe interpretirati kao pokaziva (jedna
pomalo udna posljedica ove injenice je da su sinonimi i izrazi poput niz[3] i 3[niz] mada se ovo
svojstvo teko moe iskoristiti za neto pametno). Indeksiranje primjenjeno na pokazivae ilustriraemo
sljedeim primjerom koji ispisuje elemente 4.23, 7, 3.441 i 12.9 (uz pretpostavku da je niz niz deklariran
kao u prethodnim primjerima) i koji pokazuje kako se u jeziku C++ mogu simulirati nizovi sa negativnim
indeksima kakvi postoje u nekim drugim programskim jezicima (npr. u Pascalu i FORTRAN-u):
double *p(niz + 2);
for(int i = 2; i <= 1; i++) std::cout << p[i] << " ";

Podsjetimo se da izraz niz + 2 ima isto znaenje kao i izraz &niz[2]. Kasnije emo vidjeti i mnoge
druge korisne primjene injenice da se indeksiranje moe primijeniti na pokazivae. Meutim, treba
napomenuti da sljedei primjer, koji se ak moe nai u nekim knjigama o jezicima C i C++, nije legalan.
Programer je htio da iskoristi indeksiranje pokazivaa sa ciljem simulacije nizova iji indeksi poinju od
jedinice umjesto od nule:
double *p(niz 1);
for(int i = 1; i <= 5; i++) std::cout << p[i] << " ";

// NELEGALNO!

Problem sa ovim primjerom je u tome to izraz niz 1 nije legalan, u skladu sa ve opisanim pravilima
vezanim za pokazivaku aritmetiku (isto vrijedi i za njemu ekvivalentan izraz &niz[1]). Stoga ovaj primjer
moe ali i ne mora raditi, ovisno od konkretnog kompajlera i operativnog sistema. Postoji velika vjerovatnoa

14

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

da e on raditi ispravno (inae ga ne bi citirali u mnogim knjigama), ali postoji vjerovatnoa i da nee. To je,
bez sumnje, sasvim dovoljan razlog da ne koristimo ovakva rjeenja.
Bez obzira na tijesnu vezu izmeu pokazivaa i nizova, na imena nizova ne mogu se primijeniti operatori
+=, =, ++ i koji mijenjaju sadraj pokazivake promjenljive, odnosno lokaciju na koju oni
pokazuju. Imena nizova (sama za sebe, bez indeksiranja) uvijek se tretiraju kao pokazivai na prvi element
niza, i to se ne moe promijeniti. Oni su uvijek vezani za prvi element niza. Preciznije, ime niza upotrijebljeno
samo za sebe konvertira se u konstantni (nepromjenljivi) pokaziva na prvi element niza. Stoga se imena
nizova ne mogu koristiti kao klizei pokazivai.
Treba napomenuti da su jedini izuzeci u kojima se ime niza upotrijebljeno samo za sebe ne konvertira u
odgovarajui pokaziva sluajevi kada se ime niza upotrijebi kao argument operatora sizeof ili &. Tako,
npr. sizeof niz daje veliinu itavog niza a ne veliinu pokazivaa. Takoer, izraz &niz ne predstavlja
adresu pokazivaa, ve adresu prvog elementa niza. Naravno, izrazi niz ili &niz[0] takoer predstavljaju
adresu prvog elementa niza, ali oni nisu ekvivalentni izrazu &niz. Naime, tip izraza niz ili &niz[0]
je pokaziva na element niza (npr. pokaziva na cijeli broj). S druge strane, tip izraza &niz (koji se inae
vrlo rijetko koristi) je pokaziva na itav niz (odnosno pokaziva na niz kao cjelinu). Drugim rijeima,
tipovi izraza niz (ili &niz[0]) i &niz se razlikuju (iako im je vrijednost ista). Pokazivai na nizove
kao cjeline koriste se jedino kod dinamike alokacije viedimenzionalnih nizova, i o njima emo govoriti
kada budemo obraivali tu tematiku.
Kako se nizovi po potrebi automatski konvertiraju u pokazivae kada se upotrijebe bez indeksa, to e
svaka funkcija koja kao svoj formalni parametar oekuje pokaziva na neki tip kao stvarni parametar
prihvatiti niz elemenata tog tipa (naravno, formalni parametar e pri tome pokazivati na prvi element niza).
Meutim, interesantno je da vrijedi i obrnuta situacija: funkciji koja kao formalni parametar oekuje niz
moemo kao stvarni parametar proslijediti pokaziva odgovarajueg tipa. Zapravo, jezici C i C++ uope ne
prave razliku izmeu formalnog parametra tipa pokaziva na tip Tip i formalnog parametra tipa Niz
elemenata tipa Tip (u oba sluaja funkcija dobija samo adresu prvog elementa niza). Drugim rijeima,
formalni parametri nizovnog tipa, ne samo da se mogu koristiti kao pokazivai, ve oni zaista i jesu pokazivai,
bez obzira to u opem sluaju, nizovi i pokazivai nisu u potpunosti ekvivalentni, kao to smo ve naglasili.
Zbog tog razloga, operator sizeof nee dati ispravnu veliinu niza ukoliko se primijeni na formalni
argument nizovnog tipa (naime, kao rezultat emo dobiti veliinu pokazivaa).
injenica da su formalni argumenti nizovnog tipa zapravo pokazivai, omoguava nam da funkcije koje
obrauju nizove moemo realizirati i preko klizeih pokazivaa. Na primjer, funkciju koja e ispisati na ekranu
elemente niza realnih brojeva moemo umjesto uobiajenog naina (sa for-petljom i indeksiranjem elemenata
niza) realizirati i na sljedei nain:
void IspisiNiz(double *pok, int n) {
for(int i = 0; i < n; i++) std::cout << *pok++ << std::endl;
}

Ovu funkciju moemo pozvati na posve uobiajeni nain:


IspisiNiz(niz, 5);

Nikakve razlike ne bi bilo ni da smo formalni parametar deklarirali kao double pokazivac[]. U oba sluaja
on bi se mogao koristiti kao klizei pokaziva. Dakle, za formalne parametre nizovnog tipa ne vrijedi pravilo
da su uvijek vezani iskljuivo za prvi element niza. Oni se mogu koristiti u potpunosti kao i pravi pokazivai
(iz prostog razloga to oni to i jesu). Zapravo, mogunost da se formalni parametri deklariraju kao da su
nizovnog tipa podrana je samo sa ciljem da se jasnije izrazi namjera da funkcija kao stvarni parametar
oekuje niz. Deklaracija formalnog parametra kao pokazivaa poetniku bi svakako djelovala mnogo nejasnije.
Mogue je deklarirati i pokazivae na konstantne objekte. Na primjer, deklaracijom
const double *p;

// Nekonstantni pokaziva na konstantni objekat

deklariramo pokaziva p na konstantnu realnu vrijednost (tip ovog pokazivaa je const double *, za
razliku od pokazivaa na nekonstantnu realnu vrijednost, iji je tip prosto double *). Ovakvi pokazivai
omoguavaju da se sadraj lokacije na koju oni pokazuju ita, ali ne i da se mijenja. Na primjer, sa ovakvim
pokazivaem izrazi std::cout << *p i broj = p[2] su dozvoljeni (pod uvjetom da je promjenljiva
broj deklarirana kao realna promjenljiva), ali izrazi *p = 2.5 i p[2] = 0 nisu. Odavde odmah vidimo

15

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

da se formalni parametar pok u prethodnoj funkciji mogao deklarirati kao const double *pok, to bi
ak bilo veoma preporuljivo. Naime, na taj nain bismo sprijeili eventualne nehotine promjene objekta na
koji pokaziva pok pokazuje.
Na ovom mjestu potrebno je razjasniti neke este zablude vezane za pokazivae na konstantne objekte.
Prvo, pokaziva na konstantni objekat nee uiniti objekat na koji on pokazuje konstantom. Na primjer, ako je
p pokaziva na konstantnu realnu vrijednost, a broj neka realna (nekonstantna) promjenljiva, dodjela
p = &broj nee uiniti promjenljivu broj konstantom. Promjenljiva broj e se i dalje moi mijenjati,
ali ne preko pokazivaa p! Ipak, adresu konstantnog objekta (npr. adresu neke konstante) mogue je
dodijeliti samo pokazivau na konstantni objekat. Takoer, pokazivau na nekonstantni objekat nije mogue
(bez eksplicitne primjene operatora za konverziju tipa) dodijeliti pokaziva na konstantni objekat (ak i
ukoliko su tipovi na koje oba pokazivaa pokazuju isti), jer bi nakon takve dodjele preko pokazivaa na
nekonstantni objekat mogli promijeniti sadraj konstantnog objekta, to je svakako nekonzistentno. Obrnuta
dodjela je legalna, tj. pokazivau na konstantni objekat mogue je dodijeliti pokaziva na nekonstantni objekat
istog tipa, s obzirom da takva dodjela ne moe imati nikakve tetne posljedice.
Treba primijetiti da se sadraj pokazivaa na konstantne objekte moe mijenjati, tj. sam pokaziva nije
konstantan. To treba razlikovati od konstantnih pokazivaa, ija se vrijednost ne moe mijenjati, ali se sadraj
objekata na koji oni pokazuju moe mijenjati (npr. imena nizova upotrijebljena sama za sebe konvertiraju se
upravo u konstantne pokazivae). Konstantni pokazivai mogu se deklarirati na sljedei nain (primijetimo da
se kvalifikator const ovdje pie iza zvjezdice):
double *const p(&niz[2]);

// Konstantni pokaziva na nekonstantni objekat

Primijetimo da se konstantni pokaziva mora odmah inicijalizirati, jer mu se naknadno vrijednost vie ne
moe mijenjati. Naravno, sada operacije poput p++ ili p += 2 koje bi promijenile sadraj pokazivaa p
nisu dozvoljene (s obzirom da je p konstantan pokaziva), ali je izraz p + 2 posve legalan (i predstavlja
konstantni pokaziva koji pokazuje na element niza niz[4]). Konano, mogue je deklarirati i konstantni
pokaziva na konstantni objekat, npr. deklaracijom poput sljedee:
const double *const p(&Niz[2]);

// Konstantni pokaziva na konstantni objekat

Moe se postaviti pitanje zato se uope patiti sa pokazivaima i koristiti pokazivaku aritmetiku ukoliko
se ista stvar moe obaviti i uz pomo indeksiranja. Ako zanemarimo dinamiku alokaciju memorije i dinamike
strukture podataka, o kojima e kasnije biti govora i koji se ne mogu izvesti bez upotrebe pokazivaa, osnovni
razlog je efikasnost i fleksibilnost. Naime, mnoge radnje se mogu neposrednije obaviti upotrebom klizeih
pokazivaa nego pomou indeksiranja, naroito u sluajevima kada se elementi niza obrauju sekvencijalno,
jedan za drugim. Pored toga, pokaziva koji pokazuje na neki element niza sadri u sebi cjelokupnu informaciju
koja je potrebna da se tom elementu pristupi. Kod upotrebe indeksiranja ta informacija je razbijena u dvije
neovisne cjeline: ime niza (odnosno adresa prvog elementa niza) i indeks posmatranog elementa.
Navedimo jedan klasini kolski primjer koji se navodi u gotovo svim udbenicima za programski jezik
C (neto rjee za C++, jer se C++ ne hvali toliko pokazivaima koliko C), koji ilustrira efikasnost upotrebe
klizeih pokazivaa. Naime, razmotrimo kako bi se mogla napisati funkcija nazvana KopirajString, sa
dva parametra od kojih su oba nizovi znakova, koja kopira sve znakove drugog niza u prvi niz, sve do NUL
graninika (odnosno znaka sa ASCII ifrom 0, koja po konvenciji jezika C predstavlja oznaku kraja niza
znakova, odnosno stringa), ukljuujui i njega. Zanemarimo ovdje injenicu da praktino identina funkcija,
pod imenom strcpy, ve postoji kao standardna funkcija u biblioteci cstring. Vjerovatno najoiglednija
verzija funkcije KopirajString mogla bi izgledati ovako (kvalifikator const kod imena nizovnog
parametra izvorni samo ukazuje da funkcija ne mijenja sadraj ovog niza):
void KopirajString(char odredisni[], const char izvorni[]) {
int i(0);
while(izvorni[i] != 0) {
odredisni[i] = izvorni[i];
i++;
}
odredisni[i] = 0;
// Smjetanje graninika u odredite
}

Zamijenimo sada parametre odredisni i izvorni klizeim pokazivaima (strogo uzevi, oni to ve
i sada jesu, ali emo ipak izmijeniti i njihovu deklaraciju, da jasnije istaknemo namjeru da ih elimo koristiti
kao klizee pokazivae). Odmah vidimo da nam indeks i vie nije potreban:
16

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

void KopirajString(char *odredisni, const char *izvorni) {


while(*izvorni != 0) {
*odredisni = *izvorni;
odredisni++; izvorni++;
}
*odredisni = 0;
}

Dalje, oba inkrementiranja moemo obaviti u istom izrazu u kojem vrimo dodjelu. Takoer, test razliitosti
od 0 moemo izostaviti (zbog automatske konverzije svake nenulte vrijednosti u logiku vrijednost true)
ime dobijamo sljedeu verziju funkcije:
void KopirajString(char *odredisni, const char *izvorni) {
while(*izvorni) *odredisni++ = *izvorni++;
*odredisni = 0;
}

Konano, kako je u jezicima C i C++ dodjela takoer izraz, rezultat izvrene dodjele je zapravo rezultat izraza
*izvorni (inkrementiranje se obavlja naknadno) koji je identian sa izrazom u uvjetu while petlje.
Stoga se funkcija moe svesti na sljedei oblik:
void KopirajString(char *odredisni, const char *izvorni) {
while(*odredisni++ = *izvorni++);
}

Djeluje nevjerovatno, zar ne!? Primijetimo da je nestala potreba ak i za dodjelom *odredisni = 0, jer je
lako uoiti da e ovako napisana petlja iskopirati i NUL graninik. Ovaj primjer veoma ilustrativno pokazuje
u kojoj mjeri se klizei pokazivai mogu iskoristiti za optimizaciju kda (dodue, po cijenu itljivosti i
razumljivosti, mada se na pokazivaku aritmetiku programeri brzo naviknu kada je jednom ponu koristiti).
Funkcija strcpy iz biblioteke cstring implementirana je upravo ovako. Odnosno, ne ba u potpunosti:
prava funkcija strcpy iz biblikoteke cstring implementirana je tano ovako (ako zanemarimo da su
imena promjenljivih u stvarnoj implementaciji vjerovatno na engleskom jeziku, to je naravno posve nebitno):
char *strcpy(char *odredisni, const char *izvorni) {
char *pocetak(odredisni);
while(*odredisni++ = *izvorni++);
return pocetak;
}

Praktino jedina razlika izmeu maloas napisane funkcije KopirajString i (prave) funkcije strcpy
je u tome to funkcija strcpy, pored toga to obavlja kopiranje, vraa kao rezultat pokaziva na prvi
element niza u koji se vri kopiranje (isto svojstvo posjeduje i funkcija strcat, takoer prisutna u
biblioteci cstring, koja nadovezuje niz znakova koji je zadan drugim parametrom na kraj niza znakova koji
je predstavljen prvim parametrom, odnosno vri dodavanje znakova iza nul-graninika prvog niza znakova,
uz odgovarajue pomjeranje graninika). Ovaj primjer ujedno demonstrira da funkcija moe kao rezultat vratiti
pokaziva. Povratna vrijednost koju vraaju funkcije strcpy i strcat najee se ignorira, kao to smo i
mi radili u svim dosadanjim primjerima. Meutim, ova povratna vrijednost moe se nekada korisno upotrijebiti
za ulanavanje funkcija strcpy i/ili strcat. Na primjer, posmatrajmo sljedeu sekvencu naredbi (uz
pretpostavku da su recenica, rijec1, rijec2 i rijec3 propisno deklarirani nizovi znakova):
std::strcpy(recenica, rijec1);
std::strcat(recenica, rijec2);
std::strcat(recenica, rijec3);
std::cout << recenica;

Zahvaljujui injenici da funkcije strcpy i strcat vraaju pokaziva na odredini niz, ove etiri
naredbe moemo kompaktnije zapisati u vidu sljedee naredbe:
std::cout << std::strcat(std::strcat(std::strcpy(recenica rijec1),
rijec2), rijec3);

Treba napomenuti da pomenuto svojstvo funkcija strcat i strcpy moe poetnika da dovede u
zabludu. Naime, mnogi poetnici esto isprobaju naredbu poput sljedee:
std::cout << std::strcat("Dobar ", "dan!");

17

// NELEGALNO!!!

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

Postoji velika vjerovatnoa da e ova naredba zaista ispisati pozdrav Dobar dan! na ekran (uskoro emo
vidjeti zato), iz ega poetnik moe zakljuiti da je ona ispravna. Meutim, ovakva naredba je strogo
zabranjena, jer dovodi do prepisivanja sadraja stringovne konstante. O ovoj temi treba malo detaljnije
prodiskutovati, s obzirom da ona moe biti izvor brojnih frustracija. Da bismo bolje objasnili ta se ovdje
deava, razmotrimo sljedeu naredbu (u kojoj se, ukoliko e to olakati razumijevanje, umjesto biblioteke
funkcije strcpy moe koristiti i maloas napisana funkcija KopirajString):
std::strcpy("abc", "def");

// VEOMA PROBLEMATINO!!!

Mada e ovu konstrukciju kompajler prihvatiti (u boljem sluaju uz prijavu upozorenja), ona zapravo
prepisuje sadraj memorije u kojem se uva stringovna konstanta "abc" sa sadrajem stringovne konstante
"def". Posljedice ovog su posve nepredvidljive. Jedna mogunost je da e svaka upotreba stringovne
konstante "abc" u programu dovesti do efekta kao da je upotrijebljena stringovna konstanta "def". Druga
mogunost je da doe do prekida rada programa od strane operativnog sistema (ovo se moe desiti ukoliko
kompajler sadraje stringovnih konstanti uva u dijelu memorije u kojem operativni sistem ne dozvoljava
nikakav upis). Mada nekome efekat zamjene jedne stringovne konstante drugom moe djelovati kao zgodan
trik, njegova upotreba je, ak i u sluaju da je konkretan kompajler i operativni sistem dozvoljavaju, izuzetno
loa taktika koju ne bi trebalo koristiti ni po koju cijenu (injenica da je po standardu jezika C ovakav poziv
naelno dozvoljen ne znai da ga treba ikada koristiti). Jo gore posljedice moe imati naredba
std::strcpy("abc", "defghijk");

// FATALNA GREKA!!!

U ovom sluaju se dua stringovna konstanta "defghijk" pokuava prepisati u dio memorije u kojem se
uva kraa stringovna konstanta "abc", to ne samo da e prebrisati ovu stringovnu konstantu, nego e i
prebrisati sadraj nekog dijela memorije za koji ne znamo kome pripada (za stringovnu konstantu "abc"
rezervirano je tano onoliko prostora koliko ona zauzima, tako da ve prostor koji slijedi moe pripadati
nekom drugom). Naravno, posljedice su potpuno nepredvidljive i nikada nisu bezazlene. Kao zakljuak,
nikada ne smijemo stringovnu konstantu proslijediti kao stvarni parametar nekoj funkciji koja mijenja sadraj
znakovnog niza koji joj je deklariran kao formalni parametar (ovo je tako opasna stvar da bolji kompajleri
imaju opciju kojom se moe ukljuiti da se takav pokuaj tretira kao sintaksna greka). Kao konkretan
primjer, kao prvi argument funkcije strcpy (odnosno KopirajString) smije se zadavati iskljuivo niz
za koji eksplicitno znamo da mu se sadraj smije mijenjati (recimo, neka promjenljiva nizovnog tipa), i koji je
dovoljno velik da primi sadraj niza koji se u njega kopira.
Sada postaje jasno ta nije u redu sa pozivom std::strcat("Dobar ", "dan!"). Naime, ovaj poziv
bi pokuao da nadovee stringovnu konstantu "dan!" na kraj onog dijela memorije gdje se uva stringovna
konstanta "Dobar ", ime ne samo da bi promijenio sadraj ove stringovne konstante, nego bi i prebrisao
sadraj memorijskih lokacija koje joj ne pripadaju. Posljedice ovakvog napada mogu se odraziti znatno
kasnije u programu, to moe biti veoma frustrirajue. Ako nam operativni sistem odmah prekine izvoenje
takvog programa zbog zabranjenih akcija, jo moemo smatrati da smo imali sree. Stoga, moemo zakljuiti
da funkciju strcat ima smisla koristiti samo ukoliko je njen prvi argument niz koji ve sadri neki string
(eventualno prazan) i koji je dovoljno veliki da moe prihvatiti string koji e nastati nadovezivanjem. Moemo
istu stvar rei i ovako: korisnici koji poznaju neke druge programske jezike mogu pomisliti da funkcija
strcat vraa kao rezultat string koji je nastao nadovezivanjem prvog i drugog argumenta (ne mijenjajui
svoje argumente), s obzirom da takve funkcije postoje u mnogim programskim jezicima. Meutim, to nije
sluaj sa funkcijom strcat iz jezika C++: ona nadovezuje drugi argument na prvi, pri tome mijenjajui
prvi argument. U neku ruku, razlika izmeu funkcije strcat i srodnih funkcija u drugim programskim
jezicima najlake se moe uporediti sa razlikom koja postoji izmeu izraza x += y i x + y.
Pokazivaka aritmetika moe se veoma lijepo iskoristiti zajedno sa funkcijama koje barataju sa klasinim
nul-terminiranim stringovima u stilu jezika C, poput maloas napisane funkcije KopirajString, odnosno
bibliotekih funkcija iz biblioteke cstring poput strcpy, strcat, strcmp itd. Na primjer, ukoliko
je potrebno iz niza znakova recenica izdvojiti sve znakove od desetog nadalje u drugi niz znakova
recenica2, to najlake moemo uraditi na jedan od sljedea dva naina (oba su ravnopravna):
std::strcpy(recenica2, &recenica[9]);
std::strcpy(recenica2, recenica + 9);

Ista tehnika moe se iskoristiti i sa funkcijama koje prihvataju proizvoljne vrste nizova kao parametre,
odnosno koje prihvataju pokazivae na proizvoljne tipove. Na primjer, pretpostavimo da elimo iz ranije
deklariranog niza realnih brojeva niz ispisati samo drugi, trei i etvrti element (tj. elemente sa indeksima 1, 2

18

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

i 3). Nema potrebe da prepravljamo ve napisanu funkciju IspisiNiz, s obzirom da ovaj cilj moemo veoma
jednostavno ostvariti jednim od sljedea dva poziva, bez obzira da li je funkcija napisana da prihvata pokazivae
ili nizove kao svoj prvi argument, i bez obzira da li je napisana koritenjem indeksiranja ili klizeih pokazivaa:
IspisiNiz(&niz[1], 3);
IspisiNiz(niz + 1, 3);

Ova univerzalnost ostvarena je automatskom konverzijom nizova u pokazivae, kao i mogunou da se


indeksiranje primjenjuje na pokazivae kao da se radi o nizovima.
Tijesna veza izmeu nizova i pokazivaa i injenica da se nizovi znakova tretiraju donekle razliito od
ostalih nizova ima kao posljedicu da se i pokazivai na znakove tretiraju neznatno drugaije nego pokazivai
na druge objekte. Tako, ukoliko je p pokaziva na znak (tj. na tip char), izraz std::cout << p nee
ispisati internu reprezentaciju adrese pohranjene u pokazivau p (kao to bi vrijedilo za sve druge tipove
pokazivaa), nego redom sve znakove iz memorije poev od adrese smjetene u pokaziva p, pa sve do
NUL graninika (jasno je da se ova konvencija odnosi samo na jezik C++ a ne na C, s obzirom da jezik C ne
posjeduje objekat toka cout). Na ovaj nain je, kao prvo, ostvarena konzistencija sa tretmanom znakovnih
nizova od strane objekta izlaznog toka cout, a kao drugo, omogueni su neki interesantni efekti. Na
primjer, sljedea skupina naredbi
char recenica[] = "Tehnike programiranja";
char *p(&recenica[8]);
std::cout << p;

ispisae tekst programiranja na ekran. Naime, nakon izvrene dodjele, pokaziva p pokazuje na deveti
znak reenice smjetene u niz recenica (indeksiranje poinje od nule), a to je upravo prvi znak druge
rijei u nizu recenica, odnosno poetak dijela teksta koji glasi programiranja. Naravno, umjesto izraza
&recenica[8] mogli smo pisati i recenica + 8 (s obzirom da bi se u ovom primjeru upotreba imena
recenica bez indeksa automatski konvertirala u pokaziva na prvi element niza, nakon ega bi pokazivaka
aritmetika odradila ostatak posla). Takoer, uope nismo ni morali uvoditi pokazivaku promjenljivu p:
mogli smo prosto pisati
char recenica[] = "Principi programiranja";
std::cout << &recenica[8];

odnosno
char recenica[] = "Principi programiranja";
std::cout << recenica + 8;

Isti efekat proizvela bi ak i sljedea naredba (razmislite zato):


std::cout << "Principi programiranja" + 8;

Pokazivai na znakove su jedini pokazivai koji se mogu koristiti sa ulaznim tokom cin (jasno je da
se i ova napomena odnosi samo na jezik C++). Tako, ako je p pokaziva na znak, izvravanjem izraza
std::cin >> p znakovi proitani iz ulaznog toka (do prvog razmaka, u skladu sa ustaljenim ponaanjem
operatora >>) smjestie se redom u memoriju poev od adrese smjetene u pokazivau p. Slino vrijedi i
za druge konstrukcije za unos, poput std::cin.getline(p, 100). Zajedno sa pokazivakom aritmetikom,
ovo se moe iskoristiti za razne interesantne efekte. Na primjer, pomou sekvence naredbi
char recenica[100] = "Poetak: ", *p(&recenica[9]);
std::cin >> p;

sa tastature e se unijeti jedna rije, i smjestiti u niz recenica poev od desetog znaka, odnosno tano iza
teksta Poetak: . I u ovom primjeru smo mogli izbjei uvoenje promjenljive p i pisati direktno izraz
poput std::cin >> &recenica[9] ili std::cin >> recenica + 9. Ukoliko bismo umjesto jedne rijei
eljeli unijeti itavu liniju teksta, koristili bismo konstrukciju poput std::cin.getline(p, 91) odnosno
std::cin.getline(&recenica[9], 91) ili std::cin.getline(recenica + 9, 91) ako pored toga
elimo i da izbjegnemo uvoenje promjenljive p. Maksimalna duina 91 odreena je tako to je od
maksimalnog kapaciteta od 100 znakova odbijen prostor koji je ve rezerviran za tekst Poetak: .
Pokazivai i pokazivaka aritmetika omoguavaju prilino mone trikove, koje bi inae bilo znatno tee
realizirati. S druge strane, pokazivai mogu biti i veoma opasni, jer lako mogu odlutati u neko nepredvieno

19

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

podruje memorije, to omoguava da pomou njih indirektno promijenimo praktiki bilo koji dio memorije u
kojem su doputene izmjene (posljedice ovakvih intervencija su uglavnom nepredvidljive i esto fatalne). Na
primjer, ve smo rekli da je smisao pokazivake aritmetike precizno definiran samo za pokazivae koji pokazuju
na neki element niza, i to samo pod uvjetom da novodobijeni pokaziva ponovo pokazuje na neki element
niza, ili eventualno tano iza posljednjeg elementa niza. Ukoliko se ne pridravamo ovih pravila, pokazivai
mogu odlutati u nedozvoljena podruja memorije. Razmotrimo, na primjer, sljedeu sekvencu naredbi:
int a(5), *p(&a);
p++;
*p = 3;

// NELEGALNO: p ne pokazuje na element nekog niza!

Ovdje smo prvo deklarirali cjelobrojnu promjenljivu a koju smo inicijalizirali na vrijednost 5 , a
zatim pokazivaku promjenljivu p koju smo inicijalizirali tako da pokazuje na promjenljivu a (odnosno,
ona sada sadri adresu promjenljive a). Nakon toga je na promjenljivu p primijenjen operator ++. Na
ta sada pokazuje promjenljiva p? Strogo uzevi, odgovor uope nije definiran u skladu sa pravilima koja
po standardu vrijede za pokazivaku aritmetiku, ali u skladu sa nainom kako se pokazivaka aritmetika
implementira u veini kompajlera, p e gotovo sigurno pokazivati na prostor u memoriji koji se nalazi
neposredno iza prostora koji zauzima promjenljiva a. Nakon toga e dodjela *p = 3 na to mjesto u
memoriji upisati vrijednost 3 . Meutim, ta se nalazi na tom mjestu u memoriji? Odgovor je nepredvidljiv:
moda neka druga promjenljiva (ovo je najvjerovatnija mogunost), moda nita pametno, a moda neki
podatak od vitalne vanosti za rad operativnog sistema! Samim tim, posljedica ove dodjele je nepredvidljiva.
U svakom sluaju, dodjelom *p = 3, mi smo zapravo napali memorijski prostor iju svrhu ne znamo, to
je strogo zabranjeno. Za pokaziva koji je odlutao tako da vie ne znamo na ta pokazuje kaemo da je
divlji pokaziva (engl. wild pointer). To ne mora znaiti da ne znamo koja je adresa pohranjena u njemu, nego
samo da ne znamo kome pripada adresa koju on sadri. Divlji pokazivai nikada se ne bi trebali pojaviti u
programu. Naalost, greke koje nastaju usljed divljih pokazivaa veoma se teko otkrivaju, a prilino ih je
lako napraviti. Zbog toga se u literaturi esto citira da su pokazivai zloesti (engl. pointers are evil), i oni
predstavljaju drugi tipini zloesti koji postoji u jezicima C i C++ (prvi zloesti koncept su nizovi, koji
postaju zloesti ukoliko indeks niza odluta izvan dozvoljenog opsega). Smatra se da oko 90% svih
fatalnih greaka u dananjim profesionalnim programima nastaje upravo zbog ilegalnog pristupa memoriji
preko divljih pokazivaa!
Treba primijetiti da je i pokaziva koji pokazuje tano iza posljednjeg elementa niza takoer divlji
pokaziva, jer i on pokazuje na lokaciju koja ne pripada nizu (niti se zna kome ona zapravo pripada).
Meutim, standard jezika C++ kae da je takav pokaziva legalan. Nije li ovo kontradikcija? Stvar je u tome
to je dozvoljeno generirati odnosno kreirati pokaziva koji pokazuje iza posljednjeg elementa niza. Pored
toga, garantira se da je on vei od svih pokazivaa koji pokazuju na elemente tog niza. Ipak, to jo uvijek ne
znai da takav pokaziva smijemo dereferencirati. Pokuaj njegovog dereferenciranja takoer moe imati
nepredvidljive posljedice. Meutim, njega bar smijemo kreirati bez opasnosti. Druge vrste divljih pokazivaa
ponekad ne smijemo ni kreirati moe se desiti da i smo njihovo kreiranje dovede do kraha, ak i ukoliko se
oni uope ne dereferenciraju (to se, u nekim rijetkim sluajevima, moe desiti npr. ukoliko primjenom
pokazivake aritmetike pokaziva odluta ispred prvog elementa niza). Dalje, sve operacije sa njima (npr.
poreenje, itd.) potpuno su nedefinirane. U tom smislu, pokaziva koji pokazuje neposredno iza elementa
niza legalan je u smislu da ga smijemo kreirati (tj. njegovo kreiranje garantirano nee izazvati krah) i smijemo
ga porediti sa drugim pokazivaima, jedino ga ne smijemo dereferencirati. Sa drugim divljim pokazivaima
ne smijemo raditi apsolutno nita. ak i sm pokuaj njihovog kreiranja (koji tipino nastaje neispravnom
upotrebom pokazivake aritmetike) moe biti fatalan po program!
U vezi sa divljim pokazivaima neophodno je naglasiti jednu nezgodnu osobinu: svi pokazivai su
neposredno nakon deklaracije divlji (osim u sluajevima kada se uporedo sa deklaracijom vri i inicijalizacija),
sve dok im se eksplicitno ne dodijeli vrijednost! Naime, kao i sve ostale promjenljive, njihova vrijednost je
nedefinirana neposredno nakon deklaracije (osim ukoliko se istovremeno vri i inicijalizacija), sve do prve
dodjele. Tako je i sa pokazivaima: njihova poetna vrijednost je u odsustvu inicijalizacije nedefinirana, tako
da se sve do prve dodjele ne zna ni na ta pokazuju. Na ovo smo se ve ukratko osvrnuli prilikom razmatranja
raznih neispravnih realizacija funkcije Razmijeni. Recimo, sljedea sekvenca naredbi ima nepredvidljive
posljedice jer napada nepoznati dio memorije, s obzirom da se ne zna na ta pokaziva p pokazuje:
int *p;
*p = 10;

// NELEGALNO: Gdje se smjeta ovaj broj 10???

Slinu situaciju imamo u sljedeem katastrofalno opasnom neispravnom primjeru, koji se veoma esto moe
sresti kod poetnika:

20

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

char *p;
std::cin >> p;
std::cout << p;

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

// NELEGALNO: Gdje se smjeta uneseni tekst???

Mada onome ko isproba navedeni primjer moe izgledati da on radi korektno (odnosno da ispisuje na ekran
rije prethodno unesenu sa tastature), on sadri katastrofalnu greku. Naime, kako je p neinicijaliziran
pokaziva, njegov sadraj je nepredvidljiv, pa je i potpuno nepredvidljivo gdje e se uope u memoriji
smjestiti znakovi proitani sa tastature! Moe se desiti da se program koji sadri ovakve naredbe srui tek
nakon vie sati, ukoliko se ispostavi da su smjeteni znakovi upropastili neke bitne podatke u memoriji.
Moe se desiti da u startu p pokazuje na neki dio memorije zabranjen za upis od strane operativnog sistema.
U tom sluaju program e se sruiti odmah (zapravo, operativni sistem e prekinuti njegovo izvravanje). U
tom sluaju ak moemo rei da smo imali sree!
Opisani problem nije specijalnost samo jezika C++. Sa slinim problemom se poetnici susreu u
jeziku C. Naime, prethodni problematini primjer, napisan u duhu jezika C, izgledao bi otprilike ovako:
char *p;
std::scanf("%s", p);
std::printf("%s", p);

// NELEGALNO: Gdje se smjeta uneseni tekst???

Da li ste ikada napisali neto poput ovoga? Sasvim je vjerovatno da jeste. Da li ste nakon toga bili ubjeeni da
se smije ovako pisati? Ukoliko ste ve napisali tako neto, skoro ste bili sigurni da smije. Sad znate ne samo
da ne smije, nego da je u pitanju katastrofalna greka. ovjek ui na grekama i ui dok je iv, ali je alosno
to ovakve primjere navode (mislei da su ispravni) ak i neki pojedinci koji bi druge trebali uiti osnovama
programiranja...
Postoji jedan specijalni pokaziva koji se naziva nul-pokaziva i po konvenciji se smatra da on ne
pokazuje ni na ta. Pokazivaima se obino dodjeljuje nul-pokaziva onog trenutka kada elimo rei da u tom
trenutku pokaziva vie ne pokazuje ni na ta korisno. Isto tako, pokaziva se odmah prilikom kreiranja moe
inicijalizirati na nul-pokaziva (to se toplo preporuuje) ukoliko u tom trenutku jo ne znamo na ta on treba
pokazivati. Sve do pojave standarda C++11, za nul-pokazivae se prosto koristio simbol nula (0), kao za
obian broj nula. Drugim rijeima, ukoliko smo pokazivau p eljeli pridruiti nul-pokaziva, prosto smo
vrili dodjelu poput p = 0. Isto tako, ukoliko smo eljeli inicijalizirati pokaziva p na nul-pokaziva
prilikom deklaracije, morali smo pisati deklarasije poput sljedeih:
int *p = 0;
int *p(0);

// U duhu jezika C...


// U C++ stilu...

Meutim, koritenje nule kao onzake za nul-pokaziva je pomalo konfuzno, jer daje iluziju da se pokazivaima
mogu dodjeljivati brojevi, a ne mogu (osim nule, a i ta nula nije zaista nula nego nul-pokaziva). tavie,
prema C++ standardu nul-pokaziva uope ne mora interno biti predstavljen brojem nula (tj. uope u njemu
ne mora biti adresa nula, mada u veini implementacija jeste). Pokuaj da se smanji pomenuta konfuzija bio
je uvoenje simbolike konstante NULL koja je definirana u raznim bibliotekama (poput cstring,
cstdlib itd.) koje ine sastavni dio jezika C i C++ (u jeziku C je ova konstanta pokazivakog a ne
brojanog tipa, to je uinjeno sa diljem da se sprijei da se ona koristi u ne-pokazivakim kontekstima, ali je
iz nekih tehnikih razloga u jeziku C++ uzeto da je ona bukvalno sinonim za broj 0). Ipak, mnogi su radije
koristili obian broj 0 umjesto konstante NULL, da ne bi pravili nepotrebne pretpostavke o ukljuenosti
pojedinih biblioteka u program. Situacija je konano raiena u standardu C++11 jezika C++, kada je
uvedena ova kljuna rije nullptr koja oznaava nul-pokaziva. Nju bi sada trebalo uvijek koristiti
umjesto broja 0 za modeliranje nul-pokazivaa. Vrijednost nullptr je pokazivake prirode i ne moe se
koristiti u ne-pokazivakim kontekstima. Tako, u skladu sa C++11 standardom, inicijalizaciju pokazivaa p
na nul-pokaziva trebamo izvriti na jedan od sljedeih naina:
int *p = nullptr;
int *p(nullptr);
int *p{nullptr};

// C-ovski stil, ne preporuuje se...


// Klasini C++ stil
// Jednoobrazni stil

Konvencija da se pokaziva koji ne pokazuje ni na ta eksplicitno postavlja na nul-pokaziva moe


uiniti neke divlje pokazivae manje divljim. Oni su i dalje divlji u smislu da ne pokazuju ni na ta
smisleno, tako da ih ne smijemo dereferencirati. Meutim, njihov sadraj nije nepredvidljiv, nego je tano
odreen, tako da programski moemo testirati (poreenjem sa nul-pokazivaem) da li on pokazuje na neto
smisleno ili ne. Pored oiglednih testova tipa p == nullptr odnosno p != nullptr (prihvataju se i
testovi p == 0 odnosno p != 0 koji su se koristili prije nego to je kljuna rije nullptr uvedena),
jezici C i C++ dozvoljavaju i smo navoenje izraza !p odnosno p unutar uvjeta if ili while naredbe
21

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

(pri tome se svi pokazivai automatski tretiraju kao logika vrijednost tano, osim nul-pokazivaa koji se
tretira kao netaan).
Nul-pokazivai se takoer esto vraaju kao rezultati iz funkcija koje kao rezultat vraaju pokazivae, u
sluaju da nije mogue vratiti neku drugu smislenu vrijednost. Na primjer, biblioteka cstring sadri veoma
korisnu funkciju strstr koja prihvata dva nul-terminirana stringa kao parametre. Ova funkcija ispituje da
li se drugi string sadri u prvom stringu (npr. string je lijep sadri se u stringu danas je lijep dan). Ukoliko
se nalazi, funkcija vraa kao rezultat pokaziva na znak unutar prvog stringa od kojeg poinje tekst identian
drugom stringu (u navedenom primjeru, to bi bio pokaziva na prvu pojavu slova j u stringu danas je lijep
dan). U suprotnom, funkcija kao rezultat vraa nul-pokaziva. Sljedei primjer demonstrira kako se ova
funkcija moe iskoristiti (obratite panju na igre sa pokazivakom aritmetikom):
char recenica[100], fraza[100];
std::cin.getline(recenica, sizeof recenica);
std::cin.getline(fraza, sizeof fraza);
char *pozicija(std::strstr(recenica, fraza));
if(pozicija != nullptr)
std::cout << "Navedena fraza se nalazi u reenici poev od "
<< pozicija - recenica + 1 << ". znaka\n";
else
std::cout << "Navedena fraza se ne nalazi u reenici\n";

Pored funkcije strstr, biblioteka cstring sadri i slinu funkciju strchr, samo to je njen drugi
parametar znak a ne string (prihvata se i brojana vrijednost, sa znaenjem ASCII ifre). Ona vraa pokaziva
na prvu pojavu navedenog znaka u stringu, ili nul-pokaziva ukoliko takvog znaka nema. Ova funkcija se moe
zgodno iskoristiti da se dobije pokaziva na NUL graninik stringa (za tu svrhu, funkciji strchr treba
proslijediti nulu kao drugi parametar). Biblioteka cstring sadri jo mnotvo korisnih funkcija koje kao
rezultat vraaju pokazivae, koje neemo opisivati s obzirom da bi njihov opis zauzeo previe prostora.
Napomenimo da su sve ove funkcije namijenjene iskljuivo za rad sa klasinim, nul-terminiranim stringovima,
tj. stringovima na nain kako ih tretira jezik C. Funkcije sline namjene postoje i za dinamike stringove (tj.
objekte tipa string uvedene u jeziku C++), samo to se koriste na neto drugaiji nain i one nikada ne
vraaju pokazivae kao rezultate (pokazivae i dinamike stringove nije dobro koristiti u istom kontekstu, jer
su oni konceptualno razliiti). Zainteresirani se upuuju se na mnogobrojnu literaturu koja detaljno obrauje
standardnu biblioteku string i operacije sa dinamikim stringovima. Vrijedi jo napomenuti da je, u sluaju
potrebe, legalno uzeti adresu nekog elementa dinamikog stringa i sa tako dobijenim pokazivaem (koji je
tipa pokaziva na znak, isto kao i u sluaju klasinih nul-terminiranih stringova) koristiti pokazivaku aritmetiku
(tj. garantira se da se elementi dinamikih stringova takoer uvaju u memoriji jedan za drugim). Tako dobijene
adrese su garantirano ispravne sve dok se duina stringa ne promijeni, nakon ega vie ne moraju biti (s
obzirom da promjena veliine stringa moe dovesti do pomjeranja njegovih elemenata u memoriji).
Poto je cilj ovog dodatka detaljno upoznavanje sa pokazivaima i njihovim svojstvima, recimo jo
nekoliko rijei o pokazivaima na znakove. Interesantno je da se pokazivai na konstantne znakove mogu
inicijalizirati tako da pokazuju na neku stringovnu konstantu, kao da se radi o nizovima znakova, koji se
mogu inicijalizirati na slian nain. Na primjer:
const char *p = "Ovo je neki string...";
std::cout << p;

Ovdje je takoer mogue (i preporuljivo) koristiti sintaksu sa zagradama (od verzije C++11 doputene su i
vitiaste zagrade, radi vee jednoobraznosti):
const char *p("Ovo je neki string...");
std::cout << p;

Ovakvim pokazivaima je ak mogue i naknadno dodijeliti stringovne konstante, pri emu ovakva dodjela
naravno ne dodjeljuje samu stringovnu konstantu pokazivakoj promjenljivoj (ona nema prostora da primi
cijeli niz znakova u sebe), ve samo adresu stringovne konstante. Meutim, bez obzira na tu injenicu, ovakva
poludodjela moe, zajedno sa specijalnim tretmanom pokazivaa na znakove, proizvesti interesantne
efekte. Na primjer, sljedei primjer je posve legalan:
const char *recenica;
recenica = "Ja sam prva reenica...";
std::cout << recenica << std::endl;
recenica = "A ja sam druga reenica...";
std::cout << recenica << std::endl;

22

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

S druge strane, slina konstrukcija nije mogua sa obinim nizovima znakova, nego se moraju koristiti ili
dinamiki stringovi (tj. objekti tipa string, uvedeni u jeziku C++), ili konstrukcije poput sljedee:
char recenica[100];
std::strcpy(recenica,
std::cout << recenica
std::strcpy(recenica,
std::cout << recenica

"Ja sam prva reenica...");


<< std::endl;
"A ja sam druga reenica...");
<< std::endl;

Veoma je vano uoiti sutinsku razliku izmeu dva prethodno navedena primjera. U prvom primjeru,
recenica je pokaziva, koji prvo pokazuje na jednu stringovnu konstantu, a zatim na drugu. S druge strane,
u drugom primjeru, recenica je niz znakova (za koji je rezerviran prostor od 100 mjesta za znakove), u
koji se prvo kopira jedna stringovna konstanta, a zatim druga. Ova razlika je veoma vana, mada na prvi
pogled djeluje nebitna, jer je krajnji efekat isti. Meutim, kako je u prvom primjeru recenica pokaziva na
konstantne znakove, izmjena sadraja na koji on pokazuje nije mogua. Stoga bi u primjeru poput sljedeeg
prevodilac prijavio greku:
const char *ime;
ime = "Elma";
ime[0] = 'A';
cout << ime;

// GREKA: "ime" pokazuje na KONSTANTNE znakove!

Na ovome bi sva pria o pokazivaima na znakove mogla da zavri da nije jedne nevolje. Naime, poev
od standarda C++98 jezika C++ nadalje, kompajleri ne bi trebali da dozvole da se obinim pokazivaima na
(nekonstantne) znakove dodjeljuju adrese stringovnih konstanti. Kaemo ne bi trebali, jer je injenica da
gotovo svi raspoloivi kompajleri za C++ dozvoljavaju takve dodjele (s obzirom da su one dugi niz godina
naelno bile dozvoljene, bez obzira to nisu smjele da budu po ranijim standardima stringovne konstante
zapravo uope nisu bile konstante). Stoga e sljedei primjer proi nekanjeno kroz veinu raspoloivih C++
kompajlera (i praktino sve kompajlere za C, s obzirom da jezik C ima znatno blai tretman konstanti,
odnosno ne tretira konstantnost tako strogo kao jezik C++):
char *ime;
ime = "Elma";
ime[0] = 'A';
std::cout << "Elma";

// Ovo ne bi trebalo da proe, meutim...


// Ova dodjela je vrlo problematina!

Repertoar moguih posljedica ove skupine naredbi je raznovrstan. Moda se program srui zbog toga to
ime pokazuje na stringovnu konstantu koja je moda smjetena u dio memorije koji je zabranjen za upis.
Ukoliko to nije sluaj, gotovo sigurno da emo na ekranu umjesto Elma imati ispisano Alma, bez obzira
to naredba ispisa eksplicitno ispisuje tekst Elma. Naime, obje pojave stringovne konstante Elma gotovo
sigurno se u memoriji uvaju na istom mjestu, pa e se izmjena sadraja stringovne konstante (izmjena
konstante je ve sama po sebi apsurdna) odraziti na svaku pojavu te stringovne konstante. Navedimo jo
jedan katastrofalan primjer koji se moe sresti kod poetnika (i koji kompajler ne bi trebao uope da dozvoli):
char *recenica;
recenica = "Ja sam recenica...";

// Ni ovo ne bi trebalo proi...

std::cin >> recenica;

// Ovo ima kobne posljedice!

Postavlja se pitanje gdje e se u memoriju smjestiti niz znakova koji unesemo sa tastature? Odgovor je jasan:
na najgore mogue mjesto preko znakova stringovne konstante Ja sam reenica...! Pored toga, ukoliko je
uneseni niz znakova dui od duine te stringovne konstante, viak znakova e pojesti i sadraj memorije koji
se nalazi iza ove stringovne konstante! Ukoliko nam operativni sistem odmah prekine ovakav program, imamo
mnogo sree. Gora varijanta je da program pone neispravno raditi tek nakon nekoliko sati, kad mu zatreba
sadraj unitenog dijela memorije!
Iz svega to je reeno bitno je shvatiti da se dodjela stringovnih konstanti pokazivaima na znakove
nipoto ne smije shvatiti kao lijepa i elegantna zamjena za funkciju strcpy (kako se propagira u nekim
udbenicima za jezik C), nego kao neto to treba koristiti sa izuzetnim oprezom, i to obavezno sa
kvalifikatorom const. Na taj nain e nas kompajler upozoriti pokuamo li izvriti nedozvoljeni napad na
memoriju. Napomenimo da su ovi komentari uglavnom namijenjeni onima koji imaju odreena predznanja u
jeziku C i ele da ta znanja nastave koristiti u jeziku C++. S druge strane, onima kojima programski jezik C
nije jaa strana, kao i onima kojima je C++ prvi programski jezik sa kojim se susreu, savjetuje se da za

23

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

bilo kakav ozbiljniji rad sa stringovima ne koristite niti nul-terminirane nizove znakova niti pokazivae na
znakove, ve dinamike stringove, odnosno tip string!
Mada je reeno da pokazivaima nije mogue eksplicitno dodjeljivati brojeve, niti je mogue pokazivau
na jedan tip dodijeliti pokaziva na drugi tip, postoji indirektna mogunost da se ovakva dodjela ipak izvri,
pomou operatora za pretvorbu tipova (typecasting operatora). Na primjer, neka imamo sljedee deklaracije:
int *pok_na_int;
double *pok_na_double;

Tada pretvorba tipova dozvoljava, da na sopstveni rizik, izvrimo i ovakve dodjele:


pok_na_int = (int *)3446;
pok_na_double = (double *)pok_na_int;

// RIZINO!
// RIZINO!

Prikazane konverzije tipova koriste sintaksu i stil jezika C. Ova sintaksa radi i u jeziku C++, mada se u jeziku
C++ preporuuje druga sintaksa, u kojoj se javlja kljuna rije reinterpret_cast (koja djeluje slino kao
i static_cast, samo slui za drastine pretvorbe pokazivakih tipova):
pok_na_int = reinterpret_cast<int *>(3446);
pok_na_double = reinterpret_cast<double *>(pok_na_int);

// RIZINO!
// RIZINO!

Bez obzira koja se sintaksa koristi, ovakve dodjele su veoma opasne. Prvom dodjelom smo eksplicitno
postavili pokaziva pok_na_int da pokazuje na adresu 3446 (uz pretpostavku da raunar na kojem radimo
koristi linearni memorijski model), mada ne znamo ta se tamo nalazi. Drugom dodjelom smo postavili
pokaziva pok_na_double da pokazuje na istu adresu na koju pokazuje pok_na_int, to je takoer
veoma opasno. Naime, ako se na nekoj lokaciji nalazi cijeli broj, tamo ne moe u isto vrijeme biti realni broj
(ne smijemo zaboraviti da se cijeli i realni brojevi zapisuju na posve razliite naine u raunarskoj memoriji,
ak i kada imaju istu vrijednost). Pored toga, na razliite tipove pokazivaa pokazivaka aritmetika ne djeluje
isto. Stoga su ovakve pretvorbe ostavljene samo onima koji veoma dobro znaju ta njima ele da postignu.
Treba napomenuti da se pretvorbom broja u pokaziva dobija pokaziva, koji se dalje moe dereferencirati
kao i svaki drugi pokaziva. Stoga su naredbe poput sljedee sasvim legalne (obje su ekvivalentne, s tim to
druga radi samo u jeziku C++):
*(int *)3446 = 50;
*reinterpret_cast<int *>(3446) = 50;

// RIZINO!
// RIZINO!

Ove naredbe e na adresu 3446 u memoriju smjestiti broj 50 (posljedice ovakvih akcija preuzima programer, i
u normalnim programima ih ne treba koristiti). Jezici C i C++ su rijetki jezici koji omoguavaju ovakvu
slobodu u pristupima resursima raunara, to ih ini idealnim za pisanje sistemskog softvera (naravno, za tu
svrhu treba izuzetno dobro poznavati arhitekturu raunara i operativnog sistema za koji se pie program).
Stoga, ukoliko elite koristiti ovakve konstrukcije, provjerite da li ste sigurni da znate ta radite. Ukoliko
utvrdite da ste sigurni, provjerite da li ste sigurno sigurni. Na kraju, ukoliko ste sigurni da ste sigurno sigurni,
opet ne radite to ako zaista ne morate!
Ve smo vidjeli da je, zahvaljujui pokazivakoj aritmetici, funkcije koje barataju sa nizovima, poput
ranije napisane funkcije IspisiNiz, mogue iskoristiti tako da se izvre samo nad dijelom niza, koji ne
poinje nuno od prvog elementa. Meutim, sve takve funkcije bile su heterogene po pitanju svojih parametara,
odnosno njihovi parametri bili su razliitih tipova (na primjer, jedan parametar funkcije IspisiNiz bio je
sam niz, dok je drugi parametar bio broj elemenata koje treba ispisati). Za mnoge primjene je praktinije imati
funkcije sa homogenom strukturom parametrara, odnosno funkcije iji su parametri istog tipa. Na primjer,
funkcija IspisiNiz mogla bi se napisati tako da njeni parametri budu pokazivai na prvi i posljednji element
koji e se ispisati. Ovako napisane funkcije su esto ne samo jednostavnije za koritenje, ve i jednostavnije
za implementaciju, jer se na taj nain moe efikasno koristiti pokazivaka aritmetika. Zbog nekih tehnikih
razloga, bolje je umjesto pokazivaa na posljednji element koristiti pokaziva iza posljednjeg elementa. Na
primjer, upravo takva je sljedea verzija funkcije IspisiNiz, u kojoj parametri pocetak i iza_kraja
predstavljaju respektivno pokaziva na prvi element niza koji treba ispisati i pokaziva koji pokazuje iza
posljednjeg elementa koji treba ispisati:
void IspisiNiz(double *pocetak, double *iza_kraja) {
for(double *p = pocetak; p < iza_kraja; p++)
std::cout << *p << std::endl;
}

24

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

Alternativno, mogli smo ak i izbjei koritenje dodatne promjenljive p, recimo ovako:


void IspisiNiz(double *pocetak, double *iza_kraja) {
while(pocetak < iza_kraja) std::cout << *pocetak++ << std::endl;
}

U svakom sluaju, ukoliko elimo na primjer ispisati elemente niza realnih brojeva niz sa indeksima 3, 4,
5, 6 i 7, moemo koristiti sljedei poziv:
IspisiNiz(niz + 3, niz + 8);

Takoer, ukoliko isti niz ima n elemenata, tada itav niz moemo ispisati pozivom poput
IspisiNiz(niz, niz + n);

Iz posljednjeg primjera je ujedno vidljivo zbog ega je praktino da drugi parametar pokazuje iza posljednjeg
elementa, a ne tano na njega (ovo nije jedini razlog). Istu funkciju bismo mogli iskoristiti i za ispis elemenata
vektora (ovdje se misli na tip vector uveden u jeziku C++), ukoliko eksplicitno uzmemo adrese pojedinih
elemenata pomou operatora &. Na primjer, sljedea dva poziva su analogna prethodnim pozivima za sluaj
kada umjesto niza niz treba ispisati elemente vektora v (u istom opsegu kao u prethodnim primjerima):
IspisiNiz(&v[3], &v[8]);
IspisiNiz(&v[0], &v[n]);

Kasnije emo vidjeti da standardna biblioteka algorithm posjeduje itavo mnotvo funkcija za manipulaciju
nizovima i vektorima (i drugim srodnim strukturama podataka koje emo kasnije obraivati) koje koriste
upravo ovakvu konvenciju o parametrima koja je iskoritena u posljednjoj verziji funkcije IspisiNiz.
Interesantno je napomenuti da postoje ne samo pokazivai na jednostavne objekte, kao to su cijeli i realni
brojevi ili znakovi, ve je mogue napraviti pokazivae na praktino bilo koje objekte. Tako npr. postoje
pokazivai na nizove, pokazivai na funkcije pa i pokazivai na pokazivae (tzv. dvojni pokazivai). Takoer
se mogu napraviti nizovi pokazivaa, reference na pokazivae (samo u jeziku C++) i druge komplicirane
strukture. Sa nekim od ovakvih sloenijih pokazivakih tipova upoznaemo se kasnije u okviru ovog kursa.
Izlaganje o prostim pokazivakim tipovima zavriemo kratkim opisom tzv. generikih odnosno
netipiziranih (engl. typeless) pokazivaa. Ovi pokazivai se jako mnogo koriste u jeziku C, dok je njihova
upotreba u jeziku C++ danas svedena na najnuniji minimum (uglavnom samo za potrebe pravljenja objekata
koji u sebi mogu uvati druge objekte razliitih tipova). Ipak, njihov opis moe pomoi razumijevanju prave
prirode pokazivakih tipova. Generiki pokazivai se definiraju kao pokazivai na tip void, i za njih se
smatra da je nepoznato na koji tip tano pokazuju. Stoga se oni ne mogu dereferencirati, niti se sa njima moe
vriti pokazivaka aritmetika. Zapravo, ovi pokazivai su jo ogranieniji: sa njima se ne moe raditi
praktino nita sve dok se pomou operatora za pretvorbu tipa ne pretvore u pokaziva na neki konkretan tip.
Meutim, generikom pokazivau se moe dodijeliti ma koji drugi pokaziva, i funkcija koja kao formalni
parametar oekuje generiki pokaziva prihvatie kao stvarni parametar bilo koji pokaziva. Stoga su se
generiki pokazivai intenzivno koristili prije nego to su u jezik C++ uvedene generike (ablonske)
funkcije, o kojima emo govoriti kasnije u toku ovog kursa. Naime, oni su ranije bili jedini nain da se
naprave funkcije koje mogu prihvatati nizove razliitih tipova kao parametre (u jeziku C oni su i dalje jedini
nain da se naprave funkcije koje, makar u izvjesnoj mjeri, posjeduju neka svojstva generikih funkcija).
Razmotrimo koncept generikih pokazivaa na jednom konkretnom primjeru. Neka je potrebno napisati
funkciju KopirajNiz koja posjeduje tri parametra odredisni, izvorni i br_elemenata i koja
kopira br_elemenata elemenata iz niza izvorni u niz odredisni. Neka je dalje tu funkciju potrebno
napisati tako da radi sa nizovima iji su elementi proizvoljnog tipa. Uz pomo generikih funkcija, kakve
danas postoje u jeziku C++, takvu funkciju je posve lako napisati, to emo vidjeti uskoro u okviru ovog
kursa. Meutim, na ovom mjestu nas zanima kako se isti problem moe donekle rijeiti bez upotrebe
generikih funkcija. Za tu svrhu e nam posluiti generiki pokazivai. Naime, ako bismo formalne parametre
odredisni i izvorni deklarirali tako da budu generiki pokazivai (odnosno, pokazivai na void),
takva funkcija bi kao odgovarajue stvarne parametre prihvatala bilo kakav pokaziva, pa samim tim (zbog
automatske konverzije nizova u pokazivae) i bilo kakav niz. Dakle, takva funkcija bi zaista prihvatala nizove
proizvoljnog tipa kao stvarne parametre. Meutim, sa generikim pokazivaima se ne moe raditi nita dok se
ne izvri njihova pretvorba u pokaziva na neki konkretan tip. Sad se javlja prirodno pitanje u koji tip izvriti
njihovu pretvorbu. Naime, nijedna klasina funkcija ne moe ni na kakav nain saznati kojeg su tipa njeni
stvarni parametri (generike funkcije to mogu, ali ovdje ne govorimo o njima). Stoga je najprirodnije izvriti

25

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

njihovu konverziju u pokazivae na tip char, s obzirom da je tip char najelementarniji tip podataka (svaki
podatak tipa char uvijek zauzima tano jedan bajt), a nakon toga obaviti kopiranje niza kao da se radi o
obinom nizu bajtova (odnosno, obaviti kopiranje nizova bajt po bajt, onako kako su oni pohranjeni u
memoriji). Meutim, tada se umjesto broja elemenata koje treba kopirati, kao parametar mora zadavati broj
bajtova koje elimo kopirati. Tako napisana funkcija KopirajNiz mogla bi izgledati recimo ovako:
void KopirajNiz(void *odredisni, void *izvorni, int br_bajtova) {
for(int i = 0; i < br_bajtova; i++)
((char *)odredisni)[i] = ((char *)izvorni)[i];
}

Sad, ako na primjer elimo kopirati 10 elemenata niza realnih brojeva niz1 u niz niz2, mogli bismo
koristiti sljedei poziv:
KopirajNiz(niz2, niz1, 10 * sizeof(double));

Primijetimo kako je ovdje upotrijebljen operator sizeof, sa ciljem pretvaranja broja elemenata u broj
bajtova (podsjetimo se da operator sizeof daje kao rezultat broj bajtova koji zauzima odreeni tip ili
izraz). Naravno, umjesto izraza sizeof(double) moe se koristiti i izraz sizeof niz1[0] ime poziv
funkcije postaje neovisan od stvarnog tipa elemenata niza. U sutini, izraz 10 * sizeof(double) bi se ak
mogao prosto zamijeniti sa sizeof niz1, jer ono to nas zanima je zapravo broj bajtova koje zauzima niz
niz1. Vidimo da smo, na izvjestan nain, uz pomo generikih pokazivaa uspjeli napraviti funkciju koja
kao argumente prihvata nizove proizvoljnih tipova, ali po cijenu da umjesto broja elemenata koji se kopiraju
moramo zadavati broj bajtova koji se kopiraju. Uskoro emo vidjeti da to nije jedini nedostatak ovakvog
pristupa.
Radi boljeg razumijevanja, navedimo i jo jedan neto sloeniji primjer koji koristi generike pokazivae.
Pretpostavimo da treba napisati funkciju IzvrniNiz sa dva parametra niz i br_elemenata koja
izvre br_elemenata elemenata niza niz, iji su elementi proizvoljnog tipa, u obrnuti poredak. Ponovo,
ovaj problem je vrlo lako rjeiv uz pomo generikih funkcija, ali na ovom mjestu nas ne zanima takvo
rjeenje. Ovdje je cilj pokazati kako se ovaj problem moe rijeiti uz pomo generikih pokazivaa. Jasno je
da parametar niz treba biti generiki pokaziva. Meutim, ovdje se javlja dodatni problem u odnosu na
funkciju iz prethodnog primjera. Da bismo izvrnuli elemente niza u obrnuti poredak, ne smijemo prosto
izvrnuti bajtove od kojih je niz formiran u obrnuti poredak. Naime, ukoliko to uinimo, izvrnuemo i
bajtovsku strukturu svakog individualnog elementa niza, to naravno ne smijemo uraditi. Dakle, bajtovska
struktura svakog individualnog elementa niza ne smije se naruiti. Meutim, funkcija ne moe ni na kakav
nain saznati tip elemenata niza, odakle slijedi da ne moe saznati ni koliko bajtova zauzima svaki od
elemenata niza. Ukoliko malo bolje razmislimo o svemu, zakljuiemo da se problem ne moe rijeiti ukoliko
u funkciju ne uvedemo dodatni parametar (nazvan recimo vel_elementa) koji govori upravo koliko
bajtova zauzima svaki od elemenata niza. Uz pomo ovakvog dopunskog parametra, traena funkcija se moe
napraviti uz malo igranja sa pokazivakom aritmetikom. Jedno od moguih rjeenja moglo bi izgledati
recimo ovako:
void IzvrniNiz(void *niz, int br_elemenata, int vel_elementa) {
for(int i = 0; i < br_elemenata / 2; i++) {
char *p1((char *)niz + i * vel_elementa);
char *p2((char *)niz + (br_elemenata - i - 1) * vel_elementa);
for(int j = 0; j < vel_elementa; j++) {
char pomocna(p1[j]);
p1[j] = p2[j]; p2[j] = pomocna;
}
}
}

Ukoliko sada elimo izvrnuti elemente niza niz1 od 10 elemenata u obrnuti poredak, mogli bismo koristiti
poziv poput sljedeeg:
IzvrniNiz(niz1, 10, sizeof niz1[0]);

Opisani primjeri nisu nimalo jednostavni, i trae izvjesno poznavanje naina na koji su organizirani
podaci u raunarskoj memoriji. Najbolje je da probate runo pratiti izvravanje napisanih funkcija na nekom
konkretnom primjeru. Napomenimo da su ovi primjeri ubaeni isto sa ciljem pojanjenja prave prirode
pokazivaa. Naime, u jeziku C++ ovakve primjere ne treba pisati, s obzirom da se razmotreni problemi mnogo
lake rjeavaju pomou pravih generikih funkcija (koje emo uskoro upoznati). Ovakve kvazi-generike

26

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Dodatak predavanjima:
Sve to treba znati o pokazivaima
Akademska godina 2014/15

funkcije tipino zahtijevaju udne ili dopunske parametre (poput parametra vel_elementa u posljednjem
primjeru). Pored toga, nemogue je na ovaj nain napraviti funkciju koja bi, recimo, nala najvei element
niza, ili sumu svih elemenata u nizu (koja bi radila za nizove proizvoljnih tipova elemenata), s obzirom da na
ovaj nain mi uope ne baratamo sa elementima niza, ve sa bajtovima koje tvore elementi niza. Stoga je takav
pristup mogu jedino za primjene u kojima tana priroda elemenata niza nije ni bitna (kakva je npr. kopiranje).
Bez obzira na brojna ogranienja funkcija koje koriste generike pokazivae, one se mnogo koriste u
jeziku C (jer u njemu bolja alternativa i ne postoji). Stoga mnoge funkcije iz biblioteka koje dolaze uz jezik C
koriste ovaj pristup. Kako je jezik C++ naslijedio sve biblioteke koje su postojale u jeziku C, one postoje i u
jeziku C++. Na primjer, funkcija memcpy iz biblioteke cstring identina je posljednjoj napisanoj verziji
funkcije KopirajNiz. Drugim rijeima, kopiranje niza niz1 od 10 realnih brojeva u niz niz2 moe se,
u principu, obaviti i na sljedei nain:
std::memcpy(niz2, niz1, 10 * sizeof niz1[0]);

Takoer, ni funkcije koje kao jedan od parametara zahtijevaju veliinu elemenata niza u bajtovima (poput
posljednje verzije funkcije IzvrniNiz), nisu nikakva rijetkost u standardnim bibliotekama. Takva je, na
primjer, funkcija qsort iz biblioteke cstdlib, koja slui za sortiranje elemenata niza u odreeni poredak.
Programeri u jeziku C intenzivno koriste ovakve funkcije. Mada se one, u naelu, mogu koristiti i u jeziku
C++, postoje jaki razlozi da u jeziku C++ ove funkcije ne koristimo (ova napomena namijenjena je najvie
onima koji imaju dobro prethodno programersko iskustvo u jeziku C). Naime, sve funkcije zasnovane na
generikim pokazivaima svoj rad baziraju na injenici da je manipulacije sa svim tipovima podataka mogue
izvoditi bajt po bajt. Ta pretpostavka je tana za sve tipove podataka koji postoje u jeziku C, ali ne i za neke
novije tipove podataka koji su uvedeni u jeziku C++ (kao to su svi tipovi podataka koji koriste tzv. vlastiti
konstruktor kopije). Na primjer, mada e funkcija memcpy bespijekorno raditi za kopiranje nizova iji su
elementi tipa double, njena primjena na nizove iji su elementi tipa string moe imati fatalne posljedice.
Isto vrijedi i za funkciju qsort, koja moe napraviti pravu zbrku ukoliko se pokua primijeniti za sortiranje
nizova iji su elementi dinamiki stringovi. Tipovi podataka sa kojima se moe sigurno manipulirati
pristupanjem individualnim bajtima koje tvore njihovu strukturu nazivaju se POD tipovi (od engl. Plain Old
Data). Na primjer, prosti tipovi kao to su int, double i klasini nizovi jesu POD tipovi, dok vektori i
dinamiki stringovi nisu.
Opisani problemi sa koritenjem funkcija iz biblioteka naslijeenih iz jezika C koje koriste generike
pokazivae ne treba mnogo da nas zabrinjava, jer za svaku takvu funkciju u jeziku C++ postoje bolje
alternative, koje garantirano rade u svim sluajevima. Tako, na primjer, u jeziku C++ umjesto funkcije
memcpy treba koristiti funkciju copy iz biblioteke algorithm, o kojoj emo govoriti kasnije, dok
umjesto funkcije qsort treba koristiti funkciju sort (takoer iz biblioteke algorithm, koju emo
kasnije detaljno obraditi). Ove funkcije, ne samo da su univerzalnije od srodnih funkcija naslijeenih iz jezika
C, ve su i jednostavnije za upotrebu. Na kraju, ,oramo dati jo samo jednu napomenu. Programeri koji
posjeduju bogatije iskustvo u jeziku C, teko se odriu upotrebe funkcije memcpy i njoj srodnih funkcija
(poput memset), jer im je poznato da su one, zahvaljujui raznim trikovima na kojima su zasnovane, vrlo
efikasne (mnogo efikasnije nego upotreba obine for-petlje). Meutim, bitno je znati da odgovarajue funkcije
iz biblioteke algorithm, uvedene u jeziku C++, poput funkcije copy (ili fill, koju treba koristiti
umjesto memset), tipino nisu nita manje efikasne. Zapravo, u mnogim implementacijama biblioteke
algorithm, poziv funkcije poput copy automatski se prevodi u poziv funkcije poput memcpy ukoliko
se ustanovi da je takav poziv siguran, odnosno da tipovi podataka sa kojima se barata predstavljaju POD
tipove. Stoga, programeri koji sa jezika C prelaze na C++ ne trebaju osjeati nelagodu prilikom prelaska sa
upotrebe funkcija poput memcpy na funkcije poput copy. Jednostavno, treba prihvatiti injenicu: funkcije
poput memcpy, memset, qsort i njima sline zaista ne treba koristiti u C++ programima (osim u vrlo
iznimnim sluajevima). One su zadrane preteno sa ciljem da omogue kompatibilnost sa ve napisanim
programima koji su bili na njima zasnovani!

27

You might also like