You are on page 1of 22

Dr.

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

Predavanje 5.
Jezik C++ je znatno osavremenio mehanizme za dinamikom alokaciju (dodjelu) memorije u odnosu
na jezik C. Pod dinamikom alokacijom memorije podrazumijevamo mogunost da program u toku
izvravanja zatrai da mu se dodijeli odreena koliina memorije koja ranije nije bila zauzeta, i kojom
on moe raspolagati sve dok eventualno ne zatrai njeno oslobaanje. Takvim, dinamiki dodijeljenim
blokovima memorije, moe se pristupati iskljuivo pomou pokazivaa. Dinamika alokacija memorije
se u jeziku C ostvarivala se pozivom funkcije malloc ili neke srodne funkcije iz biblioteke
cstdlib (zaglavlje ove biblioteke u jeziku C zvalo se stdlib.h). Mada ovaj nain za dinamiku
alokaciju naelno radi i u jeziku C++, C++ nudi mnogo fleksibilnije naine, tako da u C++ programima
po svaku cijenu treba izbjegavati naine za dinamiku dodjelu memorije naslijeene iz jezika C. Takoer
treba napomenuti da je potreba za dinamikom alokacijom memorije u svakodnevnim primjenama
uveliko opala nakon to su u C++ uvedeni dinamiki tipovi podataka, kao to su vector, string,
itd. Meutim, dinamiki tipovi podataka su izvedeni tipovi podataka, koji su definirani u standardnoj
biblioteci jezika C++, i koji su implementiran upravo pomou dinamike alokacije memorije! Drugim
rijeima, da ne postoji dinamika alokacija memorije, ne bi bilo mogue ni kreiranje tipova poput
vector i vector. Stoga, ukoliko elimo u potpunosti da ovladamo ovim tipovima podataka i da
samostalno kreiramo vlastite tipove podataka koji su njima srodni, moramo shvatiti mehanizam koji stoji
u pozadini njihovog funkcioniranja, a to je upravo dinamika alokacija nizova! Pored toga, vidjeemo
kasnije da dinamika alokacija memorije moe biti korisna za izgradnju drugih sloenijih tipova
podataka, koje nije mogue jednostavno svesti na postojee dinamike tipove podataka.
U jeziku C++, preporueni nain za dinamiku alokaciju memorije je pomou operatora new, koji
ima vie razliitih oblika. U svom osnovnom obliku, iza njega treba da slijedi ime nekog tipa. On e tada
potraiti u memoriji slobodno mjesto u koje bi se mogao smjestiti podatak navedenog tipa. Ukoliko se
takvo mjesto pronae, operator new e kao rezultat vratiti pokaziva na pronaeno mjesto u memoriji.
Pored toga, pronaeno mjesto e biti oznaeno kao zauzeto, tako da se nee moi desiti da isti prostor u
memoriji sluajno bude upotrijebljen za neku drugu namjenu. Ukoliko je sva memorija ve zauzeta,
operator new e baciti izuzetak, koji moemo uhvatiti. Raniji standardi jezika C++ predviali su da
operator new u sluaju neuspjeha vrati kao rezultat nul-pokaziva, ali je ISO 98 standard jezika C++
precizirao da u sluaju neuspjeha operator new baca izuzetak (naime, programeri su esto zaboravljali
ili su bili lijeni da nakon svake upotrebe operatora new provjeravaju da li je vraena vrijednost moda
nul-pokaziva, to je, u sluaju da alokacija ne uspije, moglo imati ozbiljne posljedice).
Osnovnu upotrebu operatora new najbolje je ilustrirati na konkretnom primjeru. Pretpostavimo da
nam je data sljedea deklaracija:
int *pok;

Ovom deklaracijom definirana je pokazivaka promjenljiva pok koja inicijalno ne pokazuje ni na ta


konkretno, ali koja, u naelu, treba da pokazuje na cijeli broj. Stanje memorije nakon ove deklaracije
moemo predstaviti sljedeom slikom (adrese su pretpostavljene proizvoljno):
Adrese

...

3758

3759

3760
???

3761

3762

3763

3764

3765

...

???
pok

Ukoliko sad izvrimo naredbu


pok = new int;

operator new e potraiti slobodno mjesto u memoriji koje je dovoljno veliko da prihvati jedan cijeli
broj. Ukoliko se takvo mjesto pronae, njegova adresa e biti vraena kao rezultat operatora new, i
1

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

dodijeljena pokazivau pok. Pretpostavimo, na primjer, da je slobodno mjesto pronaeno na adresi


3764. Tada e rezultat operatora new biti pokaziva na cijeli broj koji pokazuje na adresu 3764, koji se
moe dodijeliti pokazivau pok, tako da e on pokazivati na adresu 3764. Stanje u memoriji e sada
izgledati ovako:
Adrese

...

3758

3759

3760
3764

3761

3762

3763

3764

3765

...

pok

Pored toga, lokacija 3764 postaje zauzeta, u smislu da e biti evidentirano da je ova lokacija
rezervirana za upotrebu od strane programa, i ona do daljnjeg sigurno nee biti iskoritena za neku drugu
namjenu. Stoga je sasvim sigurno pristupiti njenom sadraju putem pokazivaa pok. Znamo da se
svaki dereferencirani pokaziva u potpunosti ponaa kao promjenljiva (preciznije, dereferencirani
pokaziva je l-vrijednost) iako lokacija na koju on pokazuje nema svoje vlastito simboliko ime. Stoga su
naredbe poput sljedeih posve korektne (i ispisae redom vrijednosti 5 i 18):
*pok = 5;
cout << *pok << endl;
*pok = 3 * *pok + 2;
(*pok)++;
cout << *pok << endl;

// Oprez: ovo nije isto to i *pok++

Kako se lokacija rezervirana pomou operatora new u potpunosti ponaa kao promjenljiva (osim
to nema svoje vlastito ime), kaemo da ona predstavlja dinamiku promjenljivu. Dakle, dinamike
promjenljive se stvaraju primjenom operatora new, i njihovom sadraju se moe pristupiti iskljuivo
putem pokazivaa koji na njih pokazuju. Pored automatskih promjenljivih, koje se automatski stvaraju na
mjestu deklaracije i automatski unitavaju na kraju bloka u kojem su deklarirane (takve su sve lokalne
promjenljive osim onih koje su deklarirane sa atributom static), i statikih promjenljivih koje ive
cijelo vrijeme dok se program izvrava (takve su sve globalne promjenljive i lokalne promjenljive
deklarirane sa atributom static), dinamike promjenljive se mogu smatrati za treu vrstu
promjenljivih koje postoje. One se stvaraju na zahtjev, i kao to emo uskoro vidjeti, unitavaju na
zahtjev. Samo, za razliku od automatskih i statikih promjenljivih, dinamike promjenljive nemaju
imena (stoga im se moe pristupiti samo pomou pokazivaa).
Sadraj dinamikih promjenljivih je nakon njihovog stvaranja tipino nedefiniran, sve dok im se ne
izvri prva dodjela vrijednosti (putem pokazivaa). Preciznije, sadraj zauzete lokacije zadrava svoj
raniji sadraj, jedino to se lokacija oznaava kao zauzeta. Meutim, mogue je izvriti inicijalizaciju
dinamike promjenljive odmah po njenom stvaranju, tako to iza oznake tipa u operatoru new u
zagradama navedemo izraz koji predstavlja eljenu inicijalnu vrijenost promjenljive. Na primjer,
sljedea naredba e stvoriti novu cjelobrojnu dinamiku promjenljivu, inicijalizirati njen sadraj na
vrijednost 5, i postaviti pokaziva pok da pokazuje na nju:
pok = new int(5);

Sada e stanje u memoriji biti kao na sljedeoj slici:


Adrese

...

3758

3759

3760
3764

3761

3762

3763

3764
5

3765

...

pok

Razumije se da se rezultat operatora new mogao odmah iskoristiti za inicijalizaciju pokazivaa


pok ve prilikom njegove deklaracije, kao na primjer u sljedeim deklaracijama:
2

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

int *pok = new int(5);


int *pok (new int(5));

Predavanje 5.

// Ove dvije deklaracije su


//
ekvivalentne...

Naime, za dinamiku promjenljivu je, kao i za svaku drugu promjenljivu, mogue vezati referencu,
ime moemo indirektno dodijeliti ime novostvorenoj dinamikoj promjenljivoj. Tako, ukoliko je pok
pokazivaka promjenljiva koja pokazuje na novostvorenu dinamiku promjenljivu, kao u prethodnom
primjeru, mogue je izvriti sljedeu deklaraciju:
int &dinamicka = *pok;

Na ovaj nain smo kreirali referencu dinamicka koja je vezana za novostvorenu dinamiku
promjenljivu, i koju, prema tome, moemo smatrati kao alternativno ime te dinamike promjenljive
(zapravo, jedino ime, s obzirom da ona svog vlastitog imena i nema). U sutini, isti efekat smo mogli
postii i vezivanjem reference direktno na dereferencirani rezultat operatora new (bez posredstva
pokazivake promjenljive). Naime, rezultat operatora new je pokaziva, dereferencirani pokaziva je
l-vrijednost, a na svaku l-vrijednost se moe vezati referenca odgovarajueg tipa:
int &dinamicka = *new int(5);

Ovakve konstrukcije se ne koriste osobito esto, mada se ponekad mogu korisno upotrijebiti (npr.
dereferencirani rezultat operatora new moe se prenijeti u funkciju kao parametar po referenci). U
svakom sluaju, opis ovih konstrukcija pomae da se shvati prava sutina pokazivaa i referenci.
Prilikom koritenja operatora new, treba voditi rauna da uvijek postoji mogunost da on baci
izuzetak. Dodue, vjerovatnoa da u memoriji nee biti pronaen prostor za jedan jedini cijeli broj
veoma je mala, ali se treba naviknuti na takvu mogunost, jer emo kasnije dinamiki alocirati znatno
glomaznije objekte (npr. nizove) za koje lako moe nestati memorije. Stoga bi se svaka dinamika
alokacija trebala izvoditi unutar try bloka, na nain koji je principijelno prikazan u sljedeem isjeku:
try {
int *pok = new int(5);

}
catch(...) {
cout << "Problem: Nema dovoljno memorije!\n";
}

Tip izuzetka koji se pri tome baca je tip bad_alloc. Ovo je izvedeni tip podataka (poput tipa
string i drugih izvedenih tipova podataka) definiran u biblioteci new. Tako, ukoliko elimo da
specifiziramo da hvatamo ba izuzetke ovog tipa, moemo koristiti sljedeu konstrukciju (pod uvjetom
da smo ukljuili zaglavlje biblioteke new u program):
try {
int *pok = new int(5);

}
catch(bad_alloc) {
cout << "Problem: Nema dovoljno memorije!\n";
}

Primijetimo da nismo imenovali parametar tipa bad_alloc, s obzirom da nam on nije ni


potreban. Inae, specifikacija tipa bad_alloc unutar catch bloka je korisna ukoliko elimo da
razlikujemo izuzetke koje baca operator new od drugih izuzetaka. Ukoliko smo sigurni da jedini
mogui izuzetak unutar try bloka moe potei samo od operatora new, moemo koristiti varijantu
catch bloka sa tri take umjesto formalnog parametra, to emo ubudue preteno koristiti.

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

Dinamike promjenljive se mogu ne samo stvarati, ve i unitavati na zahtjev. Onog trenutka kada
zakljuimo da nam dinamika promjenljiva na koju pokazuje pokaziva vie nije potrebna, moemo je
unititi primjenom operatora delete, iza kojeg se prosto navodi pokaziva koji pokazuje na
dinamiku promjenljivu koju elimo da unitimo. Na primjer, naredbom
delete pok;

unitiemo dinamiku promjenljivu na koju pokaziva pok pokazuje. Pri tome je vano da razjasnimo
ta se podrazumijeva pod tim unitavanjem. Pokaziva pok e i dalje pokazivati na istu adresu na koju
je pokazivao i prije, samo to e se izbrisati evidencija o tome da je ta lokacija zauzeta. Drugim rijeima,
ta lokacija moe nakon unitavanja dinamike promjenljive biti iskoritena od strane nekog drugog (npr.
operativnog sistema), pa ak i od strane samog programa za neku drugu svrhu (na primjer, sljedea
primjena operatora new moe ponovo iskoristiti upravo taj prostor za stvaranje neke druge dinamike
promjenljive). Stoga, vie nije sigurno pristupati sadraju na koju pok pokazuje. Pokazivai koji
pokazuju na objekte koji su iz bilo kojeg razloga prestali da postoje nazivaju se visei pokazivai (engl.
dangling pointers). Jedan od moguih naina da kreiramo visei pokaziva je eksplicitno unitavanje
sadraja na koji pokaziva pokazuje pozivom operatora delete, ali postoje i mnogo suptilniji naini
da stvorimo visei pokaziva (npr. da iz funkcije kao rezultat vratimo pokaziva na neki lokalni objekat
unutar funkcije, koji prestaje postojati po zavretku funkcije). Treba se uvati viseih pokazivaa, jer su
oni veoma est uzrok fatalnih greaka u programima, koje se teko otkrivaju, s obzirom da im se
posljedice obino uoe tek naknadno. Zapravo, ranije smo vidjeli da postoje i visee reference, koje su
jednako opasne kao i visei pokazivai, samo to je visei pokaziva, na alost, mnogo lake napraviti
od visee reference!
Sve dinamike promjenljive se automatski unitavaju po zavretku programa. Meutim, bitno je
napomenuti da ni jedna dinamika promjenljiva nee biti unitena sama od sebe prije zavretka
programa, osim ukoliko je eksplicitno ne unitimo primjenom operatora delete. Po tome se one bitno
razlikuju od automatskih promjenljivih, koje se automatski unitavaju na kraju bloka u kojem su
definirane. Ukoliko smetnemo sa uma ovu injenicu, moemo zapasti u probleme. Pretpostavimo, na
primjer, da smo izvrili sljedeu sekvencu naredbi:
int *pok = new int;
*pok = 5;
pok = new int;
*pok = 3;

U ovom primjeru, prvo se stvara jedna dinamika promjenljiva (recimo, na adresi 3764), nakon ega
se njen sadraj postavlja na vrijednost 5. Iza toga slijedi nova dinamika alokacija kojom se stvara nova
dinamika promjenljiva (recimo, na adresi 3765), a njen sadraj se postavlja na vrijednost 3. Pri tome,
pokaziva pok pokazuje na novostvorenu dinamiku promjenljivu (na adresi 3765). Meutim,
dinamika promjenljiva na adresi 3764 nije unitena, odnosno dio memorije u kojem se ona nalazi jo
uvijek se smatra zauzetim. Kako pokaziva pok vie ne pokazuje na nju, njenom sadraju vie ne
moemo pristupiti preko ovog pokazivaa. Zapravo, na ovu dinamiku promjenljivu vie ne pokazuje
niko, tako da je ova dinamika promjenljiva postala izgubljena (niti joj moemo pristupiti, niti je
moemo unititi). Ova situacija prikazana je na sljedeoj slici:
Adrese

...

3758

3759

3760
3765

3761

3762

3763

3764
5

3765
3

...

pok

Dio memorije koji zauzima izgubljena dinamika promjenljiva ostaje rezerviran sve do kraja
programa, i trajno je izgubljen za program. Ovakva pojava naziva se curenje memorije (engl. memory
leak) i predstavlja dosta estu greku u programima. Mada je curenje memorije manje fatalno u odnosu

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

greke koje nastaju usljed viseih pokazivaa, ono se takoer teko uoava i moe da dovede do
ozbiljnih problema. Razmotrimo na primjer sljedeu sekvencu naredbi:
int *pok;
for(int i = 1; i <= 30000; i++) {
pok = new int;
*pok = i;
}

U ovom primjeru, unutar for petlje je stvoreno 30000 dinamikih promjenljivih, jer je svaka primjena
operatora new stvorila novu dinamiku promjenljivu, od kojih niti jedna nije unitena (s obzirom da
nije koriten operator delete). Meutim, od tih 30000 promjenljivih, 29999 je izgubljeno, jer na
kraju pokaziva pok pokazuje samo na posljednju stvorenu promjenljivu! Uz pretpostavku da jedna
cjelobrojna promjenljiva zauzima 4 bajta, ovim smo bespotrebno izgubili 119996 bajta memorije, koje
ne moemo osloboditi, sve dok se ne oslobode automatski po zavretku programa!
Jedna od tipinih situacija koje mogu dovesti do curenja memorije je stvaranje dinamike
promjenljive unutar neke funkcije preko pokazivaa koji je lokalna automatska (nestatika) promjenljiva
unutar te funkcije. Ukoliko se takva dinamika promjenljiva ne uniti prije zavretka funkcije, pokaziva
koji na nju pokazuje bie uniten (poput svake druge automatske promjenljive), tako da e ona postati
izgubljena. Na primjer, sljedea funkcija demonstrira takvu situaciju:
void Curenje(int n) {
int *pok = new int;
*pok = n;
}

Prilikom svakog poziva ove funkcije stvara se nova dinamika promjenljiva, koja postaje posve
nedostupna nakon zavretka funkcije, s obzirom da se pokaziva koji na nju pokazuje unitava. Ostatak
programa nema mogunost niti da pristupi takvoj promjenljivoj, niti da je uniti, tako da svaki poziv
funkcije Curenje stvara novu izgubljenu promjenljivu, odnosno prilikom svakog njenog poziva
koliina izgubljene memorije se poveava. Stoga bi svaka funkcija koja stvori neku dinamiku
promjenljivu trebala i da je uniti. Izuzetak od ovog pravila moe imati smisla jedino ukoliko se
novostvorena dinamika promjenljiva stvara putem globalnog pokazivaa (kojem se moe pristupiti i
izvan funkcije), ili ukoliko funkcija vraa kao rezultat pokaziva na novostvorenu promjenljivu. Na
primjer, sljedea funkcija moe imati smisla:
int *Stvori(int n) {
int *pok = new int(n);
return pok;
}

Ova funkcija stvara novu dinamiku promjenljivu, inicijalizira je na vrijednost zadanu parametrom, i
vraa kao rezultat pokaziva na novostvorenu promjenljivu (zanemarimo to to je ova funkcija posve
beskorisna, s obzirom da ne radi nita vie u odnosu na ono to ve radi sam operator new). Na taj
nain omogueno je da rezultat funkcije bude dodijeljen nekom pokazivau, preko kojeg e se kasnije
moi pristupiti novostvorenoj dinamikoj promjenljivoj, i eventualno izvriti njeno unitavanje. Na
primjer, sljedei slijed naredbi je posve smislen:
int *pok;
pok = Stvori(10);
cout << *pok;
delete pok;

Napomenimo da se funkcija Stvori mogla napisati i kompaktnije, bez deklariranja lokalnog


pokazivaa:

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

int *Stvori(int n) {
return new int(n);
}

Naime, new je operator koji vraa pokaziva kao rezultat, koji kao takav smije biti vraen kao rezultat
iz funkcije. Takoer, interesantno je napomenuti da je na prvi pogled veoma neobina konstrukcija
*Stvori(5) = 8;

sintaksno posve ispravna (s obzirom da funkcija Stvori vraa kao rezultat pokaziva, a
dereferencirani pokaziva je l-vrijednost), mada je logiki smisao ovakve konstrukcije prilino upitan
(prvo se stvara dinamika promjenljiva sa vrijednou 5, nakon toga se njena vrijednost mijenja na 8, i
na kraju se gubi svaka veza sa dinamikom promjenljivom, jer pokaziva na nju nije nigdje sauvan).
Ipak, mogu se pojaviti situacije u kojima sline konstrukcije mogu biti od koristi, stoga je korisno znati
da su one mogue.
Kreiranje individualnih dinamikih promjenljivih nije od osobite koristi ukoliko se kreiraju
promjenljive prostih tipova (kao to su npr. cjelobrojne promjenljive), tako da e kreiranje individualnih
dinamikih promjenljivih postati interesantno tek kada razmotrimo sloenije tipove podataka, kakvi su
strukture i klase. Meutim, znatno je interesantnija injenica da se pomou dinamike alokacije
memorije mogu indirektno kreirati nizovi ija veliina nije unaprijed poznata (ovaj mehanizam zapravo
lei u pozadini funkcioniranja tipova poput tipa vector). Za tu svrhu koristi se taloer operator new
iza kojeg ponovo slijedi ime tipa (koje ovaj put predstavlja tip elemenata niza), nakon ega u uglastim
zagradama slijedi broj elemenata niza koji kreiramo. Pri tome, traeni broj elemenata niza ne mora biti
konstanta, ve moe biti proizvoljan izraz. Operator new e tada potraiti slobodno mjesto u memoriji
u koje bi se mogao smjestiti niz traene veliine navedenog tipa, i vratiti kao rezultat pokaziva na
pronaeno mjesto u memoriji, ukoliko takvo postoji (u suprotnom e biti baen izuzetak tipa
bad_alloc). Na primjer, ukoliko je promjenljiva pok deklarirana kao pokaziva na cijeli broj (kao
i u dosadanjim primjerima), tada e naredba
pok = new int[5];

potraiti prostor u memoriji koji je dovoljan da prihvati pet cjelobrojnih vrijednosti, i u sluaju da
pronae takav prostor, dodijelie njegovu adresu pokazivau pok (u uglastoj zagradi nije morala biti
konstanta 5, nego je mogao biti i proizvoljan cjelobrojni izraz). Na primjer, neka je pronaen prostor na
adresi 4433, a neka se sama pokazivaka promjenljiva pok nalazi na adresi 4430. Tada memorijska
slika nakon uspjenog izvravanja prethodne naredbe izgleda kao na sljedeoj slici (radi jednostavnosti
je pretpostavljeno da jedna cjelobrojna promjenljiva zauzima jednu memorijsku lokaciju, to u stvarnosti
nije ispunjeno):
Adrese

...

4430
4433

4431

4432

4433

4434

4435

4436

4437

...

pok

Ovako kreiranom dinamikom nizu moemo pristupiti samo preko pokazivaa. Meutim, kako se
na pokazivae mogu primjenjivati operatori indeksiranja (podsjetimo se da se izraz p[n] u sluaju
kada je p pokaziva interpretira kao *(p+n) uz primjenu pokazivake aritmetike), dinamiki niz
moemo koristiti na posve isti nain kao i obini niz, pri emu umjesto imena niza koristimo pokaziva.
Na primjer, da bismo postavili sve elemente novokreiranog dinamikog niza na nulu, moemo koristiti
for petlju kao da se radi o obinom nizu:
for(int i = 0; i < 5; i++) pok[i] = 0;

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

Stoga, sa aspekta programera gotovo da nema nikakve razlike izmeu koritenja obinih i dinamikih
nizova. Strogo reeno, dinamiki nizovi nemaju imena i njima se moe pristupati samo preko pokazivaa
koji pokazuju na njihove elemente. Tako, ukoliko izvrimo deklaraciju poput
int *dinamicki_niz = new int[100];

mi zapravo stvaramo dva objekta: dinamiki niz od 100 cijelih brojeva koji nema ime, i pokaziva
dinamicki_niz koji je inicijaliziran tako da pokazuje na prvi element ovako stvorenog dinamikog
niza. Meutim, kako promjenljivu dinamicki_niz moemo koristiti na gotovo isti nain kao da se
radi o obinom nizu (pri emu, zahvaljujui pokazivakoj aritmetici, preko nje zaista pristupamo
dinamikom nizu), dopustiemo sebi izvjesnu slobodu izraavanja i ponekad emo, radi kratkoe
izraavanja, govoriti da promjenljiva dinamicki_niz predstavlja dinamiki niz (iako je prava istina
da je ona zapravo pokaziva koji pokazuje na prvi element dinamikog niza).
Za razliku od obinih nizova koji se mogu inicijalizirati pri deklaraciji, i obinih dinamikih
promjenljivih koje se mogu inicijalizirati pri stvaranju, dinamiki nizovi se ne mogu inicijalizirati u
trenutku stvaranja (tj. njihov sadraj je nakon stvaranja nepredvidljiv). Naravno, inicijalizaciju je
mogue uvijek naknadno izvriti runo (npr. petljom iz prethodnog primjera). Takoer, nije problem
napisati funkciju koja e stvoriti niz, inicijalizirati ga, i vratiti kao rezultat pokaziva na novostvoreni i
inicijalizirani niz. Tada tako napisanu funkciju moemo koristiti za stvaranje nizova koji e odmah po
kreiranju biti i inicijalizirani. Na primjer, razmotrimo sljedeu generiku funkciju:
template <typename NekiTip>
NekiTip *StvoriNizPopunjenNulama(int n) {
NekiTip *pok = new NekiTip[n];
for(int i = 0; i < n; i++) pok[i] = 0;
return pok;
}

Uz pomo ovakve funkcije moemo stvarati inicijalizirane dinamike nizove proizvoljnog tipa kojem se
moe dodijeliti nula. Na primjer,
double *dinamicki_niz = StvoriNizPopunjenNulama<double>(100);

Primijetimo da smo prilikom poziva funkcije eksplicitno morali navesti tip elemenata niza u iljastim
zagradama <>. To je potrebno stoga to iz samog poziva funkcije nije mogue zakljuiti ta tip
NekiTip predstavlja, s obzirom da se ne pojavljuje u popisu formalnih parametara funkcije.
Mada je prethodni primjer veoma univerzalan, on se moe uiniti jo univerzalnijim. Naime,
prethodni primjer podrazumijeva da se elementima novostvorenog niza moe dodijeliti nula. To je tano
ukoliko su elementi niza brojevi. Meutim, ta ukoliko elimo da stvorimo npr. niz iji su elementi
stringovi (odnosno objekti tipa string) kojima se ne moe dodijeliti nula? Da bi se poveala
univerzalnost generikih funkcija, uvedena je konvencija da se ime tipa moe pozvati kao funkcija bez
parametara, pri emu je rezultat takvog poziva podrazumijevana vrijednost za taj tip (ovo vrijedi samo
za tipove koji posjeduju podrazumijevane vrijednosti, a veina tipova je takva). Na primjer,
podrazumijevana vrijednost za sve brojane tipove je 0, a za tip string podrazumijevana vrijednost
je prazan string. Tako je vrijednost izraza int() jednaka nuli, kao i izraza double() (mada tipovi
ovih izraza nisu isti: prvi je tipa int a drugi tipa double), dok je vrijednost izraza string()
prazan string. Stoga ne treba da udi da e naredba
cout << int();

ispisati nulu. Zahvaljujui ovoj, na prvi pogled udnoj konstrukciji, mogue je napisati veoma
univerzalnu generiku funkciju poput sljedee:

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

template <typename NekiTip>


NekiTip *StvoriInicijaliziraniNiz(int n) {
NekiTip *pok = new NekiTip[n];
for(int i = 0; i < n; i++) pok[i] = NekiTip();
return p;
}

Sa ovako napisanom funkcijom, mogue je pisati konstrukcije poput


double *niz_brojeva = StvoriInicijaliziraniNiz<double>(100);
string *niz_stringova = StvoriInicijaliziraniNiz<string>(150);

koje e stvoriti dva dinamika niza niz_brojeva i niz_stringova od 100 i 150 elemenata
respektivno, iji e elementi biti respektivno inicijalizirani nulama, odnosno praznim stringovima.
Kada nam neki dinamiki niz vie nije potreban, moemo ga takoer unititi (tj. osloboditi prostor
koji je zauzimao) pomou operatora delete, samo uz neznatno drugaiju sintaksu u kojoj se koristi
par uglastih zagrada. Tako, ukoliko pokaziva pok pokazuje na dinamiki niz, unitavanje dinamikog
niza realizira se pomou naredbe
delete[] pok;

Neophodno je napomenuti da su uglaste zagrade bitne. Naime, postupci dinamike alokacije obinih
dinamikih promjenljivih i dinamikih nizova interno se obavljaju na potpuno drugaije naine, tako da
ni postupak njihovog brisanja nije isti.. Mada postoje situacije u kojima bi se dinamiki nizovi mogli
obrisati primjenom obinog operatora delete (bez uglastih zagrada), takvo brisanje je uvijek veoma
rizino, pogotovo ukoliko se radi o nizovima iji su elementi sloeni tipovi podataka poput struktura i
klasa (na primjer, brisanje niza pomou obinog operatora delete sigurno nee biti obavljeno kako
treba ukoliko elementi niza posjeduju tzv. destruktore, o kojima emo govoriti kasnije). Stoga, ne treba
mnogo filozofirati, nego se treba drati pravila: dinamiki nizovi se uvijek moraju brisati pomou
konstrukcije delete[]. Ovdje treba biti posebno oprezan zbog injenice da nas kompajler nee
upozoriti ne upotrijebimo li uglaste zagrade, s obzirom na injenicu da kompajler ne moe znati na ta
pokazuje pokaziva koji se navodi kao argument operatoru delete.
Ve je reeno da bi dinamika alokacija memorije trebala uvijek da se vri unutar try bloka, s
obzirom da se moe desiti da alokacija ne uspije. Stoga, sljedei primjer, koji alocira dinamiki niz iju
veliinu zadaje korisnik, a zatim unosi elemente niza sa tastature i ispisuje ih u obrnutom poretku,
ilustrira kako se ispravno treba raditi sa dinamikim nizovima:
try {
int n;
cout << "Koliko elite brojeva? ";
cin >> n;
int *niz = new int[n];
cout << "Unesite brojeve:\n";
for(int i = 0; i < 10; i++) cin >> niz[i];
cout << "Niz brojeva ispisan naopako glasi:\n";
for(int i = 9; i >= 0; i--) cout << niz[i] << endl;
delete[] niz;
}
catch(...) {
cout << "Nema dovoljno memorije!\n";
}

Obavljanje dinamike alokacije memorije unutar try bloka je posebno vano kada se vri dinamika
alokacija nizova. Naime, alokacija sigurno nee uspjeti ukoliko se zatrai alokacija niza koji zauzima
vie prostora nego to iznosi koliina slobodne memorije (npr. prethodni primjer e sigurno baciti
izuzetak u sluaju da zatraite alociranje niza od recimo 100000000 elemenata)
8

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

Treba napomenuti da se rezervacija memorije za uvanje elemenata vektora i dekova takoer interno
realizira pomou dinamike alokacije memorije i operatora new. Drugim rijeima, prilikom
deklaracije vektora ili dekova takoer moe doi do bacanja izuzetka (tipa bad_alloc) ukoliko
koliina raspoloive memorije nije dovoljna da se kreira vektor ili dek odgovarajueg kapaciteta. Zbog
toga bi se i deklaracije vektora i dekova naelno trebale nalaziti unutar try bloka. Stoga, ukoliko
bismo u prethodnom primjeru eljeli izbjei eksplicitnu dinamiku alokaciju memorije, i umjesto nje
koristiti tip vector, modificirani primjer trebao bi izgledati ovako:
try {
int n;
cout << "Koliko elite brojeva? ";
cin >> n;
vector<int> niz(n);
cout << "Unesite brojeve:\n";
for(int i = 0; i < 10; i++) cin >> niz[i];
cout << "Niz brojeva ispisan naopako glasi:\n";
for(int i = 9; i >= 0; i--) cout << niz[i] << endl;
}
catch(...) {
cout << "Nema dovoljno memorije!\n";
}

Izuzetak tipa bad_alloc takoer moe biti baen kao posljedica operacija koje poveavaju veliinu
vektora ili deka (poput push_back ili resize) ukoliko se ne moe udovoljiti zahtjevu za
poveanje veliine (zbog nedostatka memorijskog prostora).
Moemo primijetiti jednu sutinsku razliku izmeu primjera koji koristi operator new i primjera u
kojem se koristi tip vector. Naime, u primjeru zasnovanom na tipu vector ne koristi se operator
delete. Oigledno, lokalne promjenljive tipa vector se ponaaju kao i sve druge automatske
promjenljive njihov kompletan sadraj se unitava nailaskom na kraj bloka u kojem su definirane
(ukljuujui i oslobaanje memorije koja je bila alocirana za potrebe smjetanja njihovih elemenata).
Kasnije emo detaljno razmotriti na koji je nain ovo postignuto.
Slino obinim dinamikim promjenljivim, i dinamiki nizovi se unitavaju tek na zavretku
programa, ili eksplicitnom upotrebom operatora delete[]. Stoga, pri njihovoj upotrebi takoer treba
voditi rauna da ne doe do curenja memorije, koje moe biti znatno ozbiljnije nego u sluaju obinih
dinamikih promjenljivih. Naroito treba paziti da dinamiki niz koji se alocira unutar neke funkcije
preko pokazivaa koji je lokalna promjenljiva obavezno treba i unititi prije zavretka funkcije, inae e
taj dio memorije ostati trajno zauzet do zavretka programa, i niko ga nee moi osloboditi (izuzetak
nastaje jedino u sluaju ako funkcija vraa kao rezultat pokaziva na alocirani niz u tom sluaju onaj
ko poziva funkciju ima mogunost da oslobodi zauzetu memoriju kada ona vie nije potrebna).
Viestrukim pozivom takve funkcije (npr. unutar neke petlje) moemo veoma brzo nesvjesno zauzeti svu
raspoloivu memoriju! Dakle, svaka funkcija bi prije svog zavretka morala osloboditi svu memoriju
koju je dinamiki zauzela (osim ukoliko vraa pokaziva na zauzeti dio memorije), i to bez obzira kako
se funkcija zavrava: nailaskom na kraj funkcije, naredbom return, ili bacanjem izuzetka! Naroito
se esto zaboravlja da funkcija, prije nego to baci izuzetak, takoer treba da za sobom poisti sve to
je zabrljala, to ukljuuje i oslobaanje dinamiki alocirane memorije! Jo je vei problem ukoliko
funkcija koja dinamiki alocira memoriju pozove neku drugu funkciju koja moe da baci izuzetak.
Posmatrajmo, na primjer, sljedei isjeak:
void F(int n) {
int *pok = new int[n];

G(n);

delete[] pok;
}
9

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

U ovom primjeru, funkcija F zaista brie kreirani dinamiki niz po svom zavretku, ali problem nastaje
ukoliko funkcija G koju ova funkcija poziva baci izuzetak! Kako se taj izuzetak ne hvata u funkciji
F, ona e takoer biti prekinuta, a zauzeta memorija nee biti osloboena. Naravno, prekid funkcije
F dovodi do automatskog unitavanja automatske lokalne pokazivake promjenljive pok, ali
dinamiki niz na iji poetak pok pokazuje nikada se ne unitava automatski, ve samo eksplicitnim
pozivom operatora delete[]. Stoga, ukoliko se operator delete[] ne izvri eksplicitno, zauzeta
memorija nee biti osloboena! Ovaj problem se moe rijeiti na sljedei nain:
void F(int n) {
int *pok = new int[n];

try {
G(n);
}
catch(...) {
delete[] pok;
throw;
}

delete[] pok;
}

U ovom sluaju u funkciji F izuzetak koji eventualno baca funkcija G hvatamo samo da bismo mogli
izvriti brisanje zauzete memorije, nakon ega uhvaeni izuzetak prosljeujemo dalje, navoenjem
naredbe throw bez parametara.
Iz ovog primjera vidimo da je bitno razlikovati sam dinamiki niz od pokazivaa koji se koristi za
pristup njegovim elementima. Ovdje je potrebno ponovo napomenuti da se opisani problemi ne bi
pojavili ukoliko bismo umjesto dinamike alokacije memorije koristili automatsku promjenljivu tipa
vector ona bi automatski bila unitena po zavretku funkcije F, bez obzira da li je do njenog
zavretka dolo na prirodan nain, ili bacanjem izuzetka iz funkcije G. U sutini, kad god moemo
koristiti tip vector, njegovo koritenje je jednostavnije i sigurnije od koritenja dinamike alokacije
memorije. Stoga, tip vector treba koristiti kad god je to mogue njegova upotreba sigurno nee
nikada dovesti do curenja memorije. Jedini problem je u tome to to nije uvijek mogue. Na primjer, nije
mogue napraviti tip koji se ponaa slino kao tip vector (ili sam tip vector) bez upotrebe
dinamike alokacije memorije i dobrog razumijevanja kao dinamika alokacija memorije funkcionira.
Iz svega to je do sada reeno moe se zakljuiti da dinamika alokacija memorije sama po sebi nije
komplicirana, ali da je potrebno preduzeti dosta mjera predostronosti da ne doe do curenja memorije.
Dodatne probleme izaziva injenica da operator new moe da baci izuzetak. Razmotrimo, na primjer,
sljedei isjeak:
try {
int *a = new int[n1], *b = new int[n2], *c = new int[n3];
// Radi neto sa a, b i c
delete[] a; delete[] b; delete[] c;
}
catch(...) {
cout << "Problemi sa memorijom!\n";
}

U ovom isjeku vri se alokacija tri dinamika niza, a u sluaju da alokacija ne uspije, prijavljuje se
greka. Meutim, problemi mogu nastati u sluaju da, na primjer, alokacije prva dva niza uspiju, a
alokacija treeg niza ne uspije. Tada e trei poziv operatora new baciti izuzetak, i izvravanje e se
nastaviti unutar catch bloka kao to je i oekivano, ali memorija koja je zauzeta sa prve dvije
uspjene alokacije ostaje zauzeta! Ovo moe biti veliki problem. Jedan nain da se rijei ovaj problem,

10

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

mada prilino rogobatan, je da se koriste ugnijedene try catch strukture, kao na primjer u
sljedeem isjeku:
try {
int *a = new int[n1];
try {
int *b = new int[n2];
try {
int *c = new int[n3];
// Radi neto sa a, b i c
delete[] a; delete[] b; delete[] c;
}
catch(...) {
delete[] b; throw;
}
}
catch(...) {
delete[] a; throw;
}
}
catch(...) {
cout << "Problemi sa memorijom!\n";
}

Najbolje je da sami analizirate tok navedenog isjeka uz raliite pretpostavke, koje mogu biti: prva
alokacija nije uspjela; druga alokacija nije uspjela; trea alokacija nije uspjela; sve alokacije su uspjele.
Na taj nain ete najbolje shvatiti kako ovo rjeenje radi.
Prikazano rjeenje je zaista rogobatno, mada je prilino jasno i logino. Ipak, postoji i mnogo
jednostavnije rjeenje (ne raunajui posve banalno rjeenje koje se zasniva da umjesto dinamike
alokacije memorije koristimo tip vector, kod kojeg ne moramo eksplicitno voditi rauna o brisanju
zauzete memorije). Ovo rjeenje zasniva se na injenici da standard jezika C++ garantira da operator
delete ne radi nita ukoliko im se kao argument proslijedi nul-pokaziva. To omoguava sljedee
veoma elegantno rjeenje navedenog problema:
int *a(0), *b(0), *c(0);
try {
a = new int[n1]; b = new int[n2]; c = new int[n3];
// Radi neto sa a, b i c
}
catch(...) {
cout << "Problemi sa memorijom!\n";
}
delete[] a; delete[] b; delete[] c;

U ovom primjeru svi pokazivai su prvo inicijalizirani na nulu, a zatim je u try bloku pokuana
dinamika alokacija memorije. Na kraju se na sve pokazivae primjenjuje delete operator, bez
obzira da li je alokacija uspjela ili nije. Tako e oni nizovi koji su alocirani svakako biti uniteni, a
ukoliko alokacija nekog od nizova nije uspjela, odgovarajui pokaziva e i dalje ostati nul-pokaziva (s
obzirom da kasnija dodjela nije izvrena jer je operator new bacio izuzetak), tako da operator
delete[] nee sa njim uraditi nita, odnosno nee biti nikakvih neeljenih efekata.
Bitno je napomenuti da je ponaanje operatora delete nedefinirano (i moe rezultirati krahom
programa) ukoliko se primijene na pokaziva koji ne pokazuje na prostor koji je zauzet u postupku
dinamike alokacije memorije. Naroito esta greka je primijeniti operator delete na pokaziva koji
pokazuje na prostor koji je ve obrisan (tj. na visei pokaziva). Ovo se, na primjer moe desiti ukoliko
dva puta uzastopno primijenimo ove operatore na isti pokaziva (kojem u meuvremenu izmeu dvije
primjene operatora delete nije dodijeljena neka druga vrijednost). Da bi se izbjegli ovi problemi,
11

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

veoma dobra ideja je eksplicitno dodijeliti nul-pokaziva svakom pokazivau nakon izvrenog
unitavanja bloka memorije na koju on pokazuje. Na primjer, ukoliko pok pokazuje na prvi element
nekog dinamikog niza, unitavanje tog niza najbolje je izvesti sljedeom konstrukcijom:
delete[] pok;
pok = 0;

Eksplicitnom dodjelom pok = 0 zapravo postiemo dva efekta. Prvo, takvom dodjelom eksplicitno
naglaavamo da pokaziva pok vie ne pokazuje ni na ta. Drugo, ukoliko sluajno ponovo
primijenimo operator delete na pokaziva pok, nee se desiti nita, jer je on sada nul-pokaziva.
U praksi se esto javlja potreba i za dinamikom alokacijom viedimenzionalnih nizova. Mada
dinamika alokacija viedimenzionalnih objekata nije direktno podrana u jeziku C++, ona se moe
vjeto simulirati. Kako su viedimenzionalni nizovi zapravo nizovi nizova, to je za indirektni pristup
njihovim elementima (pa samim tim i za njihovu dinamiku alokaciju) potrebno koristiti sloenije
pokazivake tipove, kao to su pokazivai na nizove, nizovi pokazivaa i pokazivai na pokazivae,
zavisno od primjene. Nije prevelik problem deklarirati ovakve sloene pokazivake tipove. Na primjer,
razmotrimo sljedee deklaracije:
int *pok1[30];
int (*pok2)[10];
int **p3;

U ovom primjeru, pok1 je niz od 30 pokazivaa na cijele brojeve, pok2 je pokaziva na niz od
10 cijelih brojeva (odnosno na itav niz kao cjelinu a ne pokaziva na prvi njegov element uskoro
emo vidjeti u emu je razlika), dok je pok3 pokaziva na pokaziva na cijele brojeve. Obratimo
panju izmeu razlike u deklaraciji pok1 koja predstavlja niz pokazivaa, i deklaracije pok2 koja
predstavlja pokaziva na niz. Na izvjestan nain, pojam niz prilikom deklaracija ima prioritet u odnosu
na pojam pokaziva, pa su u drugom primjeru zagrade bile neophodne da izvrnu ovaj prioritet.
Ovakve konstrukcije se mogu uslonjavati unedogled (npr. principijelno je mogue napraviti pokaziva
na niz od 10 pokazivaa na pokazivae na niz od 50 realnih brojeva), mada se potreba za tako sloenim
konstrukcijama javlja iznimno rijetko, i samo u veoma specifinim primjenama.
Mada konstrukcije poput gore prikazanih izgledaju veoma egzotino, potreba za njima se javlja u
izvjesnim situacijama, na primjer prilikom dinamike alokacije viedimenzionalnih nizova. Razmotrimo,
na primjer, kako bi se mogla realizirati dinamika alokacija dvodimenzionalnih nizova. Sjetimo se da su
dvodimenzionalni nizovi zapravo nizovi iji su elementi nizovi, a da se dinamika alokacija nizova iji su
elementi tipa Tip ostvaruje posredstvom pokazivaa na tip Tip. Slijedi da su nam za dinamiku
alokaciju nizova iji su elementi nizovi potrebni pokazivai na nizove. I zaista, sasvim je lako ostvariti
dinamiku alokaciju dvodimenzionalnog niza kod kojeg je druga dimenzija unaprijed poznata, npr.
matrice iji je broj kolona unaprijed poznat (situacija u kojpk druga dimenzija nije apriori poznata je
sloenija, kao to emo uskoro vidjeti). Pretpostavimo npr. da druga dimenzija niza kojeg hoemo da
alociramo iznosi 10, a da je prva dimenzija zadana u promjenljivoj n (ija vrijednost nije unaprijed
poznata). Tada ovu alokaciju moemo ostvariti pomou sljedee konstrukcije:
int (*a)[10] = new int[n][10];

Deklaracijom int (*a)[10] neposredno deklariramo a kao pokaziva na niz od 10 cijelih


brojeva, dok konstrukciju new int[n][10] moemo itati kao potrai u memoriji prostor za n
elemenata koji su nizovi od 10 cijelih brojeva, i vrati kao rezultat adresu pronaenog prostora (u formi
odgovarajueg pokazivakog tipa). Ovdje broj 10 u drugoj uglastoj zagradi iza operatora new ne
predstavlja parametar operatora, ve sastavni dio tipa, tako da on mora biti prava konstanta, a ne
promjenljiva ili nekonstantni izraz. Operator new ima samo jedan parametar, mada on doputa i drugi
par uglastih zagrada, samo to se u njemu obavezno mora nalaziti prava konstanta (s obzirom da one
ine sastavni dio tipa). Zbog toga nije mogue neposredno primjenom operatora new alocirati
dvodimenzionalne dinamike nizove ija druga dimenzija nije unaprijed poznata.

12

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

Razmotrimo malo detaljnije ta smo ovom konstrukcijom postigli. Ovdje smo pomou operatora
new alocirali dinamiki niz od n elemenata, koji su sami za sebe tipa niz od 10 cijelih brojeva (to
nije nita drugo nego dvodimenzionalni niz sa n redova i 10 kolona), i usmjerili pokaziva a da
pokazuje na prvi element takvog niza. Ova dodjela je legalna, jer se pokazivau na neki tip uvijek moe
dodijeliti adresa dinamikog niza iji su elementi tog tipa. Primijetimo da je u ovom sluaju pokaziva
a pokaziva na (itav) niz. Ovo treba razlikovati od obinih pokazivaa, za koje znamo da se po
potrebi mogu smatrati kao pokazivai na elemente niza. U brojnoj literaturi se ova dva pojma esto
brkaju, tako da se, kada se kae pokaziva na niz, obino misli na pokaziva na prvi element niza, a ne
na pokaziva na niz kao cjelinu (inae, ova dva tipa pokazivaa razlikuju se u tome kako na njih djeluje
pokazivaka aritmetika). Pokazivai na (itave) nizove koriste se prilino rijetko, i glavna im je primjena
upravo za dinamiku alokaciju dvodimenzionalnih nizova ija je druga dimenzija poznata.
U gore navedenom primjeru moemo rei da pokaziva a pokazuje na prvi element dinamikog
niza od n elemenata iji su elementi obini nizovi od 10 cjelobrojnih elemenata. Bez obzira na
injenicu da je a pokaziva, sa njim moemo baratati na iste nain kao da se radi o obinom
dvodimenzionalnom nizu. Tako je indeksiranje poput a[i][j] posve legalno, i interpretira se kao
(*(a + i))[j]. Naime, a je pokaziva, pa se izraz a[i] interpretira kao *(a + i). Meutim,
kako je a pokaziva na tip niz od 10 cijelih brojeva, to nakon njegovog dereferenciranja dobijamo
element tipa niz od 10 cijelih brojeva na koji se moe primijeniti indeksiranje. Interesantno je kako na
pokazivae na (itave) nizove djeluje pokazivaka aritmetika. Ukoliko a pokazuje na prvi element
niza, razumije se da a + 1 pokazuje na sljedei element. Meutim, kako su element ovog niza sami za
sebe nizovi, adresa na koju a pokazuje uveava se za itavu duinu nizovnog tipa niz od 10 cijelih
brojeva. U ovome je osnovna razlika izmeu pokazivaa na (itave) nizove i pokazivaa na elemente
niza. Sljedea slika moe pomoi da se shvati kako izgleda stanje memorije nakon ovakve alokacije, i ta
na ta pokazuje:
el. 0 el. 1

...

el. 9 el. 0 el. 1

...

a+1

...

el. 9

el. 0 el. 1

...

el. 9

a+n1

Gore pokazana primjena pokazivaa na nizove kao cjeline uglavnom je i jedina primjena takvih
pokazivaa. Zapravo, indirektno postoji i jo jedna primjena imena dvodimenzionalnih nizova
upotrijebljena sama za sebe automatski se konvertiraju u pokazivae na (itave) nizove s obzirom da su
dvodimenzionalni nizovi u sutini nizovi nizova. Takoer, formalni parametri funkcija koji su
deklarirani kao dvodimenzionalni nizovi zapravo su pokazivai na (itave) nizove (u skladu sa opim
tretmanom formalnih parametara nizovnog tipa). Ovo ujedno dodatno pojanjava zbog ega druga
dimenzija u takvim formalnim parametrima mora biti apriori poznata. Interesantno je primijetiti da se
pravilo o automatskoj konverziji nizova u pokazivae ne primjenjuje rekurzivno, tako da se imena
dvodimenzionalnih nizova upotrijebljena sama za sebe konvertiraju u pokazivae na nizove, a ne u
pokazivae na pokazivae, kako bi neko mogao pomisliti (u pokazivae na pokazivae se konvertiraju
imena nizova pokazivaa upotrijebljena sama za sebe). Takoer treba primijetiti da pokazivai na nizove
i pokazivai na pokazivae nisu jedno te isto (razlika je opet u djelovanju pokazivake aritmetike).
Razmotrimo sada kako bismo mogli dinamiku alokaciju dvodimenzionalnih nizova ija druga
dimenzija nije poznata unaprijed (strogo reeno, takva dinamika alokacija zbog nekih tehnikih razloga
zapravo nije ni mogua, ali se moe veoma uspjeno simulirati). Za tu svrhu su nam potrebni nizovi
pokazivaa. Sami po sebi, nizovi pokazivaa nisu nita osobito: to su nizovi iji su elementi pokazivai.
Na primjer, sljedea deklaracija
int *niz_pok[5];

13

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

deklarira niz niz_pok od 5 elemenata koji su pokazivai na cijele brojeve. Stoga, ukoliko su npr.
br_1, br_2 i br_3 cjelobrojne promjenljive, sve dolje navedene konstrukcije imaju smisla:
niz_pok[0] = &br_1;
niz_pok[1] = &br_2;
niz_pok[2] = new int(3);
niz_pok[3] = new int[10];
niz_pok[4] = new int[br_1];
*niz_pok[0] = 1;
br_3 = *niz_pok[1];
*niz_pok[br_1] = 5;
cout << niz_pok[2];
cout << *niz_pok[2];
delete niz_pok[2];
delete[] niz_pok[4];
niz_pok[3][5] = 100;

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

"niz_pok[0]" pokazuje na "br_1"


"niz_pok[1]" pokazuje na "br_2"
Alocira dinamiku promjenljivu
Alocira dinamiki niz
Takoer...
Djeluje poput "br_1 = 1";
Djeluje poput "br_3 = br_2"
Indirektno mijenja promjenljivu "br_2"
Ovo ispisuje adresu...
A ovo sadraj dinamike promjenljive...
Brie dinamiku promjenljivu
Brie dinamiki niz
Vrlo interesantno...

Posljednja naredba je posebno interesantna, jer pokazuje da se nizovima pokazivaa moe pristupati
kao da se radi o dvodimenzionalnim nizovima (pri emu je takav pristup smislen samo ukoliko
odgovarajui element niza pokazivaa pokazuje na element nekog drugog niza, koji moe biti i
dinamiki kreiran). Zaista, svaki element niza pokazivaa je pokaziva, a na pokazivae se moe
primijeniti indeksiranje. Stoga se izraz niz_pok[i][j] u sutini interpretira kao
*(niz_pok[i] + j).
Navedeni primjer ukazuje kako je mogue realizirati dinamiki dvodimenzionalni niz ija je prva
dimenzija poznata, ali ija druga dimenzija nije poznata (npr. matrice iji je broj redova poznat, ali broj
kolona nije). Kao to je ve reeno, jezik C++ ne podrava neposredno mogunost kreiranja takvih
nizova, ali se oni veoma lako mogu efektivno simulirati. Naime, dovoljno je formirati niz pokazivaa, a
zatim svakom od pokazivaa dodijeliti adresu dinamiki alociranih nizova koji predstavljaju redove
matrice. Na primjer, neka je potrebno realizirati dinamiku matricu sa 10 redova i m kolona, gdje je
m promjenljiva. To moemo uraditi na sljedei nain:
int *a[10];
for(int i = 0; i < 10; i++) a[i] = new int[m];

U ovom sluaju elementi niza pokazivaa a pokazuju na prve elemente svakog od redova matrice.
Strogo reeno, a uope nije dvodimenzionalni niz, nego niz pokazivaa koji pokazuju na poetke
neovisno alociranih dinamikih nizova, od kojih svaki predstavlja po jedan red matrice, i od kojih svaki
moe biti lociran u potpuno razliitim dijelovima memorije! Meutim, sve dok elementima ovakvog
dvodimenzionalnog niza moemo pristupati pomou izraza a[i][j] (a moemo, zahvaljujui
pokazivakoj aritmetici) ne trebamo se mnogo brinuti o stvarnoj organizaciji u memoriji. Zaista, a
moemo koristiti kao dvodimenzionalni niz sa 10 redova i m kolona, iako se radi samo o veoma vjetoj
simulaciji. Sa ovako simuliranim dvodimenzionalnim nizovima moemo raditi na gotovo isti nain kao i
sa pravim dvodimenzionalnim nizovima. Jedine probleme mogli bi eventualno izazvati prljavi trikovi
sa pokazivakom aritmetikom koji bi se oslanjali na pretpostavku da su redovi dvodimenzionalnih
nizova smjeteni u memoriji striktno jedan za drugim (to ovdje nije sluaj). Pravo stanje u memoriji kod
ovako simuliranih dvodimenzionalnih nizova moglo bi se opisati recimo sljedeom slikom:
a[0] a[1]

... a[9]

...

...

...
a

...

...

...
...

14

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

Moe se primijetiti upadljiva slinost izmeu opisane tehnike za dinamiku alokaciju


dvodimenzionalnih nizova i kreiranja nizova iji su elementi vektori. Kasnije emo vidjeti da u oba
sluaja u pozadini zapravo stoji isti mehanizam.
Kada smo govorili o vektorima iji su elementi vektori, govorili smo o mogunosti kreiranja
struktura podataka nazvanih grbave matrice, koji predstavljaju strukture koje po nainu upotrebe lie na
obine matrice, ali kod kojih razliiti redovi imaju razliit broj elemenata. Primijetimo da nam upravo
opisana simulacija dvodimenzionalnih nizova preko nizova pokazivaa takoer omoguava veoma
jednostavno kreiranje grbavih nizova. Na primjer, ukoliko izvrimo sekvencu naredbi
int *a[10];
for(int i = 0; i < 10; i++) a[i] = new int[i + 1];

tada e se niz pokazivaa a sa aspekta upotrebe ponaati kao dvodimenzionalni niz u kojem prvi red
ima jedan element, drugi element dva reda, trei element tri reda, itd.
Vano je primijetiti da je u svim prethodnim primjerima zasnovanim na nizovima pokazivaa,
dvodimenzionalni niz zapravo sastavljen od gomile posve neovisnih dinamikih jednodimenzionalnih
nizova, koje meusobno objedinjuje niz pokazivaa koji pokazuju na poetke svakog od njih. Stoga,
ukoliko elimo unititi ovakve dvodimenzionalne nizove (tj. osloboditi memorijski prostor koji oni
zauzimaju), moramo posebno unititi svaki od jednodimenzionalnih nizova koji ih tvore. To moemo
uiniti npr. na sljedei nain:
for(int i = 0; i < 10; i++) delete[] a[i];

Razmotrimo kako sada simulirati dvodimenzionalni niz u kojem niti jedna dimenzija nije unaprijed
poznata. Rjeenje se samo namee: potrebno je i niz pokazivaa (koji pokazuju na poetke svakog od
redova) realizirati kao dinamiki niz. Pretpostavimo, na primjer, da elimo simulirati dvodimenzionalni
niz sa n redova i m kolona, pri emu niti m niti n nemaju unaprijed poznate vrijednosti. To
moemo uraditi recimo ovako:
int **a = new int*[n];
for(int i = 0; i < n; i++) a[i] = new int[m];

Obratite panju na zvjezdicu koja govori da alociramo niz pokazivaa na cijele brojeve a ne niz
cijelih brojeva. Nakon ovoga, a moemo koristiti kako da se radi o dvodimenzionalnom nizu i pisati
a[i][j], mada je a zapravo pokaziva na pokaziva (dvojni pokaziva)! Zaista, kako je a
pokaziva, na njega se moe primijeniti indeksiranje, tako da se izraz a[i] interpretira kao
*(a + i). Meutim, nakon dereferenciranja dobijamo ponovo pokaziva (jer je a pokaziva na
pokaziva), na koji se ponovo moe primijeniti indeksiranje, tako da se na kraju izraz a[i][j]
zapravo interpretira kao *(*(a + i) + j). Bez obzira na interpretaciju, za nas je vana injenica da
se a gotovo svuda moe koristiti na nain kako smo navikli raditi sa obinim dvodimenzionalnim
nizovima). Naravno, ovakav dvodimenzionalni niz morali bismo brisati postupno (prvo sve redove, a
zatim niz pokazivaa koji ukazuju na poetke redova):
for(int i = 0; i < n; i++) delete[] a[i];
delete[] a;

Interesantno je uporediti obine (statike) dvodimenzionalne nizove, dinamike dvodimenzionalne


nizove sa poznatom drugom dimenzijom, simulirane dinamike dvodimenzionalne nizove sa poznatom
prvom dimenzijom, i simulirane dinamike dvodimenzionalne nizove sa obje dimenzije nepoznate. U
sva etiri sluaja za pristup individualnim elementima niza moemo koristiti sintaksu a[i][j], s tim
to se ona u drugom sluaju logiki interpretira kao (*(a + i))[j], u treem sluaju kao
*(a[i] + j), a u etvrtom sluaju kao *(*(a + i) + j). Meutim, uzmemo li u obzir da se ime
niza upotrijebljeno bez indeksiranja automatski konvertira u pokaziva na prvi element tog niza, kao i
injenicu da su istinski elementi dvodimenzionalnih nizova zapravo jednodimenzionalni nizovi, doi
15

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

emo do veoma interesantnog zakljuka da su u sva etiri sluaja sve etiri sintakse (a[i][j],
(*(a + i))[j], *(a[i] + j) i *(*(a + i) + j)) u potpunosti ekvivalentne, i svaka od njih se
moe koristiti u sva etiri sluaja! Naravno da se u praksi gotovo uvijek koristi sintaksa a[i][j], ali
je za razumijevanje sutine korisno znati najprirodniju interpretaciju za svaki od navedenih sluajeva.
Pokazivai na pokazivae poetnicima djeluju komplicirano zbog dvostruke indirekcije, ali oni nisu
nita kompliciraniji od klasinih pokazivaa, ako se ispravno shvati njihova sutina. U jeziku C++
pokazivai na pokazivae preteno se koriste za potrebe dinamike alokacije dvodimenzionalnih nizova,
dok su se u jeziku C dvojni pokazivai intenzivno koristili u situacijama u kojima je u jeziku C++
prirodnije upotrijebiti reference na pokaziva (tj. reference vezane za neki pokaziva). Na primjer,
pomou deklaracije
int *&ref = pok;

deklariramo referencu ref vezanu na pokazivaku promjenljivu pok (tako da je ref zapravo
referenca na pokaziva). Sada se ref ponaa kao alternativno ime za pokazivaku promjenljivu
pok. Obratite panju na redoslijed znakova * i &. Ukoliko bismo zamijenili redoslijed ovih
znakova, umjesto reference na pokaziva pokuali bismo deklarirati pokaziva na referencu, to nije
dozvoljeno u jeziku C++ (postojanje pokazivaa na referencu omoguilo bi pristup internoj strukturi
reference, a dobro nam je poznato da tvorci jezika C++ nisu eljeli da ni na kakav nain podre takvu
mogunost). Inae, deklaracije svih pokazivakih tipova mnogo su jasnije ako se itaju zdesna nalijevo
(tako da uz takvo itanje, iz prethodne deklaracije jasno vidimo da ref predstavlja referencu na
pokaziva na cijele brojeve).
Reference na pokazivae najee se koriste ukoliko je potrebno neki pokaziva prenijeti po
referenci kao parametar u funkciju, to je potrebno npr. ukoliko funkcija treba da izmijeni sadraj samog
pokazivaa (a ne objekta na koji pokaziva pokazuje). Na primjer, sljedea funkcija vri razmjenu dva
pokazivaa koji su joj proslijeeni kao stvarni parametri (zanemarimo ovom prilikom injenicu da e
generika funkcija Razmijeni koju smo razmatrali na prethodnim predavanjima sasvim lijepo
razmijeniti i dva pokazivaa, pri emu e se u postupku dedukcije tipa izvesti zakljuak da nepoznati tip
predstavlja tip pokazivaa na realne brojeve):
void RazmijeniPokazivace(double *&x, double *&y) {
double *pomocna = x; x = y; y = pomocna;
}

Kako u jeziku C nisu postojale reference, slian efekat se mogao postii jedino upotrebom dvojnih
pokazivaa, kao u sljedeoj funkciji
void RazmijeniPokazivace(double **p, double **q) {
double *pomocna = *p;
*p = *q; *q = pomocna;
}

Naravno, prilikom poziva ovakve funkcije RazmijeniPokazivace, kao stvarne argumente morali
bismo navesti adrese pokazivaa koje elimo razmijeniti (a ne same pokazivae), pri emu nakon
uzimanja adrese dobijamo dvojni pokaziva. Drugim rijeima, za razmjenu dva pokazivaa p1 i p2
(na tip double) morali bismo izvriti sljedei poziv:
RazmijeniPokazivace(&p1, &p2);

Oigledno, upotreba referenci na pokazivae (kao uostalom i upotreba bilo kakvih referenci) oslobaa
korisnika funkcije potrebe da eksplicitno razmilja o adresama.
Svi do sada prikazani postupci dinamike alokacije matrica nisu vodili rauna o tome da li su
alokacije zaista uspjele ili nisu. U realnim situacijama moramo i o tome voditi rauna, tako da bismo

16

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

dinamiku alokaciju matrice realnih brojeva sa n redova i m kolona zapravo trebali realizirati ovako
(razmislite sami zbog ega se prvo svi pokazivai inicijaliziraju na nule):
try {
double **a = new int*[n];
for(int i = 0; i < n; i++) a[i] = 0;
try {
for(int i = 0; i < n; i++) a[i] = new int[m];
}
catch(...) {
for(int i = 0; i < n; i++) delete[] a[i];
delete[] a;
throw;
}
}
catch(...) {
cout << "Problemi sa memorijom!\n";
}

Vidimo da dinamika alokacija matrica proizvoljnih dimenzija moe biti mukotrpna ukoliko vodimo
rauna o moguim memorijskim problemima. Naroito je mukotrpno identian postupak ponavljati za
svaku od matrica koju elimo da kreiramo. Zbog toga se kao prirodno rjeenje namee pisanje funkcija
koje obavljaju dinamiko stvaranje odnosno unitavanje matrica proizvoljnih dimenzija, koje na sebe
preuzimaju opisani postupak (naravno, jo jednostavnije rjeenje je koristiti vektore vektora, ali ovdje
elimo da nauimo mehanizme nieg nivoa koji lee u pozadini rada takvih tipova podataka). Sljedei
primjer pokazuje kako bi takve funkcije mogle izgledati (funkcije su napisane kao generike funkcije,
tako da omoguavaju stvaranje odnosno unitavanje matrica iji su elementi proizvoljnog tipa):
template <typename TipElemenata>
void UnistiMatricu(TipElemenata **mat, int broj_redova) {
if(mat == 0) return;
for(int i = 0; i < broj_redova; i++) delete[] mat[i];
delete[] mat;
}
template <typename TipElemenata>
TipElemenata **StvoriMatricu(int broj_redova, int broj_kolona) {
double **mat = new TipElemenata*[broj_redova];
for(int i = 0; i < broj_redova; i++) mat[i] = 0;
try {
for(int i = 0; i < broj_redova; i++)
mat[i] = new TipElemenata[broj_kolona];
}
catch(...) {
UnistiMatricu(mat, broj_redova);
throw;
}
return mat;
}

Obratimo panju na nekoliko detalja u ovim funkcijama. Prvo, funkcija UnistiMatricu je


napisana ispred funkcije StvoriMatricu, s obzirom da se ona poziva iz funkcije
StvoriMatricu. Takoer, funkcija UnistiMatricu napisana je tako da ne radi nita u sluaju da
joj se kao parametar prenese nul-pokaziva, ime je ostvarena konzistencija sa nainom na koji se
ponaaju operator delete. Konano, funkcija UnistiMatricu vodi rauna da iza sebe poisti
sve dinamiki alocirane nizove prije nego to eventualno baci izuzetak u sluaju da dinamika alokacija
nije uspjela do kraja (ovo ienje je realizirano pozivom funkcije UnistiMatricu).

17

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

Sljedei primjer ilustrira kako moemo koristiti napisane funkcije za dinamiko kreiranje i
unitavanje matrica:
int n, m;
cin >> n, m;
try {
double **a = 0, **b = 0;
a = StvoriMatricu<double>(n, m);
b = StvoriMatricu<double>(n, m);
// Radi neto sa matricama a i b
}
catch(...) {
cout << "Problemi sa memorijom!\n";
}
UnistiMatricu(a, n);
UnistiMatricu(b, n);

Primijetimo da smo dvojne pokazivae a i b koje koristimo za pristup dinamiki alociranim


matricama prvo inicijalizirali na nulu, prije nego to zaista pokuamo dinamiku alokaciju pozivom
funkcije StvoriMatricu. Na taj nain garantiramo da e u sluaju da alokacija ne uspije,
odgovarajui pokaziva biti nul-pokaziva, tako da kasniji poziv funkcije UnistiMatricu nee
dovesti do problema u sluaju da kreiranje matrice uope nije izvreno (ovdje imamo situaciju identinu
situaciji o kojoj smo ve govorili prilikom razmatranja dinamike alokacije obinih nizova). Generalno,
kad god dinamiki alociramo vie od jednog objekta, trebamo izbjegavati obavljanje dinamike alokacije
odmah prilikom inicijalizacije pokazivaa, jer u suprotnom prilikom brisanja objekata moemo imati
problema u sluaju da nije uspjela alokacija svih objekata (naime, ne moemo znati koji su objekti
alocirani, a koji nisu). Primijetimo jo da u funkciju UnistiMatricu moramo kao parametar
prenositi i broj redova matrice, s obzirom da nam je on potreban u for petlji, a njega nije mogue
saznati iz samog pokazivaa.
Prilikom poziva generike funkcije StvoriMatricu morali smo unutar iljastih zagrada <>
eksplicitno specificirati tip elemenata matrice, s obzirom da se tip elemenata ne moe odrediti
dedukcijom tipa iz parametara funkcije. Ovaj problem moemo izbjei ukoliko modificiramo funkciju
StvoriMatricu tako da kao jedan od parametara prihvata dvojni pokaziva za pristup elementima
matrice. Tako modificirana funkcija treba da u odgovarajui parametar smjesti adresu dinamiki
alocirane matrice (umjesto da tu adresu vrati kao rezultat). Naravno, u tom sluaju emo za dinamiko
alociranje matrice umjesto poziva poput
a = StvoriMatricu<double>(n, m);

koristiti poziv poput


StvoriMatricu(a, n, m);

Naravno, prvi parametar se mora prenositi po referenci da bi uope mogao biti promijenjen, to znai da
odgovarajui formalni parametar mora biti referenca na dvojni pokaziva (ne plaite se to ovdje imamo
trostruku indirekciju). Ovakvu modifikaciju moete sami uraditi kao korisnu vjebu. Treba li uope
napominjati da se u jeziku C (koji ne posjeduje reference) slian efekat moe ostvariti jedino upotrebom
trostrukih pokazivaa (odnosno pokazivaa na pokazivae na pokazivae)!?
Dinamiki kreirane matrice (npr. matrice kreirane pozivom funkcije StvoriMatricu) u veini
konteksta moemo koristiti kao i obine dvodimenzionalne nizove. Mogue ih je i prenositi u funkcije,
samo to odgovarajui formalni parametar koji slui za pristup dinamioj matrici mora biti definiran kao
dvojni pokaziva (kao u funkciji UnistiMatricu). Ipak, razlike izmeu dinamiki kreiranih matrica
kod kojih nijedna dimenzija nije poznata unaprijed i obinih dvodimenzionalnih nizova izraenije su u
odnosu na razlike izmeu obinih i dinamikih jednodimenzionalnih nizova. Ovo odraava injenicu da

18

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

u jeziku C++ zapravo ne postoje pravi dvodimenzionalni dinamiki nizovi kod kojih druga dimenzija
nije poznata unaprijed. Oni se mogu veoma vjerno simulirati, ali ne u potpunosti savreno. Tako, na
primjer, nije mogue napraviti jedinstvenu funkciju koja bi prihvatala kako statike tako i dinamike
matrice, jer se ime dvodimenzionalnog niza upotrijebljeno samo za sebe ne konvertira u dvojni
pokaziva, nego u pokaziva na niz, o emu smo ve govorili (naravno, u sluaju potrebe, mogue je
napraviti dvije funkcije sa identinim tijelima a razliitim zaglavljima, od kojih jedna prima statike a
druga dinamike matrice).
Ve smo rekli da nizovi pokazivaa glavnu primjenu imaju prilikom simuliranja dinamike alokacije
dvodimenzionalnih nizova. Meutim, nizovi pokazivaa mogu imati i druge primjene. Naroito su
korisni nizovi pokazivaa na znakove. Zamislimo, na primjer, da je neophodno da u nekom nizu uvamo
imena svakog od 12 mjeseci u godini. Ukoliko za tu svrhu koristimo obian niz nul-terminiranih
stringova odnosno dvodimenzionalni niz znakova, koristili bismo sljedeu deklaraciju:
char imena_mjeseci[12][10] = {"Januar", "Februar", "Mart", "April",
"Maj", "Juni", "Juli", "August", "Septembar", "Oktobar", "Novembar",
"Decembar"};

U ovom sluaju smo za svaki naziv morali zauzeti po 10 znakova, koliko iznosi najdui naziv
(Septembar), ukljuujui i prostor za NUL graninik. Mnogo je bolje kreirati niz dinamikih
stringova, odnosno niz objekata tipa string, kao u sljedeoj deklaraciji:
string imena_mjeseci[12] = {"Januar", "Februar", "Mart", "April",
"Maj", "Juni", "Juli", "August", "Septembar", "Oktobar", "Novembar",
"Decembar"};

Na ovaj nain, svaki element niza zauzima samo onoliki prostor koliko je zaista dug odgovarajui string.
Meutim, jo racionalnije rjeenje je deklaracija niza pokazivaa na znakove, kao u sljedeoj deklaraciji:
char *imena_mjeseci[12] = {"Januar", "Februar", "Mart", "April",
"Maj", "Juni", "Juli", "August", "Septembar", "Oktobar", "Novembar",
"Decembar"};

Ovakva inicijalizacija je posve legalna, s obzirom da se pokazivai na znakove mogu inicijalizirati


stringovnim konstantama. Primijetimo da postoji velika razlika izmeu prethodne tri deklaracije. U
prvom sluaju se za niz imena_mjeseci zauzima prostor od 12 10 = 120 znakova nakon ega se u
taj prostor kopiraju navedene stringovne konstante. U drugom sluaju, navedene stringovne konstante se
ponovo kopiraju u elemente niza, ali se pri tome za svaki element niza zauzima samo onoliko prostora
koliko je neophodno za smjetanje odgovarajueg stringa (ukljuujui i prostor za NUL graninik), to
ukupno iznosi prostor za 83 znaka. Oigledno je time ostvarena znaajna uteda. Meutim, u treem
sluaju se za niz imena_mjeseci zauzima samo prostor za 10 pokazivaa (tipino 4 bajta po
pokazivau na dananjim raunarima) koji se inicijaliziraju da pokazuju na navedene stringovne
konstante (bez njihovog kopiranja), koje su svakako pohranjene negdje u memoriji pa se na taj nain ne
troi dodatni memorijski prostor. Utede koje se na taj nain postiu svakako su primjetne, i bile bi jo
vee da su stringovi bili dui.
Nizovi pokazivaa su zaista korisne strukture podataka, i njihove primjene u jeziku C++ su
viestruke, tako da emo se sa njima intenzivno susretati kasnije. Da bismo bolje upoznali njihovu pravu
prirodu, razmotrimo jo jedan ilustrativan primjer. Pretpostavimo, na primjer, da je neophodno sa
tastature unijeti nekoliko reenica, pri emu broj reenica nije unaprijed poznat. Dalje, neka je poznato
da duine reenica mogu znatno varirati, ali da nee biti due od 1000 znakova. Jasno je da nam je
potrebna neka dvodimenzionalna znakovna struktura podataka, ali kakva? Kako broj reenica nije
poznat, prva dimenzija nije poznata unaprijed. to se tie druge dimenzije, mogli bismo je fiksirati na
1000 znakova, ali je veoma je neracionalno za svaku reenicu rezervirati prostor od 1000 znakova, s
obzirom da e veina reenica biti znatno kraa. Mnogo je bolje za svaku reenicu zauzeti onoliko
prostora koliko ona zaista zauzima. Jedan nain da to uinimo, i to zaista dobar, je koritenje

19

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

dinamikog niza ili vektora iji su elementi tipa string. Meutim, ovdje emo demonstrirati rjeenje
koje se zasniva na koritenju (dinamikog) niza pokazivaa. Rjeenje se zasniva na injenici da nizovi
pokazivaa omoguavaju simulaciju grbavih nizova, odnosno dvodimenzionalnih nizova iji redovi
nemaju isti broj znakova. Tako moemo formirati grbavi niz reenica, odnosno dvodimenzionalni niz
znakova u kojem svaki red moe imati ima razliit broj znakova. Sljedei primjer ilustrira kako to
najbolje moemo izvesti:
int broj_recenica;
cout << "Koliko elite unijeti reenica: ";
cin >> broj_recenica;
cin.ignore(10000, '\n');
char **recenice = new char*[broj_recenica];
for(int i = 0; i < broj_recenica; i++) {
char radni_prostor[1000];
cin.getline(radni_prostor, 1000);
recenice[i] = new char[strlen(radni_prostor) + 1];
strcpy(recenice[i], radni_prostor);
}
for(int i = broj_recenica - 1; i >= 0; i--)
cout << recenice[i] << endl;

U ovom primjeru svaku reenicu prvo unosimo u pomoni (statiki) niz radni_prostor. Nakon
toga, pomou funkcije strlen saznajemo koliko unesena reenica zaista zauzima prostora, alociramo
prostor za nju (duina se uveava za 1 radi potrebe za smjetanjem NUL graninika) i kopiramo
reenicu u alocirani prostor pomou funkcije strcpy. Postupak se ponavlja za svaku unesenu
reenicu. Primijetimo da je pomoni niz radni_prostor deklariran lokalno unutar petlje, tako da se
on automatski unitava im se petlja zavri. Kada smo unijeli reenice, sa njima moemo raditi ta god
elimo (u navedenom primjeru, ispisujemo ih u obrnutom poretku u odnosu na poredak u kojem smo ih
unijeli). Na kraju, ne smijemo zaboraviti unititi alocirani prostor onog trenutka kada nam on vie nije
potreban (to u prethodnom primjeru radi jednostavnosti nije izvedeno), inae e doi do curenja
memorije.
Kao korisna alternativa nizovima iji su elementi pokazivai, mogu se koristiti vektori iji su
elementi pokazivai, ime izbjegavamo potrebu za eksplicitnom alokacijom i dealokacijom memorije za
potrebe dinamikog niza. Sljedei primjer prikazuje kako bi prethodni primjer izgledao uz koritenje
vektora iji su elementi pokazivai (obratite panju kako se deklarira vektor iji su elementi pokazivai
na znakove):
int broj_recenica;
cout << "Koliko elite unijeti reenica: ";
cin >> broj_recenica;
cin.ignore(10000, '\n');
vector<char*> recenice(broj_recenica);
for(int i = 0; i < broj_recenica; i++) {
char radni_prostor[1000];
cin.getline(radni_prostor, 1000);
recenice[i] = new char[strlen(radni_prostor) + 1];
strcpy(recenice[i], radni_prostor);
}
for(int i = broj_recenica - 1; i >= 0; i--)
cout << recenice[i] << endl;

Opisano rjeenja radd veoma dobro. Ipak, ona su znatno komplikovanija od rjeenja koja se
zasnivaju na nizu ili vektoru iji su elementi tipa string. Prvo, u prikazanom primjeru moramo se
eksplicitno brinuti o alokaciji memorije za svaku reenicu, dok na kraju moramo eksplicitno unititi
alocirani prostor. U sluaju koritenja tipa string sve se odvija automatski (mada je, interno
posmatrano, niz ili vektor iji su elementi tipa string u memoriji organiziran na vrlo slian nain kao

20

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

i opisani grbavi niz znakova, odnosno kao niz ili vektor pokazivaa na poetke svakog od stringova).
Dalje, da bi gore prikazani primjer bio pouzdan, bilo bi potrebno dodati dosta sloene try catch
konstrukcije, koje e u sluaju da tokom rada ponestane memorije, obezbijediti brisanje do tada
alociranog prostora (pri upotrebi tipa string, ovo se takoer odvija automatski). Ipak, sa aspekta
efikasnosti, rjeenje zasnovano na upotrebi nizova pokazivaa moe imati izrazite prednosti u odnosu na
rjeenje zasnovano na nizu elemenata tipa string, pogotovo u sluajevima kada sa elementima niza
treba vriti neke operacije koje zahtijevaju intenzivno premjetanje elemenata niza (to se deava,
recimo, prilikom sortiranja), o emu emo govoriti u nastavku ovog teksta.
Iz izloenih primjera vidimo da grbavi nizovi mogu biti izuzetno efikasni sa aspekta utroka
memorije. Meutim, pri radu sa njima neophodan je izvjestan oprez zbog injenice da njegovi redovi
mogu imati razliite duine. Pretpostavimo, recimo, da je potrebno sortirati uneseni niz reenica iz
prethodnog primjera. Posve je jasno da je pri ma kakvom postupku sortiranja ma kakvog niza neophodno
s vremena na vrijeme vriti razmjenu elemenata niza ukoliko se ustanovi da su oni u neispravnom
poretku. Za sluaj da sortiramo niz reenica recenice koji je zadan kao obian dvodimenzionalni niz
znakova, razmjenu reenica koje se nalaze na i-toj i j-toj poziciji vjerovatno bismo izveli ovako:
char pomocna[1000];
strcpy(pomocna, recenice[i]);
strcpy(recenice[i], recenice[j]);
strcpy(recenice[j], pomocna);

S druge strane, ukoliko je niz recenice izveden kao niz elemenata tipa string, razmjenu bismo
vjerovatno izvrili ovako (na isti nain kao kad razmjenjujemo recimo elemente niza brojeva):
string pomocna = recenice[i];
recenice[i] = recenice[j]; recenice[j] = pomocna;

ta bismo, meutim, trebali uraditi ukoliko koristimo niz recenice koji je organiziran kao grbavi
niz znakova, odnosno niz pokazivaa na poetke svake od reenica? Jasno je da varijanta u kojoj bismo
kopirali itave reenice pomou funkcije strcpy nije prihvatljiva, s obzirom da smo za svaku
reenicu alocirali tano onoliko prostora koliko ona zaista zauzima. Stoga bi kopiranje due reenice na
mjesto koje je zauzimala kraa reenica neizostavno dovelo do pisanja u nedozvoljeni dio memorije
(npr. preko neke druge reenice), to bi moglo vrlo vjerovatno dovesti i do kraha programa. Da bismo
stekli uvid u to ta zaista trebamo uiniti, razmislimo malo o tome ta u ovom sluaju tano predstavlja
niz recenice. On je niz pokazivaa koji pokazuju na poetke svake od reenica. Meutim, da bismo
efektivno sortirali takav niz, uope nije potrebno da ispremjetamo pozicije reenica u memoriji:
dovoljno je samo da ispremjetamo pokazivae koji na njih pokazuju! Dakle, umjesto da razmijenimo
itave reenice, dovoljno je samo razmijeniti pokazivae koji na njih pokazuju, kao u sljedeem isjeku:
char *pomocni = recenice[i];
recenice[i] = recenice[j]; recenice[j] = pomocni;

Da bismo lake shvatili ta se zaista deava, pretpostavimo da niz recenice sadri 5 imena
Kemo, Stevo, Ivo, Amira i Meho (on zapravo sadri pokazivae na mjesta u memoriji gdje se
uvaju zapisi tih imena). Stoga situaciju u memoriji moemo ilustrirati sljedeom slikom (pri emu
oznake a1, a2, a3, a4 i a5 oznaavaju neke adrese ija je tana vrijednost nebitna):
...

a1 a2

a3

a4 a5

a1
'K' 'e' 'm' 'o' 0

a2
'S' 't' 'e' 'v' 'o' 0

...

recenice

...

'I' 'v' 'o' 0


a3

'A' 'm' 'i' 'r' 'a' 0


a4
21

'M' 'e' 'h' 'o' 0


a5

...

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 5.

Nakon izvrenog sortiranja, u kojem se vri razmjena pokazivaa, situacija u memoriji izgledala bi
ovako:
...

a4 a3

a1

a1
a5 a2

a2
'K' 'e' 'm' 'o' 0

'S' 't' 'e' 'v' 'o' 0

recenice

...

'I' 'v' 'o' 0


a3

'A' 'm' 'i' 'r' 'a' 0


a4

'M' 'e' 'h' 'o' 0


a5

...

Vidimo da ukoliko pokazivae itamo redom, oni pokazuju respektivno na imena Amira, Ivo,
Kemo, Meho i Stevo, a to je upravo sortirani redoslijed! Ovim ne samo da smo rijeili problem,
nego smo i samo sortiranje uinili znatno efikasnijim. Naime, sigurno je mnogo lake razmijeniti dva
pokazivaa u memoriji, nego premjetati itave reenice upotrebom funkcije strcpy. Interesantno je
da postupak razmjene sa aspekta sintakse obavljamo na isti nain kao da se radi o nizu iji su elementi
tipa string, mada treba voditi rauna da se u ovom sluaju uope ne razmjenjuju stringovi ve samo
pokazivai. Meutim, bitno je napomenuti da bismo poreenje reenica (sa ciljem utvrivanja da li su u
ispravnom poretku) morali koristiti funkciju strcmp iz biblioteke cstring, jer bi obina upotreba
relacionih operatora < ili > uporeivala adrese na kojima se reenice nalaze (u skladu sa
pokazivakom aritmetikom), a to nije ono to nam treba (relacione operatore smijemo koristiti samo
ukoliko su elementi niza koje uporeujemo tipa string).
Izloeni primjer ujedno ilustrira najvaniju primjenu nizova pokazivaa (ili vektora pokazivaa) u
jeziku C++. Kada god je potrebno kreirati nizove iji su elementi masivni objekti (poput stringova) sa
kojima je nezgrapno vriti razliite manipulacije poput premjetanja elemenata niza, mnogo je isplatnije
kreirati niz pokazivaa na objekte, a same objekte dinamiki alocirati (sa ovakvim primjerima emo se
sve ee susretati u izlaganjima koja slijede, jer emo se susretati sa sve vie i vie tipova masivnih
objekata). Tada, umjesto manipulacija sa samim objektima vrimo manipulacije sa pokazivaima koji na
njih pokazuju, to je neuporedivo efikasnije. Postoje ak i objekti koji se uope ne mogu premjetati ili
kopirati, tako da se manipulacije sa takvim objektima mogu vriti jedino indirektno, putem pokazivaa
koji na njih pokazuju!

22

You might also like