Professional Documents
Culture Documents
Boris Motik I Julijan Šribar - Demistificirani - C++ PDF
Boris Motik I Julijan Šribar - Demistificirani - C++ PDF
Demistificirani
Zagreb, 1997.
1
2
Roditeljima i bratu
Boris
Sadraj
7. Funkcije............................................................................... 150
7.1. to su i zato koristiti funkcije........................................................................150
7.2. Deklaracija i definicija funkcije .......................................................................152
7.2.1. Deklaracije funkcija u datotekama zaglavlja ..........................................155
7.3. Tip funkcije ....................................................................................................158
7.4. Lista argumenata...........................................................................................161
7.4.1. Funkcije bez argumenata ......................................................................161
7.4.2. Prijenos argumenata po vrijednosti .......................................................161
7.4.3. Pokaziva i referenca kao argument .....................................................164
7.4.4. Promjena pokazivaa unutar funkcije ....................................................166
7.4.5. Polja kao argumenti ...............................................................................168
7.4.6. Konstantni argumenti.............................................................................172
7.4.7. Podrazumijevani argumenti ...................................................................173
5
Prilikom pisanja ove knjige mnogi su nas, s podsmijehom kao da gledaju posljednji
primjerak snjenog leoparda, pitali: No zato se bavite C++ jezikom? To je
kompliciran jezik, spor, nedovoljno efikasan za primjenu u komercijalnim programima.
Na kraju, ja to sve mogu napraviti u obinom C-u. Prije nego to ponemo objanjavati
pojedine znaajke jezika, nalazimo vanim pokuati dati koliko-toliko suvisle odgovore
na ta i slina pitanja.
Dakle, osnovno pitanje je to C++ ini boljim i pogodnijim openamjenskim
jezikom za pisanje programa, od operacijskih sustava do baza podataka. Da bismo to
razumjeli, pogledajmo kojim putem je tekao povijesni razvoj jezika. Na taj nain e
moda biti jasnija motivacija Bjarne Stroustrupa, oca i majke jezika C++.
pojave C++ programskog jezika: potrebno je razviti programerski alat koji e osloboditi
programera rutinskih poslova te mu dopustiti da se usredotoi na problem koji rjeava.
Zbog toga se pojavljuje niz viih programska jezika, koji preuzimaju na sebe neke
dosadne programerske poslove. Tako je FORTRAN bio posebno pogodan za
matematike proraune, zatim BASIC koji se vrlo brzo uio, te COBOL koji je bio u
pravilu namijenjen upravljanju bazama podataka.
Oko 1972. se pojavljuje jezik C, koji je direktna pretea dananjeg jezika C++. To
je bio prvi jezik ope namjene te je postigao nevien uspjeh. Vie je razloga tome: jezik
je bio jednostavan za uenje, omoguavao je modularno pisanje programa, sadravao je
samo naredbe koje se mogu jednostavno prevesti u strojni jezik, davao je brzi kd. Jezik
nije bio optereen mnogim sloenim funkcijama, kao na primjer skupljanje smea (engl.
garbage collection): ako je takav podsustav nekome trebao, korisnik ga je sam napisao.
Jezik je omoguavao vrlo dobru kontrolu strojnih resursa te je na taj nain omoguio
programerima da optimiziraju svoj kd. Do unatrag nekoliko godina, 99% komercijalnih
programa bili su pisani u C-u, ponegdje dopunjeni odsjecima u strojnom jeziku kako bi
se kritini dijelovi sustava uinili dovoljno brzima.
No kako je razvoj programske podrke napredovao, stvari su se i na podruju
programskih jezika poele mijenjati. Sloeni projekti od nekoliko stotina tisua, pa i
vie redaka vie nisu rijetkost, pa je zbog toga bilo potrebno uvesti dodatne mehanizme
kojima bi se takvi programi uinili jednostavnijima za izradu i odravanje, te kojima bi
se omoguilo da se jednom napisani kd iskoristi u vie razliitih projekata.
Bjarne Stroustrup je zapoeo 1979. godine rad na jeziku C s klasama (engl. C
with Classes). Prije toga, u Computing Laboratory of Cambridge on je radio na svom
doktoratu te istraivao distribuirane sustave: granu raunalne znanosti u kojoj se
prouavaju modeli obrade podataka na vie jedinica istodobno. Pri tome koristio se
jezikom Simula, koji posjedovao neka vana svojstva koja su ga inila prikladnim za taj
posao. Na primjer, Simula je posjedovala pojam klase: strukture podataka koje
objedinjavaju podatke i operacije nad podacima. Koritenje klasa omoguilo je da se
koncepti problema koji se rjeava izraze direktno pomou jezinih konstrukcija.
Dobiveni kd je bio vrlo itljiv i razumljiv, a g. Stroustrup je bio posebno fasciniran
nainom na koji je sam programski jezik upuivao programera u razmiljanju o
problemu. Takoer, jezik je posjedovao sustav tipizacije, koji je esto pomagao
korisniku u pronalaenju pogreaka ve prilikom prevoenja.
No naoko idealan u teoriji, jezik Simula je posrnuo u praksi: prevoenje je bilo
iznimno dugotrajno, a dobiveni kd se izvodio ekstremno sporo. Dobiveni program je
bio neupotrebljiv, i da bi ipak poteno zaradio svoj doktorat, gospodin Stroustrup se
morao potruditi i ponovo napisati cjelokupan program u jeziku BCPL jeziku niske
razine koji je omoguio vrlo dobre performanse prevedenog programa. No iskustvo
pisanja sloenog programa u takvom jeziku je bilo uasno i g. Stroustrup je, po
zavretku svog posla na Cambridgeu, vrsto sebi obeao da vie nikada nee takav
sloen problem pokuati rijeiti neadekvatnim alatima poput BCPL-a ili Simule.
Kada se 1979. zaposlio u Bell Labs u Murray Hillu, zapoeo je rad na onome to e
kasnije postati C++. Pri tome je iskoristio svoje iskustvo steeno prilikom rada na
doktoratu te pokuao stvoriti univerzalni jezik koji e udovoljiti dananjim zahtjevima.
12
Pri tome je uzeo dobra svojstva niza jezika: Simula, Clu, Algol68 i Ada, a kao osnovu je
uzeo jezik C.
vam ba sad treba, a vie ete razmiljati o tome kako da ubrzate postupak sjenanja ili
kako da ga uinite realistinijim.
Objektna tehnologija je pokuala dati odgovore na neke potrebe ljudi koji rjeavaju
svoje zadatke raunalom; na vama je da procijenite koliko je to uspjeno, a u svakom
sluaju da prije toga proitate ovu knjigu do kraja i preporuite ju prijateljima, naravno.
Jer tak dobru i guba knjigu niste vidli ve sto godina i ba vam je bilo fora ju itat.
2.5. Literatura
Tijekom pisanja knjige koristili smo sljedeu literaturu (navodimo ju abecednim
slijedom autora):
[ANSI95] Working Paper for Draft Proposed International Standard for
Information Systems Programming Language C++, Technical Report
X3J16/95-0087, American National Standards Institute (ANSI), April
1995
[Barton94] John J. Barton, Lee R. Nackman: Scientific and Engineering C++ An
Introduction with Advanced Techniques and Examples, Addison-
Wesley, Reading, MA, 1994, ISBN 0-201-53393-6
[Borland94] Borland C++ 4.5 Programmers Guide, Borland International, Scotts
Valley, CA
[Carroll95] Martin D. Carroll, Margaret A. Ellis: Designing and Coding Reusable
C++, Addison-Wesley, Reading, MA, 1995, ISBN 0-201-51284-X
[Ellis90] Margaret A. Ellis, Bjarne Stroustrup: The Annotated C++ Reference
Manual, Addison-Wesley, Reading, MA, 1990, ISBN 0-201-51459-1
17
Iako se radi o nacrtu standarda, on u potpunosti definira sve karakteristike programskog jezika
kakav e on biti kada se (prema planu ANSI komiteta za standardizaciju jezika C++) u
prosincu 1998. godine objavi konana verzija standarda. Sve promjene koje e se dogaati do
tada odnosit e se iskljuivo na tekst standarda sintaksa jezika i definicije biblioteka ostaju
nepromijenjene.
18
mree trebalo oko 6 sati vremena da se prebaci na nae raunalo. Najnovija verzija
nacrta standarda moe se naruiti i potom (po cijeni od 50 USD + potarina) na
sljedeoj adresi:
X3 Secretariat
CBEMA
1250 Eye Street NW
Suite 200
Washington, DC 20005
Naravno da ga ne preporuujemo za uenje jezika C++, a takoer vjerujemo da nee
trebati niti iskusnijim korisnicima Standard je prvenstveno namijenjen proizvoaima
softvera. Svi noviji prevoditelji (engl. compilers) bi se trebali ponaati u skladu s tim
standardom.
Zadnje poglavlje ove knjige posveeno je principima objektno orijentiranog
programiranja. Naravno da to nije dovoljno za ozbiljnije sagledavanje detaljnije o
objektno orijentiranom programiranju zainteresirani itatelj moe proitati u referentnoj
knjizi Grady Booch: Object-Oriented Analysis and Design with Applications (2nd
Edition), Benjamin/Cummings Publishing Co., Redwood City, CA, 1994, ISBN 0-8053-
5340-2, kao i u nizu lanaka istog autora u asopisu C++ Report.
2.6. Zahvale
Zahvaljujemo se svima koji su nam izravno ili posredno pomogli pri izradi ove knjige.
Posebno se zahvaljujemo Branimiru Pejinoviu (Portland State University) koji nam
je omoguio da doemo do Nacrta ANSI C++ standarda, Vladi Glaviniu (Fakultet
elektrotehnike i raunarstva) koji nam je omoguio da doemo do knjiga
[Stroustrup94] i [Carroll95] te nam je tijekom pripreme za tisak dao na raspolaganje
laserski pisa, te Zdenku imiu (Fakultet elektrotehnike i raunarstva) koji nam je,
tijekom svog boravka u SAD, pomogao pri nabavci dijela literature.
Posebnu zahvalu upuujemo Ivi Mesari koja je proitala cijeli rukopis te svojim
korisnim i konstruktivnim primjedbama znatno doprinijela kvaliteti iznesene materije.
Takoer se zahvaljujemo Zoranu Kalafatiu (Fakultet elektrotehnike i raunarstva) i
Damiru Hodaku koji su itali dijelove rukopisa i dali na njih korisne opaske.
Boris se posebno zahvaljuje gospoama ribar na tonama kolaa pojedenih za
vrijeme dugih, zimskih noi itanja i ispravljanja rukopisa, a koje su ga kotale kure
mravljenja.
I naravno, zahvaljujemo se Bjarne Stroustrupu i dekima iz AT&T-a to su izmislili
C++, na najdrai programski jezik. Bez njih ne bi bilo niti ove knjige (ali moda bi bilo
sline knjige iz FORTRAN-a).
19
to je to naredba?
Da ugasim svjetiljku. Dobra veer. I on je ponovo upali.
Ne razumijem ree mali princ.
Nema to tu razumjeti ree noobdija. Naredba je
naredba. Dobar dan. I on ugasi svoju svjetiljku.
Antoine de Saint-Exupry (19001944), Mali princ
Ne
Da bi se za neki program moglo rei da je
uspjeno zgotovljen, treba uspjeno proi
Izvoenje programa
kroz sve etiri faze.
Kao i svaki drugi posao, i pisanje
programa iziskuje odreeno znanje i vjetinu.
Prilikom pisanja programera vrebaju Scile i
Da Pogreke tijekom
Haribde, danas poznatije pod nazivom
izvoenja? pogreke ili bugovi (engl. bug - stjenica) u
programu. Uoi li se pogreka u nekoj od faza
Ne
izrade programa, izvorni kd treba doraditi i
ponoviti sve prethodne faze. Zbog toga
Konac postupak izrade programa nije pravocrtan, ve
manje-vie podsjea na mukotrpno petljanje u
krug. Na slici 3.1 je shematski prikazan
Slika 3.1. Tipian razvoj programa cjelokupni postupak izrade programa, od
njegova zaetka, do njegova okonanja.
Analizirajmo najvanije faze izrade programa.
Prva faza programa je pisanje izvornog kda. U principu se izvorni kd moe pisati
u bilo kojem programu za ureivanje teksta (engl. text editor), meutim velika veina
dananjih prevoditelja i povezivaa se isporuuje kao cjelina zajedno s ugraenim
programom za upis i ispravljanje izvornog kda. Te programske cjeline poznatije su pod
21
Ovdje neemo opisivati konkretno kako se pokreu postupci prevoenja ili povezivanja, jer to
varira ovisno o prevoditelju, odnosno povezivau.
22
int main() {
return 0;
}
Svaki program napisan u C++ jeziku mora imati tono (ni manje ni vie) jednu
main() funkciju.
Pritom valja uoiti da je to samo simboliko ime koje daje do znanja prevoditelju koji se
dio programa treba prvo poeti izvoditi ono nema nikakve veze s imenom izvedbenog
programa koji se nakon uspjenog prevoenja i povezivanja dobiva. eljeno ime
izvedbenog programa odreuje sam programer: ako se koriteni prevoditelj pokree iz
komandne linije, ime se navodi kao parametar u komandnoj liniji, a ako je prevoditelj
ugraen u neku razvojnu okolinu, tada se ime navodi kao jedna od opcija. Tone detalje
definiranja imena izvedbenog imena itateljica ili itatelj nai e u uputama za
prevoditelj kojeg koristi.
Rije int ispred oznake glavne funkcije ukazuje na to da e main() po zavretku
izvoenja naredbi i funkcija sadranih u njoj kao rezultat tog izvoenja vratiti cijeli broj
(int dolazi od engleske rijei integer koja znai cijeli broj). Budui da se glavni
program pokree iz operacijskog sustava (DOS, UNIX, MS Windows), rezultat glavnog
programa se vraa operacijskom sustavu. Najee je to kd koji signalizira pogreku
nastalu tijekom izvoenja programa ili obavijest o uspjenom izvoenju.
Iza rijei main slijedi par otvorena-zatvorena zagrada. Unutar te zagrade trebali bi
doi opisi podataka koji se iz operacijskog sustava prenose u main(). Ti podaci
nazivaju se argumenti funkcije. Za funkciju main() to su parametri koji se pri
pokretanju programa navode u komandnoj liniji iza imena programa, kao na primjer:
U nekim programskim jezicima glavna funkcija se zove glavni program, a sve ostale funkcije
potprogrami.
24
pkunzip -t mojzip
Radi kratkoe emo u veini primjera u knjizi izostavljati uvodni i zakljuni dio
glavne funkcije te emo podrazumijevati da oni u konkretnom kdu postoje.
Pogledajmo jo na trenutak to bi se dogodilo da smo kojim sluajem napravili
neku pogreku prilikom upisivanja gornjeg kda. Recimo da smo zaboravili desnu
vitiastu zagradu na kraju kda:
int main() {
return 0;
int main() {
}
neki prevoditelji e javiti upozorenje oblika Funkcija bi trebala vratiti vrijednost. Iako
se radi o pogreki, prevoditelj e umetnuti odgovarajui kd prema svom nahoenju i
prevesti program. Ne mali broj korisnika e zbog toga zanemariti takva upozorenja.
Meutim, valja primijetiti da esto taj umetnuti kd ne odgovara onome to je
programer zamislio. Zanemarivanjem upozorenja programer gubi pregled nad
korektnou kda, pa se lako moe dogoditi da rezultirajui izvedbeni kd daje na prvi
pogled neobjanjive rezultate.
25
#include <iostream.h>
int main() {
cout << "Ja sam za C++!!! A vi?" << endl;
return 0;
}
U ovom primjeru uoavamo dva nova retka. U prvom retku nalazi se naredba
#include <iostream.h> kojom se od prevoditelja zahtijeva da u na program
ukljui biblioteku iostream. U toj biblioteci nalazi se izlazni tok (engl. output stream)
te funkcije koje omoguavaju ispis podataka na zaslonu. Ta biblioteka nam je
neophodna da bismo u prvom retku glavnoga programa ispisali tekst poruke. Naglasimo
da #include nije naredba C++ jezika, nego se radi o pretprocesorskoj naredbi.
Naletjevi na nju, prevoditelj e prekinuti postupak prevoenja kda u tekuoj datoteci,
skoiti u datoteku iostream.h, prevesti ju, a potom se vratiti u poetnu datoteku na
redak iza naredbe #include. Sve pretprocesorske naredbe poinju znakom #.
iostream.h je primjer datoteke zaglavlja (engl. header file, odakle i slijedi
nastavak .h). U takvim datotekama se nalaze deklaracije funkcija sadranih u
odgovarajuim bibliotekama. Jedna od osnovnih znaajki (zli jezici e rei mana) jezika
C++ jest vrlo oskudan broj funkcija ugraenih u sam jezik. Ta oskudnost olakava
uenje samog jezika, te bitno pojednostavnjuje i ubrzava postupak prevoenja. Za
specifine zahtjeve na raspolaganju je veliki broj odgovarajuih biblioteka funkcija i
klasa.
cout je ime izlaznog toka definiranog u biblioteci iostream, pridruenog zaslonu
raunala. Operatorom << (dva znaka manje od) podatak koji slijedi upuuje se na
izlazni tok, tj. na zaslon raunala. U gornjem primjeru to je kratka promidbena poruka:
cout << "Ja sam za C++!!! A vi?" << endl << "Ne, hvala!";
26
Zadatak. Kako bi izgledao izvorni kd ako bismo eljeli ispis endl znaka staviti u
posebnu naredbu? Dodajte u gornji primjer ispis poruke Imamo C++!!! u sljedeem
retku (popratno sklapanje ruku iznad glave nije neophodno). Nemojte zaboraviti dodati
i ispis endl na kraju tog retka!
#include <iostream.h>
int main() {
int a, b, c;
int a, b, c;
Svaki puta kada nam u programu treba ispis podataka na zaslonu ili unos
podataka preko tipkovnice pomou ulazno-izlaznih tokova cin ili cout,
treba ukljuiti zaglavlje odgovarajue iostream biblioteke
pretprocesorskom naredbom #include.
c = a + b;
Ona kae raunalu da treba zbrojiti vrijednosti varijabli a i b, te njihov zbroj pohraniti u
varijablu c, tj. u memoriju na mjesto gdje je prevoditelj rezervirao prostor za tu
varijablu.
U poetku e itatelj zasigurno imati problema s razluivanjem operatora << i >>.
U vjeri da e olakati razlikovanje operatora za ispis i uitavanje podataka, dajemo
sljedei naputak.
Primjerice,
>> a
<< c
preslikava vrijednost iz c.
28
3.4. Komentari
Na nekoliko mjesta u gornjem kdu moemo uoiti tekstove koji zapoinju dvostrukim
kosim crtama (//). Radi se o komentarima. Kada prevoditelj naleti na dvostruku kosu
crtu, on e zanemariti sav tekst koji slijedi do kraja tekueg retka i prevoenje e
nastaviti u sljedeem retku. Komentari dakle ne ulaze u izvedbeni kd programa i slue
programerima za opis znaenja pojedinih naredbi ili dijelova kda. Komentari se mogu
pisati u zasebnom retku ili recima, to se obino rabi za dulje opise, primjerice to
odreeni program ili funkcija radi:
//
// Ovo je moj trei C++ program, koji zbraja
// dva broja unesena preko tipkovnice, a zbroj
// ispisuje na zaslonu.
// Autor: N. N. Hacker III
//
Za krae komentare, na primjer za opise varijabli ili nekih operacija, komentar se pie u
nastavku naredbe, kao to smo vidjeli u primjeru.
Uz gore navedeni oblik komentara, jezik C++ podrava i komentare unutar para
znakova /* */. Takvi komentari zapoinju slijedom /* (kosa crta i zvjezdica), a
zavravaju slijedom */ (zvjezdica i kosa crta). Kraj retka ne znai podrazumijevani
zavretak komentara, pa se ovakvi komentari mogu protezati na nekoliko redaka
izvornog kda, a da se pritom znak za komentiranje ne mora ponavljati u svakom retku:
/*
Ovakav nain komentara
preuzet je iz programskog
jezika C.
*/
/**********************************************************
**********************************************************/
c = a + b; // zbraja a i b
i++; // uveava i za 1
y = sqrt(x) // poziva funkciju sqrt
c = a + b;
mogli pisati
30
c=a+b;
c= a + b ;
c =
a + b;
#include <iostream.h>
int main() {
// pogreka: nepravilno razdvojeni znakovni niz
cout << "Pojavom jezika C++ ostvaren je
tisuljetni san svih programera" << endl;
return 0;
}
pri emu nastavak niza ne smijemo uvui, jer bi prevoditelj dotine praznine prihvatio
kao dio niza. Stoga je u takvim sluajevima preglednije niz rastaviti u dva odvojena
niza:
Pritom se ne smije zaboraviti staviti prazninu na kraju prvog ili poetak drugog niza.
Gornji niz mogli smo rastaviti na bilo kojem mjestu:
#include <iostream.h>
int main() {
int a, b, c; cout << "Upii prvi broj:"; cin >> a;
cout << "Upii i drugi broj:"; cin >> b; c = a + b;
cout << "Njihov zbroj je: " << c << endl;
return 0;
}
Program e biti preveden bez pogreke i raditi e ispravno. No, oito je da je ovako
pisani izvorni kd daleko neitljiviji pisanje vie naredbi u istom retku treba
prakticirati samo u izuzetnim sluajevima.
32
1
Svaki program sadri u sebi podatke koje obrauje. Njih moemo podijeliti na
nepromjenjive konstante, odnosno promjenjive varijable (promjenjivice).
Najjednostavniji primjer konstanti su brojevi (5, 10, 3.14159). Varijable su podaci koji
openito mogu mijenjati svoj iznos. Stoga se oni u izvornom kdu predstavljaju ne
svojim iznosom ve simbolikom oznakom, imenom varijable.
Svaki podatak ima dodijeljenu oznaku tipa, koja govori o tome kako se dotini
podatak pohranjuje u memoriju raunala, koji su njegovi dozvoljeni rasponi vrijednosti,
kakve su operacije mogue s tim podatkom i sl. Tako razlikujemo cjelobrojne, realne,
logike, pokazivake podatke. U poglavlju koje slijedi upoznat emo se ugraenim
tipovima podataka i pripadajuim operatorima.
4.1. Identifikatori
Mnogim dijelovima C++ programa (varijablama, funkcijama, klasama) potrebno je dati
odreeno ime. U tu svrhu koriste se identifikatori, koje moemo proizvoljno nazvati. Pri
tome je bitno potivati tri osnovna pravila:
1. Identifikator moe biti sastavljen od kombinacije slova engleskog alfabeta (A - Z, a -
z), brojeva (0 - 9) i znaka za podcrtavanje '_' (engl. underscore).
2. Prvi znak mora biti slovo ili znak za podcrtavanje.
3. Identifikator ne smije biti jednak nekoj od kljunih rijei (vidi tablicu 4.1) ili nekoj od
alternativnih oznaka operatora (tablica 4.2). To ne znai da kljuna rije ne moe biti
dio identifikatora moj_int je dozvoljeni identifikator. Takoer, treba izbjegavati
da naziv identifikatora sadri dvostruke znakove podcrtavanja (_ _) ili da zapoinje
znakom podcrtavanja i velikim slovom, jer su takve oznake rezervirane za C++
implementacije i standardne biblioteke (npr. _ _LINE_ _, _ _FILE_ _).
Stoga si moemo pustiti mati na volju pa svoje varijable i funkcije nazivati svakojako.
Pritom je vjerojatno svakom jasno da je zbog razumljivosti kda poeljno imena
odabirati tako da odraavaju stvarno znaenje varijabli, na primjer:
Pribrojnik1
Pribrojnik2
rezultat
33
34
Snjeguljica_i_7_patuljaka
MojaPrivatnaVarijabla
FrankieGoesToHollywood
RockyXXVII
maliNarodiVelikeIdeje
MaliNarodiVelikeIdeje
malinarodiVELIKEIDEJE
nekoliko je ovdje prilino rastezljiv), pa ako prevoditelj kojeg koristite uzima u obzir
samo prvih 14 znakova, tada e identifikatore
Snjeguljica_i_7_patuljaka
Snjeguljica_i_sedam_patuljaka
interpretirati kao iste, iako se razlikuju od petnaestog znaka ('7' odnosno 's') na dalje.
Naravno da dugaka imena treba izbjegavati radi vlastite komocije, posebice za
varijable i funkcije koje se esto ponavljaju.
Ponekad je pogodno koristiti sloena imena zbog boljeg razumijevanja kda. Iz
gornjih primjera itateljica ili itatelj mogli su razluiti dva najea pristupa
oznaavanju sloenih imena. U prvom pristupu rijei od kojih je ime sastavljeno
odvajaju se znakom za podcrtavanje, poput
Snjeguljica_te_sedam_patuljaka
SnjeguljicaTeSedamPatuljaka
Mi emo u primjerima preferirati potonji nain samo zato jer tako oznaene varijable i
funkcije zauzimaju ipak neto manje mjesta u izvornom kdu.
Iako ne postoji nikakvo ogranienje na poetno slovo naziva varijabli, veina
programera preuzela je iz programskog jezika FORTRAN naviku da imena cjelobrojnih
varijabli zapoinje slovima i, j, k, l, m ili n.
int a, b, c;
int a, b, c;
int a; // pogreka: ponovno koritenje naziva a
Varijable postaju realna zbiljnost tek kada im se pokua pristupiti, na primjer kada im se
pridrui vrijednost:
a = 2;
b = a;
c = a + b;
int
int
int
int aa :
22
a = 5;
tada se u memoriju raunala na mjestu gdje je prije bio smjeten broj 2 pohranjuje broj
5, kako je prikazano slikom 4.2.
37
int
int
int
int aa :
55
int a = 2;
int b = a;
int c = a + b;
2 = 4 / 2 // pogreka!!!
3 * 4 = 12 // pogreka!!!
3.14159 = pi // Bingooo!!! Trea pogreka!
Pokuamo li prevesti ovaj kd, prevoditelj e javiti pogreke. Objekti koji se smiju
nalaziti s lijeve strane znaka pridruivanja nazivaju se lvrijednosti (engl. lvalues, kratica
od left-hand side values). Pritom valja znati da se ne moe svakoj lvrijednosti
pridruivati nova vrijednost neke varijable se mogu deklarirati kao konstantne (vidi
poglavlje 4.4.4) i pokuaj promjene njihove vrijednosti prevoditelj e naznaiti kao
pogreku. Stoga se posebno govori o promjenjivim lvrijednostima. S desne strane
operatora pridruivanja mogu biti i lvrijednosti i konstante.
Evo tipinog primjera pridruivanja kod kojeg se ista varijabla nalazi i s lijeve i s
desne strane znaka jednakosti:
38
int i = 5;
i = i + 3;
a = b = c = 0;
a = b = c + d; // OK!
a = b + 1 = c; // pogreka: b + 1 nije lvrijednost
4.4.1. Brojevi
U jeziku C++ ugraena su u sutini dva osnovna tipa brojeva: cijeli brojevi (engl.
integers) i realni brojevi (tzv. brojevi s pominom decimalnom tokom, engl. floating-
point). Najjednostavniji tip brojeva su cijeli brojevi njih smo ve upoznali u Naem
39
Takva varijabla e zauzeti u memoriji vie prostora i operacije s njom e dulje trajati.
Cjelobrojne konstante se mogu u izvornom kdu prikazati u razliitima brojevnim
sustavima:
dekadskom,
oktalnom,
heksadekadskom.
Dekadski prikaz je razumljiv veini obinih nehakerskih smrtnika koji se ne sputaju na
razinu raunala. Meutim, kada treba raditi operacije nad pojedinim bitovima, oktalni i
heksadekadski prikazi su daleko primjereniji.
U dekadskom brojevnom sustavu, koji koristimo svakodnevno, postoji 10 razliitih
znamenki (0, 1, 2,..., 9) ijom kombinacijom se dobiva bilo koji eljeni vieznamenkasti
broj. Pritom svaka znamenka ima deset puta veu teinu od znamenke do nje desno. U
oktalnom brojevnom sustavu broj znamenki je ogranien na 8 (0, 1, 2,..., 7), tako da
svaka znamenka ima osam puta veu teinu od svog desnog susjeda. Na primjer, 11 u
oktalnom sustavu odgovara broju 9 u dekadskom sustavu (118 = 910). U
heksadekadskom sustavu postoji 16 znamenki: 0, 1, 2,..., 9, A, B, C, D, E. Budui da za
prikaz brojeva postoji samo 10 brojanih simbola, za prikaz heksadekadskih znamenki
iznad 9 koriste se prva slova engleskog alfabeta, tako da je A16 = 1010, B16 = 1110, itd.
Oktalni i heksadekadski prikaz predstavljaju kompromis izmeu dekadskog prikaza
kojim se koriste ljudi i binarnog sustava kojim se podaci pohranjuju u memoriju
raunala. Naime, binarni prikaz pomou samo dvije znamenke (0 i 1) iziskivao bi puno
prostora u programskom kdu, te bi bio nepregledan. Oktalni i heksadekadski prikazi
iziskuju manje prostora i iskusnom korisniku omoguuju brzu pretvorbu brojeva iz
dekadskog u binarni oblik i obrnuto.
Oktalne konstante se piu tako da se ispred prve znamenke napie broj 0 iza kojeg
slijedi oktalni prikaz broja:
40
Jo jednom uoimo da e bez obzira na brojevni sustav u kojem broj zadajemo, njegova
vrijednost u memoriju biti pohranjena prema predloku koji je odreen deklaracijom
pripadne varijable. To najbolje ilustrira sljedei primjer:
int i = 32;
cout << i << endl;
Bez obzira to smo varijabli i pridruivali broj 32 u tri razliita prikaza, u sva tri sluaja
na zaslonu e se ispisati isti broj, u dekadskom formatu.
Ako je potrebno raunati s decimalnim brojevima, najee se koriste varijable tipa
float:
float pi = 3.141593;
float brzinaSvjetlosti = 2.997925e8;
float nabojElektrona = -1.6E-19;
Praznine unutar broja, na primjer iza predznaka, ili izmeu znamenki i slova
'e' nisu dozvoljene.
41
Prevoditelj e broj
interpretirati samo do etvrte znamenke. Prazninu koja slijedi prevoditelj e shvatiti kao
zavretak broja, pa e po nailasku na znak e javiti pogreku.
Osim to je ogranien raspon vrijednosti koje se mogu prikazati float tipom (vidi
tablicu 4.3), treba znati da je i broj decimalnih znamenki u mantisi takoer ogranien na
7 decimalnih mjesta. ak i ako se napie broj s vie znamenki, prevoditelj e
zanemariti sve nie decimalne znamenke. Stoga e ispis varijabli
dati potpuno isti broj! Usput spomenimo da su broj i ostale znaajnije matematike
konstante definirani u biblioteci math.h, i to na veu tonost, tako da ukljuivanjem te
datoteke moete prikratiti muke traenja njihovih vrijednosti po raznim prirunicima i
srednjokolskim udbenicima.
Ako tonost na sedam decimalnih znamenki ne zadovoljava ili ako se koriste
brojevi vei od 1038 ili manji od 1038, tada se umjesto float mogu koristiti brojevi s
dvostrukom tonou tipa double koji pokrivaju opseg vrijednosti od 1.710308 do
1.710308, ili long double koji pokrivaju jo iri opseg od 3.4104932 do 1.1104932.
Brojevi vei od 1038 vjerojatno e vam zatrebati samo za neke astronomske proraune,
ili ako ete se baviti burzovnim meetarenjem. Realno gledano, brojevi manji od 1038
nee vam gotovo nikada trebati, osim ako elite izraunati vjerojatnost da dobijete
glavni zgoditak na lutriji.
U tablici 4.3 dane su tipine duljine ugraenih brojevnih tipova u bajtovima,
rasponi vrijednosti koji se njima mogu obuhvatiti, a za decimalne brojeve i broj tonih
decimalnih znamenki. Duljina memorijskog prostora i opseg vrijednosti koje odreeni
tip varijable zauzima nisu standardizirani i variraju ovisno o prevoditelju i o platformi
za koju se prevoditelj koristi. Stoga itatelju preporuujemo da za svaki sluaj
konzultira dokumentaciju uz prevoditelj koji koristi. Relevantni podaci za cjelobrojne
tipove mogu se nai u datoteci limits.h, a za decimalne tipove podataka u values.h.
Ako elite sami provjeriti veliinu pojedinog tipa, to moete uiniti i pomou
sizeof operatora:
Broj tonih znamenki ovisi o prevoditelju i o raunalu. U veini sluajeva broj tonih
znamenki i dozvoljene pogreke pri osnovnim aritmetikim operacijama ravnaju se po IEEE
standardu 754 za prikaz brojeva s pominim decimalnim zarezom.
42
Tablica 4.3. Ugraeni brojevni tipovi, njihove tipine duljine i rasponi vrijednosti
char 1
short int 2 32768 do 32767
int 2 32768 do 32767
(4) 2147483648 do 2147483647
long int 4 2147483648 do 2147483647
float 4 3,41038 do 3,41038 i 7 dec. znamenki
3,41038 do 3,41038
double 8 1,710308 do 1,710308 i 15 dec. znamenki
1,710308 do 1,710308
long double 10 1,1104932 do 3,4104932 i 18 dec. znamenki
3,4104932 do 1,1104932
Svi navedeni brojevni tipovi mogu biti deklarirani i bez predznaka, dodavanjem rijei
unsigned ispred njihove deklaracije:
+x unarni plus
-x unarni minus
unarni operatori x++ uveaj nakon
++x uveaj prije
x-- umanji nakon
--x umanji prije
x + y zbrajanje
x - y oduzimanje
binarni operatori x * y mnoenje
x / y dijeljenje
x % y modulo
objekta. Osim unarnog plusa i unarnog minusa koji mijenjaju predznak broja, u jeziku
C++ definirani su jo i unarni operatori za uveavanje (inkrementiranje) i umanjivanje
vrijednosti (dekrementiranje) broja. Operator ++ uveat e vrijednost varijable za 1,
dok e operator --) umanjiti vrijednost varijable za 1:
int i = 0;
++i; // uvea za 1
cout << i << endl; // ispisuje 1
--i; // umanji za 1
cout << i << endl; // ispisuje 0
Pritom valja uoiti razliku izmeu operatora kada je on napisan ispred varijable i
operatora kada je on napisan iza nje. U prvom sluaju (prefiks operator), vrijednost
varijable e se prvo uveati ili umanjiti, a potom e biti dohvaena njena vrijednost. U
drugom sluaju (postfiks operator) je obrnuto: prvo se dohvati vrijednost varijable, a tek
onda slijedi promjena. To najbolje doarava sljedei kd:
44
int i = 1;
cout << i << endl; // ispii za svaki sluaj
cout << (++i) << endl; // prvo povea, pa ispisuje 2
cout << i << endl; // ispisuje opet, za svaki sluaj
cout << (i++) << endl; // prvo ispisuje, a tek onda uvea
cout << i << endl; // vidi, stvarno je uveao!
float a = 2.;
float b = 3.;
cout << (a + b) << endl; // ispisuje 5.
cout << (a - b) << endl; // ispisuje -1.
cout << (a * b) << endl; // ispisuje 6.
cout << (a / b) << endl; // ispisuje 0.666667
Operator modulo kao rezultat vraa ostatak dijeljenja dva cijela broja:
int i = 6;
int j = 4;
cout << (i % j) << endl; // ispisuje 2
On se vrlo esto koristi za ispitivanje djeljivosti cijelih brojeva: ako su brojevi djeljivi,
ostatak nakon dijeljenja e biti nula.
Za razliku od veine matematiki orijentiranih jezika, jezik C++ nema ugraeni
operator za potenciranje, ve programer mora sam pisati funkciju ili koristiti funkciju
pow(). iz standardne matematike biblioteke deklarirane u datoteci zaglavlja math.h.
Valja uoiti dva sutinska problema vezana uz aritmetike operatore. Prvi problem
vezan je uz pojavu numerikog preljeva, kada uslijed neke operacije rezultat nadmai
opseg koji dotini tip objekta pokriva. Drugi problem sadran je u pitanju kakvog e
tipa biti rezultat binarne operacije s dva broja razliitih tipova?.
Razmotrimo prvo pojavu brojanog preljeva. U donjem programu e prvo biti
ispisan broj 32767, to je najvei mogui broj tipa int. Izvoenjem naredbe u treem
retku, na zaslonu raunala e se umjesto oekivanih 32768 ispisati 32768, tj. najvei
negativni int!
int i = 32766;
cout << (++i) << endl;
cout << (++i) << endl;
Uzrok tome je preljev podataka do kojeg dolo zbog toga to oekivani rezultat vie ne
stane u 15 bita predvienih za int varijablu. Podaci koji su se prelili uli su u bit za
predznak (zato je rezultat negativan), a raspored preostalih 15 bitova daje broj koji
odgovara upravo onom to nam je raunalo ispisalo. Slino e se dogoditi oduzmete li
od najveeg negativnog broja neki broj. Ovo se moe ilustrirati brojevnom krunicom
45
-1 0 1
-2 2
oduzimanje zbrajanje
Ako postoji mogunost
-3 pojave numerikog 3preljeva, tada deklarirajte
varijablu s veim opsegom u gornjem primjeru umjesto int uporabite
long int.
int
-32767 32766
-32768 32767
int i = 3;
float a = 0.5;
cout << (a * i) << endl;
uzrokovat e ispis broja 1.5 na zaslonu, jer je od tipova int i float sloeniji tip
float. Cjelobrojnu varijablu i prevoditelj prije mnoenja pretvara u float (prema
pravilu u toki 3), tako da se provodi mnoenje dva broja s pominom tokom, pa je i
rezultat tipa float. Da smo umnoak prije ispisa pridruili nekoj cjelobrojnoj varijabli
int c = a * i;
cout << c << endl;
dobili bismo ispis samo cjelobrojnog dijela rezultata, tj. broj 1. Decimalni dio rezultata
se gubi prilikom pridruivanja umnoka cjelobrojnoj varijabli c.
Problem konverzije brojevnih tipova najjae je izraen kod dijeljenja cijelih
brojeva, to poetnicima (ali i nepaljivim C++ guruima) zna prouzroiti dosta
glavobolje. Pogledajmo sljedei jednostavni primjer:
int Brojnik = 1;
int Nazivnik = 4;
float Kvocijent = Brojnik / Nazivnik;
cout << Kvocijent << endl;
float DiskutabilniKvocijent = 3 / 2;
Brojeve 3 i 2 prevoditelj e shvatiti kao cijele, jer ne sadre decimalnu toku. Zato e
primijeniti cjelobrojni operator /, pa e rezultat toga dijeljenja biti cijeli broj 1.
47
Dovoljno bi bilo decimalnu toku staviti samo uz jedan od operanada prema pravilima
aritmetike konverzije i drugi bi operand bio sveden na float tip. Iako je kd ovako
dulji, izaziva manje nedoumica i stoga svakom poetniku preporuujemo da ne tedi na
decimalnim tokama.
Neupueni poetnik postavit e logino pitanje zato uope postoji razlika izmeu
cjelobrojnog dijeljenja i dijeljenja decimalnih brojeva. Za programera bi bilo
najjednostavnije kada bi se oba operanda bez obzira na tip, prije primjene operatora
svela na float (ili jo bolje, na double), na takve modificirane operande primijenio
eljeni operator, a dobiveni rezultat se pretvorio u zajedniki tip. U tom sluaju
programer uope ne bi trebao paziti kojeg su tipa operandi rezultat bi uvijek bio
korektan (osim ako nemate procesor s pogrekom, to i nije nemogue). Nedostatak
ovakvog pristupa jest njegova neefikasnost. Zamislimo da treba zbrojiti dva broja tipa
int! U gornjem programer-ne-treba-paziti pristupu, izvedbeni kd dobiven nakon
prevoenja trebao bi prvo jedan cjelobrojni operand pretvoriti u float. Budui da se
razliiti tipovi podataka pohranjuju u memoriju raunala na razliite naine, ta pretvorba
nije trivijalna, ve iziskuje odreeni broj strojnih instrukcija. Istu pretvorbu treba
ponoviti za drugi operand. Ve sada uoavamo dvije operacije konverzije tipa koje kod
izravnog zbrajanja cijelih brojeva ne postoje! tovie, samo zbrajanje se provodi na dva
float-a, koji u memoriji zauzimaju vei prostor od int-a. Takvo zbrajanje iziskivat e
puno vie strojnih instrukcija i strojnog vremena, ne samo zbog vee duljine float
brojeva u memoriji, ve i zbog sloenijeg naina na koji su oni pohranjeni. Za mali broj
operacija korisnik zasigurno ne bi osjetio razliku u izvoenju programa s izravno
primijenjenim operatorima i operatorima la programer-ne-treba-paziti. Meutim, u
sloenim programima s nekoliko tisua ili milijuna operacija, ta razlika moe biti
zamjetna, a esto i kritina. U krajnjoj liniji, shvatimo da prevoditelj poput vjernog psa
nastoji to bre ispuniti gospodarevu zapovijed, ne pazei da li e pritom protrati kroz
upravo ureen susjedin cvjetnjak ili zagaziti u svjee betoniran plonik.
Zadatak. to e ispisati sljedee naredbe:
int a = 10;
float b = 10.;
float c = a / 3;
cout << c * b << endl;
c = a / 3.;
cout << c * b << endl;
c = b * a;
cout << c / 3 << endl;
int Brojnik = 1;
int Nazivnik = 3;
float TocniKvocijent = (float)Brojnik / (float)Nazivnik;
int a = 10000;
int b = 10;
49
long c = a * b;
float a = 2.71;
float b = (int)a;
cout << b << endl;
float b = 1.234F;
double pi = 3.14159265359;
float Opseg = 2. * r * pi;
float Povrsina = r * r * pi;
double Oplosje = 4. * r * r * pi;
double Volumen = 4. / 3. * r * r * r * pi;
pi = 2 * pi;
kojom ste promijenili vrijednost varijable pi. Prevoditelj vas nee upozoriti (Opet taj
prokleti kompjutor!) i dobit ete da zemaljska kugla ima dva puta vei volumen nego
to ga je imala proli puta kada ste koristili isti program, ali bez inkriminirane naredbe.
Koristite li program za odreivanje putanje svemirskog broda, ovakva pogreka sigurno
e rezultirati odailjanjem broda u bespua svemirske zbiljnosti.
Da bismo izbjegli ovakve peripetije, na raspolaganju nam je kvalifikator const
kojim se prevoditelju daje na znanje da varijabla mora biti nepromjenjiva. Na svaki
pokuaj promjene vrijednosti takve varijable, prevoditelj e javiti pogreku:
#define PI 3.14159265359
Ona daje uputu prevoditelju da, prije nego to zapone prevoenje izvornog kda, sve
pojave prvog niza (PI) zamijeni drugim nizom znakova (3.14159265359). Stoga e
prevoditelj naredbe
float a;
cin >> a;
const float DvijeTrecine = a;
int izadjiVan = 0;
while (!izadjiVan) {
// ekaj dok se vrijednost izadjiVan ne postavi na 1
}
4.4.7. Pobrojenja
Ponekad su varijable u programu elementi pojmovnih skupova, tako da je takvim
skupovima i njihovim elementima zgodno pridijeliti lakopamtiva imena. Za takve
sluajeve obino se koriste pobrojani tipovi (engl. enumerated types):
Naredbom
ThankGodItIs = petak;
SunnyDay = nedjelja;
Ako nam ne treba vie varijabli tog tipa, ime tipa iza rijei enum moe se izostaviti:
Programski jezik C dozvoljava da se pobrojanom tipu pridrui int vrijednost. Zato, radi
prenosivosti kda pisanog u programskom jeziku C, te kda pisanog u starijim varijantama
jezika C++, ANSI/ISO C++ standard dozvoljava da se u nekim implementacijama prevoditelja
omogui to pridruivanje, uz napomenu da to moe prouzroiti neeljene efekte.
55
Pri ispisu logikih tipova, te pri njihovom koritenju u aritmetikim izrazima, logiki
tipovi se pravilima cjelobrojne promocije (vidi prethodno poglavlje) pretvaraju u int:
true se pretvara u cjelobrojni 1, a false u 0. Isto tako, logikim varijablama se mogu
pridruivati aritmetiki tipovi: u tom sluaju se vrijednosti razliite od nule pretvaraju u
true, a nula se pretvara u false.
Gornja svojstva tipa bool omoguavaju da se za predstavljanje logikih podataka
koriste i cijeli brojevi. Broj 0 u tom sluaju odgovara logikoj neistini, a bilo koji broj
razliit od nule logikoj istini. Iako je za logiku istinu na raspolaganju vrlo iroki
raspon cijelih brojeva (zlobnici bi rekli da ima vie istina), ona se ipak najee zastupa
brojem 1. Ovakvo predstavljanje logikih
podataka je vrlo esto, budui da je tip bool
vrlo kasno uveden u standard jezika C++.
Tablica 4.6. Logiki operatori
Za logike podatke definirana su svega
tri operatora: ! (logika negacija), && !x logika negacija
(logiki i), te || (logiki ili) (tablica 4.6). x && y logiki i
Logika negacija je unarni operator koji x || y logiki ili
mijenja logiku vrijednost varijable: istinu
pretvara u neistinu i obrnuto. Logiki i daje
kao rezultat istinu samo ako su oba operanda istinita; radi boljeg razumijevanja u tablici
4.7 dani su rezultati logikog i za sve mogue vrijednosti oba operanda. Logiki ili daje
istinu ako je bilo koji od operanada istinit (vidi tablicu 4.8).
Razmotrimo primjenu logikih operatora na sljedeem primjeru:
a b a .i. b
tono (1) tono (1) tono (1)
tono (1) pogreno (0) pogreno (0)
pogreno (0) tono (1) pogreno (0)
pogreno
Rije bool dolazi (0)
od prezimena pogreno
engleskog (0) pogreno
matematiara Georgea(0)Boolea (18151864),
utemeljitelja logike algebre.
56
a b a .ili. b
tono (1) tono (1) tono (1)
tono (1) pogreno (0) tono (1)
pogreno (0) tono (1) tono (1)
pogreno (0) pogreno (0) pogreno (0)
Logicki b = ISTINA;
Logicki c = DRUGAISTINA;
x < y manje od
x <= y manje ili jednako
x > y vee od
x >= y vee ili jednako
x == y jednako
x != y razliito
int a = 3;
a == 5;
a = 1.23 * (b == c + 5);
to upozorenje e izostati, jer ovdje poredbeni operator moe imati smisla. Za poetnike
jedno od nezgodnih svojstava jezika C++ jest da trpi i besmislene izraze, poput:
i + 5;
ili
58
i == 5 == 6;
4.4.10. Znakovi
Znakovne konstante tipa char piu se uglavnom kao samo jedan znak unutar
jednostrukih znakova navodnika:
\n novi redak
\t horizontalni tabulator
\v vertikalni tabulator
\b pomak za mjesto unazad (backspace)
\r povrat na poetak retka (carriage return)
\f nova stranica (form feed)
\a zvuni signal (alert)
\\ kosa crta ulijevo (backslash)
\? upitnik
\' jednostruki navodnik
\" dvostruki navodnik
\0 zavretak znakovnog niza
\ddd znak iji je kd zadan oktalno s 1, 2 ili 3 znamenke
\xddd znak iji je kd zadan heksadekadski
b = a + 1;
cout << b << endl; // ispisuje 'i'
Kod prvog ispisa znakovna varijabla a (koja ima vrijednost slova h) se, zbog operacije
sa cijelim brojem 1, pretvara se u ASCII kd za slovo h, tj. u broj 104, dodaje joj se 1, te
ispisuje broj 105. Druga naredba za ispis daje slovo i, jer se broj 105 pridruuje
znakovnoj varijabli b, te se 105 ispisuje kao ASCII znak, a ne kao cijeli broj.
Zadatak. Razmislite i provjerite to e se ispisati izvoenjem sljedeeg kda:
char a = 'a';
char b = a;
int asciiKod = a;
cout << a << endl;
cout << b << endl;
60
Za pohranjivanje znakova koji ne stanu u tip char moe se koristiti tip wchar_t.
Duljina tipa wchar_t nije definirana standardom; na prevoditelju koji smo koristili pri
pisanju knjige ona je iznosila dva bajta, tako da smo u jedan znak tipa wchar_t mogli
smjestiti dva znaka:
Slovo L ispred pridruene eksplicitno odreuje da je znak koji slijedi tipa wchar_t.
B7 B6 B5 B4 B3 B2 B1 B0
0 0 1 0 0 1 0 1
~00100101 = 11011010
B7 B6 B5 B4 B3 B2 B1 B0
1 1 0 1 1 0 1 0
0 0 1 0 0 1 0 1
B7 B6 B5 B4 B3 B2 B1 B0
0 0 0 0 0 1 0 1
Bitovni i se najee koristi kada se ele pojedini bitovi broja postaviti u 0 (obrisati) ili
ako se ele izdvojiti samo neki bitovi broja. Drugi operand je pritom maska koja
odreuje koji e se bitovi postaviti u nulu, odnosno koji e se bitovi izluiti. Za
postavljanje odreenih bitova u nulu svi bitovi maske na tim mjestima moraju biti
jednaki 0. Ako u gornjem primjeru uzmemo da je maska donji operand, tj. 000011112,
tada moemo rei da smo kao rezultat dobili gornji broj u kojem su etiri najznaajnija
(tj. lijeva) bita obrisana. etiri preostala bita su ostala nepromijenjena, jer maska na tim
mjestima ima jedinice, iz ega odmah slijedi zakljuak da u maski za izluivanje svi
62
bitovi koje elimo izluiti iz prvog broja moraju biti jednaki 1. Ako u gornjem primjeru
uzmemo da je maska donji operand, kao rezultat smo dobili stanja etiri najnia bita
gornjeg operanda. Zbog svojstva komutativnosti moemo rei i da je prvi broj maska
pomou koje briemo, odnosno vadimo odreene bitove drugog broja. Izvorni kd za
gornju operaciju mogli bismo napisati kao:
int a = 0x0025;
int maska = 0x000f;
0 0 1 0 0 1 0 1
B7 B6 B5 B4 B3 B2 B1 B0
0 0 1 0 1 1 1 1
int a = 0x0025;
int maska = 0x000f;
0 0 1 0 0 1 0 1
B7 B6 B5 B4 B3 B2 B1 B0
0 0 1 0 1 0 1 0
int a = 0x0025;
int maska = 0x000f;
cout << hex << (a ^ maska) << endl; // ispisuje 0x2a
Ova injenica esto se koristi za jednostavnu zatitu podataka ili kda od nepoeljnog
itanja, odnosno koritenja: primjenom iskljuivog ili na podatke pomou tajne maske
podaci se ifriraju. Ponovnom primjenom iskljuivog ili s istom maskom podaci se
vraaju u izvorni oblik.
Operatori << (pomak ulijevo, engl. shift left) i >> (pomak udesno, engl. shift right)
pomiu sve bitove lijevog operanda za broj mjesta odreen desnim operandom, kako je
prikazano na slici 4.8.
Pri pomaku ulijevo najznaajniji (tj. krajnji lijevi) bitovi se gube, dok najmanje
znaajni bitovi poprimaju vrijednost 0. Slino, pri pomaku udesno, gube se najmanje
znaajni bitovi, a najznaajniji bitovi poprimaju vrijednost 0. Uoimo da pomak bitova
u cijelim brojevima za jedno mjesto ulijevo odgovara mnoenju broja s 2 situacija je
potpuno identina dodavanju nula iza dekadskog broja, samo to je kod dekadskog broja
baza 10, a u binarnoj notaciji je baza 2. Slino, pomak bitova za jedno mjesto udesno
odgovara dijeljenju cijelog broja s 2:
int a = 20;
cout << (a << 1) << endl; // pomak za 1 bit ulijevo -
// ispisuje 40
cout << (a >> 2) << endl; // pomak za 2 bita udesno -
// ispisuje 5
64
0 0 1 0 0 1 0 1
B7 B6 B5 B4 B3 B2 B1 B0
0 0 1 0 0 1 0 1
B7 B6 B5 B4 B3 B2 B1 B0
0 0 0 1 0 0 1 0
B7 B6 B5 B4 B3 B2 B1 B0
0 1 0 0 1 0 1 0
B7 B6 B5 B4 B3 B2 B1 B0
0 0 0 0 1 0 0 1
Meutim, pri koritenju pomaka valja biti krajnje oprezan, jer se kod cjelobrojnih
konstanti s predznakom najvii bit koristi za predznak. Ako je broj negativan, tada se
pomakom ulijevo bit predznaka e se izgubiti, a na njegovo mjesto e doi najvia
binarna znamenka. Kod pomaka udesno bit predznaka ulazi na mjesto najvie
znamenke. Istina, neki prevoditelji vode rauna o bitu predznaka, ali se zbog
prenosivosti kda ne treba previe oslanjati na to.
Mogunost izravnog pristupa pojedinim bitovima esto se koristi za stvaranje
skupova logikih varijabli pohranjenih u jednom cijelom broju, pri emu pojedini bitovi
tog broja predstavljaju zasebne logike varijable. Takvim pristupom se tedi na
memoriji, jer umjesto da svaka logika varijabla zauzima zasebne bajtove, u jednom je
bajtu pohranjeno osam logikih varijabli. Ilustrirajmo to primjerom u kojem se
definiraju parametri za prijenos podataka preko serijskog prikljuka na raunalu. Radi
lakeg praenja, izvorni kd emo ralaniti na segmente, koje emo analizirati zasebno.
Za serijsku komunikaciju treba, osim brzine prijenosa izraene u baudima,
definirati broj bitova po podatku (7 ili 8), broj stop-bitova (1 ili 2), te paritet (parni
paritet, neparni paritet ili bez pariteta). Uzmimo da na raspolaganju imamo osam brzina
prijenosa: 110, 150, 300, 600, 1200, 2400, 4800 i 9600 bauda. Svakoj toj brzini pridruit
emo po jedan broj u nizu od 0 do 7:
Broj stop-bitova pohranit emo u B4 i B5. Ako se trai jedan stop-bit, tada e B4 biti 1,
a za dva stop-bita B5 je jednak 1:
Konano, podatke o paritetu emo pohraniti u dva najvia bita (B6 - B7):
B7 B6 B5 B4 B3 B2 B1 B0
0 0 0 0 0 1 0 0 Baud1200 |
B7 B6 B5 B4 B3 B2 B1 B0
0 0 0 0 1 0 0 0 Bitova8 |
B7 B6 B5 B4 B3 B2 B1 B0
0 0 1 0 0 0 0 0 StopB2 |
B7 B6 B5 B4 B3 B2 B1 B0
0 0 0 0 0 0 0 0 ParitetNjet
bez64748
pariteta 8 bitova
678
podataka
B7 B6 B5 B4 B3 B2 B1 B0
0 0 1 0 1 1 0 0
14243 1442443
2 stop-bita 1200 bauda
B7 B6 B5 B4 B3 B2 B1 B0
0 0 0 0 0 1 1 1 00000111 &
B7 B6 B5 B4 B3 B2 B1 B0
0 0 1 0 1 1 0 0 SerCom
B7 B6 B5 B4 B3 B2 B1 B0
0 0 0 0 0 1 0 0 00000100
Prenosi li se osam bitova podataka provjerit emo sljedeom bitovnom ili operacijom:
budui da 816 ima u binarnom prikazu 1 samo na mjestu B3. Ako je bit B3 postavljen,
tj. ako ima vrijednost 1, varijabla JeLiOsamBita e poprimiti vrijednost dekadsko (i
heksadekadsko!) 8; u protivnom e biti 0.
Analogno, za izluivanje broja stop-bitova posluit emo se maskom 3016 koja na
mjestima B4 i B5 ima 1:
U naem primjeru kao rezultat emo dobiti 32. Ako rezultantu bitovnu strukturu
pomaknemo udesno za 4 mjesta tako da B4 i B5 dou na B0 odnosno B1 (slika 4.11):
1 2 3 4
B7 B6 B5 B4 B3 B2 B1 B0
0 0 1 0 0 0 0 0 00100000 >> 4
144424443
644474448
B7 B6 B5 B4 B3 B2 B1 B0
0 0 0 0 0 0 1 0 00000010
a += 5;
ekvivalentna je naredbi
a = a + 5;
int n = 10;
n += 5; // isto kao: n = n + 5
cout << n << endl; // ispisuje: 15
n -= 20; // isto kao: n = n - 20
cout << n << endl; // ispisuje: -5
n *= -2; // isto kao: n = n * (-2)
cout << n << endl; // ispisuje: 10
n %= 3; // isto kao: n = n % 3
cout << n << endl; // ispisuje 1
a = a - b - c;
a -= b - c; // to je zapravo a = a - (b - c)
ve kao
a -= b + c; // a = a - (b + c)
int main()
int a = 5;
char b = '0';
int c = a b;
return 0;
namjenu je u jeziku C++ dozvoljena i upotreba nizova od tri znaka (engl. trigraph),
naslijeenih iz programskog jezika C (tablica 4.14). Sreom, operacijski sustavi koji za
predstavljanje znakova koriste neki 8-bitni standard (npr. kodna stranica 852 pod DOS-
om ili kodna stranica 1250 pod Windows-ima) omoguavaju istovremeno koritenje
nacionalnih znakova i specijalnih znakova neophodnih za pisanje programa u jeziku
C++, tako da veini korisnika tablice 4.13 i 4.14 nee nikada zatrebati. Ipak za
ilustraciju pogledajmo kako bi izgledao neki program napisan pomou alternativnih
oznaka (itatelju preputamo da deifrira kd):
%:include <iostream.h>
broj pi = 3.14159;
Budui da deklaracija typedef ne uvodi novi tip podataka, niti mijenja standardna
pravila pretvorbe podataka, sljedea pridruivanja su dozvoljena i nee prouzroiti
nikakve promjene u tonosti:
float a = pi;
typedef float pliva;
pliva = pi;
Kada je broj objekata mali to i nije teko, ali za veliki broj deklaracija te zamjene mogu
biti naporne i podlone pogrekama. Posao emo si bitno olakati, ako u gornjem
primjeru dodamo deklaraciju typedef za podatke iji tip emo povremeno mijenjati:
Sada e svi podaci tipa brojevi biti prevedeni kao double podaci, bez potrebe
daljnjih izmjena u izvornom kdu.
71
Valja naglasiti da standard jezika C++ ne definira veliinu bajta, osim u smislu rezultata
to ga daje sizeof operator; tako je sizeof(char) jednak 1. Naime, duljina bajta
(broj bitova koji ine bajt) ovisi o arhitekturi raunala. Mi emo u knjizi uglavnom
podrazumijevati da bajt sadri 8 bitova, to je najei sluaj u praksi.
Operand sizeof operatora moe biti identifikator tipa (npr. int, float, char) ili
konkretni objekt koji je ve deklariran:
float f;
int i;
cout << sizeof(f) << endl; // duljina float
cout << sizeof(i) << endl; // duljina int
float f;
int i;
cout << sizeof(f * i) << endl; // duljina float
cout << sizeof((int)(i * f)) << endl; // duljina int
Rezultat operatora sizeof je tipa size_t, cjelobrojni tip bez predznaka koji ovisi
o implementaciji prevoditelja, definiran u zaglavlju stddef.h (o zaglavljima e biti
rijei u kasnijim poglavljima).
Operator sizeof se uglavnom koristi kod dinamikog alociranja memorijskog
prostora kada treba izraunati koliko memorije treba osigurati za neki objekt, o emu e
biti govora u kasnije u knjizi.
i = 10, i + 5;
y = a + b * c / d;
c = a - --b;
za to napie naredbu
c = a---b;
to e stvarno ta naredba uraditi pouzdano emo saznati ako ispiemo vrijednosti svih
triju varijabli nakon naredbe:
int main() {
int a = 2;
int b = 5;
int c;
c = a---b;
cout << "a = " << a << ", b = " << b
<< ", c = " << c << endl;
return 1;
}
a = 1, b = 5, c = -3
c = a-- - b;
tj. uzeo je vrijednost varijable a, od nje oduzeo broj b te rezultat pridruio varijabli c, a
varijablu a je umanjio (ali tek nakon to je upotrijebio njenu vrijednost).
Kao i u matematici, okruglim zagradama se moe zaobii ugraena hijerarhija
operatora, budui da one imaju vii prioritet od svih operatora. Tako e se u kdu
d = a * (b + c);
c = a - (--b);
75
U veini realnih problema tok programa nije pravocrtan i jedinstven pri svakom
izvoenju. Redovito postoji potreba da se pojedini odsjeci programa ponavljaju
programski odsjeci koji se ponavljaju nazivaju se petljama (engl. loops). Ponavljanje
moe biti unaprijed odreeni broj puta, primjerice elimo li izraunati umnoak svih
cijelih brojeva od 1 do 100. Meutim, broj ponavljanja moe ovisiti i o rezultatu neke
operacije. Kao primjer za to uzmimo uitavanje podataka iz neke datoteke datoteka se
uitava podatak po podataka, sve dok se ne uita znak koji oznaava kraj datoteke (end-
of-file). Duljina datoteke pritom moe varirati za pojedina izvoenja programa.
Gotovo uvijek se javlja potreba za grananjem toka, tako da se ovisno o
postavljenom uvjetu u jednom sluaju izvodi jedan dio programa, a u drugom sluaju
drugi dio. Na primjer, elimo izraunati realne korijene kvadratne jednadbe. Prvo emo
izraunati diskriminantu ako je diskriminanta vea od nule, izraunat emo oba
korijena, ako je jednaka nuli izraunat emo jedini korijen, a ako je negativna ispisat
emo poruku da jednadba nema realnih korijena. Grananja toka i ponavljanja dijelova
kda omoguavaju posebne naredbe za kontrolu toka.
#include <iostream.h>
int main() {
{ // poetak bloka naredbi
77
78
proi uredno, ali ako naredbu za ispis lokalne varijable a prebacimo izvan bloka
#include <iostream.h>
int main() {
{
int a = 1;
}
cout << a << endl; // greka, jer a vie ne postoji!
return 0;
}
#include <iostream.h>
int main() {
int a = 5;
{
int a = 1;
cout << a << endl; // ispisuje 1
}
cout << a << endl; // ispisuje 5
return 0;
}
Prva naredba za ispis dohvatit e lokalnu varijablu a = 1, budui da ona ima prednost
pred istoimenom varijablom a = 5 koja je deklarirana ispred bloka. Po izlasku iz bloka,
lokalna varijabla a se gubi te je opet dostupna samo varijabla a = 5. Naravno da bi
ponovna deklaracija istoimene varijable bilo u vanjskom, bilo u unutarnjem bloku
rezultirala pogrekom tijekom prevoenja. Podrujem dosega varijable pozabavit emo
se detaljnije u kasnijim poglavljima.
Ako se blok u naredbama za kontrolu toka sastoji samo od jedne naredbe, tada se
vitiaste zagrade mogu i izostaviti.
79
if ( logiki_izraz )
// blok_naredbi
Ako je vrijednost izraza iza rijei if logika istina (tj. bilo koji broj razliit od nule),
izvodi se blok naredbi koje slijede iza izraza. U protivnom se taj blok preskae i
izvoenje nastavlja od prve naredbe iza bloka. Na primjer:
if (a < 0) {
cout << "Broj a je negativan!" << endl;
}
U sluaju da blok sadri samo jednu naredbu, vitiaste zagrade koje omeuju blok mogu
se i izostaviti, pa smo gornji primjer mogli pisati i kao:
if (a < 0)
cout << "Broj a je negativan!" << endl;
ili
if (a < 0)
cout << "Broj a je negativan!" << endl;
cout << "Njegova apsolutna vrijednost je " << -a
<< endl;
Druga naredba ispod if uvjeta izvest e se i za pozitivne brojeve, jer vie nije u bloku,
pa e se za pozitivne brojeve ispisati pogrena apsolutna vrijednost! Inae, u primjerima
emo izbjegavati pisanje vitiastih zagrada gdje god je to mogue radi utede na
prostoru.
80
if ( logiki_izraz )
// prvi_blok_naredbi
else
// drugi_blok_naredbi
Kod ovog oblika, ako izraz u if uvjetu daje rezultat razliit od nule, izvest e se prvi
blok naredbi. Po zavretku bloka izvoenje programa nastavlja se od prve naredbe iza
drugog bloka. Ako izraz daje kao rezultat nulu, preskae se prvi blok, a izvodi samo
drugi blok naredbi, nakon ega se nastavlja izvoenje naredbi koje slijede. Evo
jednostavnog primjera u kojem se raunaju presjecita pravca s koordinatnim osima.
Pravac je zadan jednadbom a x + b y + c = 0.
#include <iostream.h>
int main() {
float a, b, c; // koeficijenti pravca
cin >> a >> b >> c; // uitaj koeficijente
cout << "Koeficijenti: " << a << ","
<< b << "," << c << endl; // ispii ih
return 0;
}
if ( logiki_izraz1 )
// prvi_blok_naredbi
else if ( logiki_izraz2 )
// drugi_blok_naredbi
else if ( logiki_izraz3 )
// trei_blok_naredbi
.
81
else
// zadnji_blok_naredbi
#include <iostream.h>
int main() {
float a, b, c;
return 0;
}
#include <iostream.h>
int main() {
float a, b, c;
return 0;
}
Za logiki izraz u prvom if uvjetu postavili smo samo vrijednost varijable a ako e
ona biti razliita od nule, uvjet e biti zadovoljen i izvest e se naredbe u prvom if
bloku. Taj blok sastoji se od niza if-else blokova identinih onima iz prethodnog
primjera. Ako poetni uvjet nije zadovoljen, tj. ako varijabla a ima vrijednost 0,
preskae se cijeli prvi blok i ispisuje poruka da je jednadba linearna. Uoimo u
gornjem primjeru dodatno uvlaenje ugnijeenih blokova.
Pri definiranju logikog izraza u naredbama za kontrolu toka poetnik treba biti
oprezan. Na primjer, elimo li da se dio programa izvodi samo za odreeni opseg
vrijednosti varijable b, naredba
nee raditi onako kako bi se prema ustaljenim matematikim pravilima oekivalo. Ovaj
logiki izraz u biti se sastoji od dvije usporedbe: prvo se ispituje je li 10 manji od b, a
potom se rezultat te usporedbe usporeuje s 0, tj.
Kako rezultat prve usporedbe moe biti samo true ili false, odnosno 1 ili 0, druga
usporedba dat e uvijek logiku neistinu. Da bi program poprimio eljeni tok,
usporedbe moramo razdvojiti i logiki izraz formulirati ovako: ako je 10 manji od b i
ako je b manji od 0:
Druga nezgoda koja se moe dogoditi jest da se umjesto operatora za usporedbu ==, u
logikom izrazu napie operator pridruivanja =. Na primjer:
if (k = 0) // pridruivanje, a ne usporedba!!!
k++;
else
k = 0;
Ako izraz uvjet daje logiku istinu, izraunava se izraz1, a u protivnom izraz2. U
primjeru
x = (x < 0) ? -x : x; // x = abs(x)
((diskr = b * b -4 * a * c) >= 0) ?
(x1 = (-b + diskr) / 2 / a, x2 = (-b + diskr) / 2 / a) :
(cout << "Ne valja ti korijen!", x1 = x2 = 0);
84
switch ( cjelobrojni_izraz ) {
case konstantan_izraz1 :
// prvi_blok_naredbi
case konstantan_izraz2 :
// drugi_blok_naredbi
break;
case konstantan_izraz3 :
case konstantan_izraz4 :
// trei_blok_naredbi
break;
default:
// etvrti_blok_naredbi
}
#include <iostream.h>
int main() {
cout << "Upii datum u formatu DD MM GGGG:";
int dan, mjesec;
long int godina;
cin >> dan >> mjesec >> godina;
long datum;
if (mjesec < 3) {
datum = 365 * godina + dan + 31 * (mjesec - 1)
+ (godina - 1) / 4
- 3 * ((godina - 1) / 100 + 1) / 4;
}
else {
// uoimo operator dodjele tipa (int):
datum = 365 * godina + dan + 31 * (mjesec - 1)
- (int)(0.4 * mjesec + 2.3) + godina / 4
- 3 * (godina / 100 + 1) / 4;
}
cout << dan << "." << mjesec << "." << godina
<< ". pada u ";
switch (datum % 7) {
case 0:
cout << "subotu." << endl;
break;
case 1:
cout << "nedjelju." << endl;
break;
case 2:
cout << "ponedjeljak." << endl;
break;
case 3:
cout << "utorak." << endl;
break;
case 4:
cout << "srijedu." << endl;
break;
case 5:
cout << "etvrtak." << endl;
break;
default:
cout << "petak." << endl;
}
return 0;
}
mnoi s 365. U protivnom bi vrlo vjerojatno dolo do brojanog preljeva, osim ako
bismo se ograniili na datume iz ivota Kristovih suvremenika. Uoimo u gornjem kdu
operator dodjele tipa (int)
n! = 1 2 (n 2) (n 1) n
Pogledajmo kd:
87
#include <iostream.h>
int main() {
int n;
cout << "Upii prirodni broj: "; // manji od 13!
cin >> n;
Prije ulaska u petlju trebamo definirati poetnu vrijednost varijable fjel u koju emo
gomilati umnoke. Na ulasku u petlju deklariramo broja petlje, varijablu i tipa int te
joj pridruujemo poetnu vrijednost 2 (poetni_izraz: int i = 2). Unutar same
petlje mnoimo fjel s brojaem petlje, a na kraju tijela petlje uveavamo broja
(izraz_prirasta: i++). Petlju ponavljamo sve dok je broja manji ili jednak
unesenom broju (uvjet_izvoenja: i <= n). Pri testiranju programa pazite da
uneseni broj ne smije biti vei od 12, jer e inae doi do brojanog preljeva varijable
fjel.
Zanimljivo je uoiti to e se dogoditi ako za n unesemo brojeve manje od 2. Ve
pri prvom ulasku u petlju nee biti zadovoljen uvjet ponavljanja petlje te e odmah biti
preskoeno tijelo petlje i ona se nee izvesti niti jednom! Ovo nam odgovara, jer je 1! =
1 i (po definiciji) 0! = 1. Naravno da smo petlju mogli napisati i na sljedei nain:
Rezultat bi bio isti. Iskusni programer bi gornji program mogao napisati jo saetije
(vidi poglavlje 5.11), no u ovom trenutku to nam nije bitno.
Moe se dogoditi da je uvjet izvoenja uvijek zadovoljen, pa e se petlja izvesti
neogranieni broj puta. Program e uletjeti u slijepu ulicu iz koje nema izlaska, osim
pomou tipki Power ili Reset na kuitu vaeg raunala. Na primjer:
Varijabla i je uvijek vea od 1, tako da ako se i usudite pokrenuti ovaj program, ekran
e vam vrlo brzo biti preplavljen slovom a. Iako naizgled banalne, ovakve pogreke
mogu poetniku zadati velike glavobolje.
88
Naredba unutar petlje potpuno potire izraz prirasta, te varijabla i alternira izmeu 1 i
0.
Poetni izraz i izraz prirasta mogu se sastojati i od vie izraza odvojenih operatorom
nabrajanja , (zarez) . To nam omoguava da program za raunanje faktorijela napiemo
i (neto) krae:
#include <iostream.h>
int main() {
int n;
cout << "Upii prirodni broj: ";
cin >> n;
long fjel;
int i;
for (i = 2, fjel = 1; i <= n; fjel *= i, i++) ;
je od bloka ostala prazna naredba, tj. sam znak ;. Njega ne smijemo izostaviti, jer bi
inae prevoditelj prvu sljedeu naredbu (a to je naredba za ispis rezultata) obuhvatio u
petlju. Sada je i deklaracija brojaa prebaena ispred for naredbe, jer poetni izraz ne
trpi viestruke deklaracije. Da smo for naredbu napisali kao:
#include <iostream.h>
int main() {
int i = -1;
for (int i = 1; i <= 10; i++)
cout << i << endl;
cout << i << endl; // ispisuje -1
return 0;
}
for petlje mogu biti ugnijeene jedna unutar druge. Ponaanje takvih petlji razmotrit
emo na sljedeem programu:
#include <iostream.h>
#include <iomanip.h>
int main() {
for (int redak = 1; redak <= 10; redak++) {
for (int stupac = 1; stupac <= 10; stupac++)
cout << setw(5) << redak * stupac;
cout << endl;
}
return 0;
}
1 2 3 4 5 6 7 8 9 10
2 4 6 8 10 12 14 16 18 20
3 6 9 12 15 18 21 24 27 30
4 8 12 16 20 24 28 32 36 40
5 10 15 20 25 30 35 40 45 50
Tijekom razvoja standarda to pravilo se mijenjalo. Tako e neki stariji prevoditelji ostaviti
varijablu i ivom i iza for petlje.
90
6 12 18 24 30 36 42 48 54 60
7 14 21 28 35 42 49 56 63 70
8 16 24 32 40 48 56 64 72 80
9 18 27 36 45 54 63 72 81 90
10 20 30 40 50 60 70 80 90 100
Kako se gornji program izvodi? Pri ulasku u vanjsku petlju, inicijalizira se broja redaka
na vrijednost 1 te se s njom ulazi u unutarnju petlju. U unutarnjoj petlji se broja stupaca
mijenja od 1 do 10 i za svaku pojedinu vrijednost izraunava se njegov umnoak s
brojaem redaka (potonji je cijelo to vrijeme redak = 1). Po zavrenoj unutarnjoj petlji
ispisuje se znak za novi redak, ime zavrava blok naredbi vanjske petlje. Slijedi prirast
brojaa redaka (redak = 2) i povrat na poetak vanjske petlje. Unutarnja petlja se
ponavlja od stupac = 1 do stupac = 10 itd. Kao to vidimo, za svaku vrijednost
brojaa vanjske petlje izvodi se cjelokupna unutarnja petlja.
Da bismo dobili ispis brojeva u pravilnim stupcima, u gornjem primjeru smo rabili
operator za rukovanje (manipulator) setw(). Argument tog manipulatora (tj. cijeli broj
u zagradi) odreuje koliki e se najmanji prostor predvidjeti za ispis podatka koji slijedi
u izlaznom toku. Ako je podatak krai od predvienog prostora, preostala mjesta bit e
popunjena prazninama. Manipulator setw() definiran je u datoteci zaglavlja
iomanip.h.
while ( uvjet_izvoenja )
// blok_naredbi
#include <iostream.h>
#include <fstream.h>
91
int main() {
ifstream ulazniTok("brojevi.dat");
cout << "Sadraj datoteke:" << endl << endl;
float broj;
while ((ulazniTok >> broj) != 0)
cout << broj << endl;
return 0;
}
uitava podatak. Ako je uitavanje bilo uspjeno, ulTok e biti razliit od nule
(neovisno o vrijednosti uitanog broja), te se izvodi blok naredbi u while-petlji
ispisuje se broj na izlaznom toku cout. Nakon to se izvede naredba iz bloka, izvoenje
se vraa na ispitivanje uvjeta petlje. Ako ulTok poprimi vrijednost 0, to znai da nema
vie brojeva u datoteci, petlja se prekida. Blok petlje se preskae, te se izvoenje
nastavlja prvom naredbom iza bloka.
Primijetimo zgodno svojstvo petlje while: ako je datoteka prazna, ulazni tok e
odmah poprimiti vrijednost 0 te e uvjet odmah prilikom prvog testiranja biti
neispunjen. Blok petlje se tada nee niti jednom izvesti, to u naem sluaju i trebamo.
Zadatak. Dopunite gornji program tako da nakon sadraja datoteke ispie i broj
iitanih brojeva i njihovu srednju vrijednost.
Zanimljivo je uoiti da nema sutinske razlike izmeu for- i while-bloka naredbi
svaki for-blok se uz neznatne preinake moe napisati kao while-blok i obrnuto. Da
bismo se u to osvjedoili, napisat emo program za raunanje faktorijela iz prethodnog
poglavlja koritenjem while naredbe:
#include <iostream.h>
int main() {
int n;
92
Trebalo je samo poetni izraz (int i = 2) izluiti ispred while naredbe, a izraz prirasta
(i++) prebaciti iza bloka naredbi.
Zadatak. Program za ispis datoteke napiite tako da umjesto while-bloka upotrijebite
for-blok naredbi.
Koji e se pristup koristiti (for-blok ili while-blok) prvenstveno ovisi o
sklonostima programera. Ipak, zbog preglednosti i razumljivosti kda for-blok je
preporuljivo koristiti kada se broj ponavljanja petlje kontrolira cjelobrojnim brojaem.
U protivnom, kada je uvjet ponavljanja odreen nekim logikim uvjetom, praktinije je
koristiti while naredbu.
do
// blok_naredbi
while ( uvjet_ponavljanja );
#include <iostream.h>
#include <stdlib.h>
int main() {
int raspon = 100;
randomize();// inicijalizira generator slu. br.
// generira sluajni broj izmeu 1 i raspon
93
do {
cout << ++brojPokusa << ". pokuaj: ";
cin >> mojBroj;
if (mojBroj > trazeniBroj)
cout << "MANJE!" << endl;
else if (mojBroj < trazeniBroj)
cout << "VIE!" << endl;
} while(mojBroj != trazeniBroj);
Petlju za raunanje lanova reda treba ponavljati sve dok je apsolutna vrijednost
zadnjeg izraunatog lana reda vea od nekog zadanog broja (npr. 107).
izvoenje naeg program za ispis brojeva iz datoteke brojevi.dat (na str. 90) prekine
kada se uita 0. Tada emo while-petlju modificirati na sljedei nain:
// ...
while ((ulazniTok >> broj) != 0) {
if (broj == 0)
break; // prekida petlju uitavanja
cout << broj << endl;
}
// ...
// ...
while ((znak = fgetc(ulazniTok)) != EOF) {
if (znak < ' ' && znak != '\r')
continue; // preskae naredbe do kraja petlje
cout << znak;
}
// ...
Nailaskom na znak iji je kd manji od kda za prazninu i nije jednak znaku za novi
redak, izvodi se naredba continue preskau se sve naredbe do kraja petlje (u ovom
sluaju je to naredba za ispis znaka), ali se ponavljanje petlje dalje nastavlja kao da se
nita nije dogodilo.
Ako je vie petlji ugnijeeno jedna unutar druge, naredba break ili naredba
continue prouzroit e prekid ponavljanja, odnosno nastavak okolne petlje u kojoj se
naredba nalazi.
goto ime_oznake ;
ime_oznake je simboliki naziv koji se mora nalaziti ispred naredbe na koju se eli
prenijeti kontrola, odvojen znakom : (dvotoka). Na primjer
if (a < 0)
goto negativniBroj;
//...
negativniBroj: //naredba
Naredba na koju se eli skoiti moe se nalaziti bilo gdje (ispred ili iza naredbe goto)
unutar iste funkcije. ime_oznake mora biti jedinstveno unutar funkcije, ali moe biti
jednako imenu nekog objekta ili funkcije. Ime oznake jest identifikator, pa vrijede
pravila navedena u poglavlju 1.
Zadnju naredbu za kontrolu toka koju emo ovdje spomenuti upoznali smo na samom
poetku knjige. To je naredba return kojom se prekida izvoenje funkcija te emo se
njome pozabaviti u poglavlju posveenom funkcijama.
for ( /*...*/ )
{
// blok naredbi
// ...
}
96
for ( /*...*/ )
{ // blok naredbi
// ...
}
for ( /*...*/ )
{ // blok naredbi
// ...
}
for ( /*...*/ ) {
// blok naredbi
// ...
}
ili
Koji ete pristup preuzeti ovisi iskljuivo o vaoj odluci, ali ga onda svakako
koristite dosljedno.
a=b*c-d/(2.31+e)+e/8.21e-12*2.43;
i = i + 1;
i++;
a = b + 25.6;
c = e * a - 12;
c = e * (a = b + 25.6) - 12;
s potpuno istim efektom. Ovakvo saimanje kda ne samo da zauzima manje prostora u
editoru i izvornom kdu, ve esto olakava prevoditelju generiranje kraeg i breg
98
c = 4 * (a = b - 5);
c = 4 * ((a = b) - 5);
a = b;
c = 4 * (b - 5);
if (a != 0) {
// ...
}
if (a) {
// ...
}
Iako e oba kda raditi potpuno jednako, sutinski gledano je prvi pristup ispravniji (i
itljiviji) izraz u if ispitivanju po definiciji mora davati logiki rezultat (tipa bool). U
drugom (saetom) primjeru se, sukladno ugraenim pravilima konverzije, aritmetiki tip
pretvara u tip bool (tako da se brojevi razliiti od nule pretvaraju u true, a nula se
pretvara u false), pa je konani ishod isti.
Slina situacija je prilikom ispitivanja da li je neka varijabla jednaka nuli. U
dosljednom pisanju ispitivanje bismo pisali kao:
if (a == 0) {
// ...
}
99
if (!a) {
// ...
}
I u ovom sluaju e oba ispitivanja poluiti isti izvedbeni kd, ali e neiskusnom
programeru iitavanje drugog kda biti zasigurno tee. tovie, neki prevoditelji e
prilikom prevoenja ovako saetih ispitivanja ispisati upozorenja.
Mogunost saimanja izvornog kda (s eventualnim popratnim zamkama) ilustrirat
emo programom u kojem traimo zbroj svih cijelih brojeva od 1 do 100. Budui da se
radi o trivijalnom raunu, izvedbeni program e i na najsporijim suvremenim strojevima
rezultat izbaciti za manje od sekunde. Stoga neemo koristiti Gaussov algoritam za
rjeenje problema, ve emo (zdravo-seljaki) napraviti petlju koja e zbrojiti sve
brojeve unutar zadanog intervala. Krenimo s prvom verzijom:
100
// ver. 1.0
#include <iostream.h>
int main() {
int zbroj = 0;
for(int i = 1; i <= 100; i++)
zbroj = zbroj + i;
cout << zbroj << endl;
return 0;
}
// ver. 2.0
//...
for(int i = 1; i <= 100; i++) {
zbroj += i;
//...
// ver. 3.0
//...
for (int i = 1; i <= 100; zbroj += i, i++) ;
//...
Blok naredbi u petlji je sada ostao prazan, ali ne smijemo izbaciti znak ;. U
dosadanjim realizacijama mogli smo broja petlje i poveavati i prefiks-operatorom:
// ver. 3.1
//...
for (int i = 1; i <= 100; zbroj += i, ++i) ;
//...
konani zbroj bi bio pogrean prvo bi se uveao broja, a zatim tako uvean dodao
zbroju kao rezultat bismo dobili zbroj brojeva od 2 do 101. Meutim, kada stopimo
oba izraza za prirast
// ver. 4.0
//...
101
Usporedimo li zadnju inaicu (4.0) s prvom, skraenje kda je oevidno. Ali je isto
tako oito da je kd postao nerazumljiviji: prebacivanjem naredbe za pribrajanje brojaa
u for-naredbu, zakamuflirali smo osnovnu naredbu zbog koje je uope petlja napisana.
Budui da veina dananjih prevoditelja ima ugraene postupke optimizacije
izvedbenog kda, pitanje je koliko e i hoe li uope ovakva saimanja rezultirati
brim i efikasnijim programom. Stoga vam preporuujemo:
float x[5];
Navede li se ipak kod inicijalizacije duljina polja, treba paziti da ona bude vea ili
jednaka broju inicijalizirajuih lanova:
Tada lanovi brojanog polja kojima nedostaju inicijalizatori postaju jednaki nuli. Nije
dozvoljeno pridruiti praznu inicijalizacijsku listu polju koje nema definiranu duljinu:
Pri odabiru imena za polje treba voditi rauna da se ime polja ne smije
podudarati s imenom neke varijable u podruju dosega polja, tj. u podruju u
kojemu je polje vidljivo.
int a;
int a[] = {1, 9, 9, 6}; // pogreka: ve postoji a!
104
Pojedini lanovi polja se dalje u kdu dohvaaju pomou cjelobrojnog indeksa koji se
navodi u uglatoj zagradi [] iza imena polja.
Prvi lan u polju ima indeks 0, a zadnji lan ima indeks za 1 manji od duljine
polja.
U primjeru sa slike 6.2 to znai da su lanovi polja: x[0], x[1], x[2], x[3] i x[4].
Prema tome:
Kao to je iz ovih primjera oito, svaki pojedini lan polja moe se dohvaati i mijenjati
neovisno o ostalim lanovima.
Primjenu polja ilustrirat emo programom koji iz datoteke podaci.dat uitava
(x, y) parove toaka neke funkcije, a zatim za proizvoljni x unesen preko tipkovnice
izraunava interpoliranu vrijednost funkcije f(x) u toj toki. Koristit emo Lagrangeovu
formulu za interpolaciju:
n
f ( x ) Li ( x ) f i = L0 ( x ) f 0 + L1 ( x ) f 1 + Ln ( x ) f n ,
i =0
Tako dobivena funkcija prolazi tono kroz zadane toke (xi, fi) bez obzira kako su one
rasporeene (slika 6.3). Prvo emo uitati parove podataka u polja x[], odnosno y[]:
y
xn , fn
x3 , f3
x1 , f1
x0 , f0 x 2 , f2
#include <iostream.h>
#include <fstream.h>
int main() {
const int nmax = 100;
float x[nmax], y[nmax];
fstream ulazniTok("podaci.dat", ios::in);
if (!ulazniTok) {
cerr << "Ne mogu otvoriti traenu datoteku"
<< endl;
return 1;
}
int i = -1;
while (ulazniTok) { // prekida se na kraju datoteke
if (++i >= nmax) {
cerr << "Previe podataka!" << endl;
return 2;
}
ulazniTok >> x[i] >> y[i];
}
if (i < 0) {
cerr << "Nema podataka!" << endl;
return 3;
}
// nastavak slijedi...
Jedno od ogranienja pri koritenju polja jest da duljina polja mora biti
specificirana i poznata u trenutku prevoenja kda. Duljina jednom
deklariranog polja se ne moe mijenjati tijekom izvoenja programa.
Zbog toga smo morali definirati vrijednost simbolike konstante nmax prije deklaracija
polja. elimo li promijeniti dimenziju polja, promijenit emo varijablu nmax i kd
ponovno prevesti. nmax obavezno treba biti deklarirana kao const. Ako to ne bi bio
sluaj, duljina polja bi bila zadana obinom varijablom ija je vrijednost poznata tek
prilikom izvoenja programa, to je u suprotnosti s gornjim pravilom.
Za uitavanje podataka koristi se ulazni tok tipa fstream iz biblioteke fstream.
Pri otvaranju se provjerava li je datoteka dostupna ako nije ispisuje se pogreka,
zavrava program, a operacijskom sustavu se kao rezultat vraa broj 1. Uitavanje
podataka se obavlja u while-petlji. Petlja se ponavlja sve dok je ulazniTok razliit od
nule; nailaskom na kraj datoteke, ulazniTok postaje jednak nuli i petlja se prekida.
0.00 0.0
0.49 0.7
1.00 1.0
1.44 1.2
2.25 1.5
2.89 1.7
4.00 2.0
6.25 2.5
9.00 3.0
f(2) = 1.41701
vana mjera zatite, jer za razliku od mnogih programskih jezika, u jeziku C++ nema
provjere granica polja prilikom pristupa elementima.
int main() {
float a[] = {10, 20, 30};
float b[] = {40, 50, 60};
float c[] = {70, 80};
b[-1] b[3]
Zbog toga su naredbama za ispis lanova b[-1] i b[3] dohvaeni lanovi c[1],
odnosno a[0].
Kod dohvaanja lanova polja indeks openito smije biti cjelobrojna konstanta,
cjelobrojna varijabla ili cjelobrojni izraz. Stoga su sljedee naredbe dozvoljene:
108
float a[10];
int i = 2;
int j = 5;
float b = a[i + j]; // a[7]
b = a[i % j]; // a[2]
b = a[2 * i - j]; // a[-1]
Meutim, budui da indeks ne smije biti nita osim cjelobrojnog tipa, naredbe poput:
b = a[2.3]; // pogreka
float c = 1.23;
b = a[c / 2]; // pogreka
int Tablica[3][5];
lanove tog dvodimenzionalnog polja dohvaamo preko dva indeksa: prvi je odreen
retkom, a drugi stupcem u kojem se podatak u tablici nalazi. Ne zaboravimo pritom da
je poetni indeks 0:
U nekim jezicima se indeksi za sve dimenzije smjetaju unutar zajednikog para uglatih
zagrada, odvojeni zarezom:
C++ prevoditelj na ovakvu naredbu nee javiti sintaksnu pogreku! Zarez koji odvaja
bojeve 2 i 1 je operator razdvajanja koji e prouzroiti odbacivanje prvog broja; stoga
e gornja naredba biti ekvivalentna naredbi
Tablica[1];
Zapravo se dohvaa adresa prvog lana u drugom retku (o adresama e biti rijei
kasnije u ovom poglavlju).
Pravila koja vrijede za jednodimenzionalna polja vrijede i za viedimenzionalna
polja. lanovi viedimenzionalnog polja mogu se takoer inicijalizirati prilikom
deklaracije:
21 22 23 24 25
0 0 0 0 0
#include <iostream.h>
#include <fstream.h>
int main() {
const int nmax = 20;
float a[nmax][nmax+1];
fstream ulazniTok("koefic.dat", ios::in);
if (!ulazniTok) {
cerr << "Ne mogu otvoriti datoteku!" << endl;
return 1;
}
int n;
ulazniTok >> n;
if (n >= nmax) {
cerr << "Sustav jednadbi prevelik!" << endl;
112
return 2;
}
int r, s;
for (r = 0; r < n; r++) // po recima
for (int s = 0; s <= n; s++) // po stupcima
ulazniTok >> a[r][s];
// nastavak slijedi...
3
2 -7 4 9
1 9 -6 1
-3 8 5 6
Ostali lanovi polja ostaju nedefinirani, ali kako ih neemo dohvaati, to nema
nikakvog utjecaja na raun. Slijedi ranije opisani Gaussov postupak:
return 0;
}
x1 = 4.
x2 = 1.
x3 = 2.
Unato injenici da se unutar vanjske for-petlje nalaze dvije naredbe (unutarnja for-
naredba te naredba za uitavanje), prevoditelj blok za prvu for-naredbu uzima do kraja
prve izvrne naredbe, tj. do prvog znaka ; (toka-zarez).
Ponekad se moe ukazati potreba i za trodimenzionalnim poljima, pa e primjerice
u nekom programu za analizu prostorne raspodjele temperature trebati deklarirati polje
float temperatura[x][y][z];
kod kojeg e pojedini indeksi biti odreeni (x, y, z) koordinatama toaka za koje se
rauna temperatura. Iako broj dimenzija polja nije ogranien, polja s vie od dvije
dimenzije se koriste vrlo rijetko. Pritom valja uoiti da zauzee memorije raste s
potencijom broja dimenzija: jednodimenzionalno polje a[10] broji deset lanova,
dvodimenzionalno polje b[10][10] broji 10 10 = 100 lanova, a trodimenzionalno
polje c[10][10][10] sadri 10 10 10 = 1000 lanova.
Poneki itatelj e se zapitati kako se dvodimenzionalna ili trodimenzionalna polja
pohranjuju u memoriju koja se adresira linearno. Prevoditelj pojedine dimenzije polja
114
Zbog toga se moe dogoditi (slino kao u primjeru na stranici 107 za tri
jednodimenzionalna polja) da indeks izvan dozvoljenog opsega dohvati lan iz drugog
retka.
6.2. Pokazivai
Kao to samo ime kae, pokazivai (engl. pointers) su objekti koji pokazuju na drugi
objekt. Sam pokaziva sadri memorijsku adresu objekta na kojeg pokazuje. Iako na
prvi pogled svrsishodnost pokazivaa nije oita, u jeziku C++ njihova primjena je
veoma rasprostranjena, jer pruaju praktiki neogranienu fleksibilnost u pisanju
programa. Pokazivae smo posredno upoznali kod polja, budui da se lanovi polja
dohvaaju upravo preko pokazivaa.
Pokaziva se moe deklarirati tako da pokazuje na bilo koji tip podataka. U
deklaraciji se navodi tip podatka na koji pokaziva pokazuje, a ispred imena se stavlja *
(zvjezdica). Naredba
int *cajger;
deklarira pokaziva cajger na objekt tipa int. Smisao pokazivaa ilustrirat emo
sljedeim kdom:
Prvom naredbom se deklarira varijabla kazalo kao pokaziva na (neki) broj tipa int
(slika 6.8a). Zatim se deklarira varijabla n te joj se pridruuje vrijednost 5 (slika 6.8b).
Na kraju se varijabli kazalo, pomou operatora za dohvaanje adrese & pridruuje
vrijednost memorijske adrese na kojoj je pohranjena varijabla n (slika 6.8c). Sada
vrijednost varijable n moemo dohvaati izravno ili posredno, preko pokazivaa:
115
int*
int*
int*
int* kazalo :
kazalo
??
a)
int
int
int
int nn :
55
b)
int*
int* int
int
int*
int* kazalo :
kazalo int
int nn :
55
c)
Uoimo kako smo pri dohvaanju sadraja varijable n preko pokazivaa, morali ispred
imena pokazivaa umetnuti znak za pokaziva; nas naime zanima sadraj memorije na
koju pokazuje pokaziva, a ne sadraj memorije u kojoj se nalazi pokaziva.
Izostavimo li znak * ispred imena pokazivaa, dohvatit emo sadraj pokazivaa, tj.
memorijsku adresu na kojoj je pohranjena varijabla n:
Kod operacija preko pokazivaa naroito treba paziti da se ne izostavi operator *, jer u
protivnom moe doi do neugodnih efekata. Izostavimo li u prvoj gornjoj naredbi
operator *
int m = 3;
kazalo = &m;
int n = 5;
int *kazalo = &n;
int*
int* int
int
int*
int* kazalo :
kazalo int
int nn :
88
a)
int
int
int
int mm :
33
int*
int* int
int
int*
int* kazalo :
kazalo int
int nn :
88
int
int
int
int mm :
33 b)
int i = 1;
int j = 10;
int *p = &j;
*p *= *p;
i = i + j;
p = &i;
cout << i << endl << j << endl << *p << endl;
int n = 5;
int *pokn = &n;
float x = 10.27;
float *pokx = &x;
*pokn = *pokx; // n = 10
pokx = pokn; // pogreka
pokx = &n; // pogreka
*pokn = *pokx;
118
int *pokn;
float x = 10.27;
float *pokx = &x;
int n = 5;
int *pokn = &n;
float x = 10.27;
float *pokx = &x;
void *nesvrstan; // pokaziva na void
nesvrstan = &n;
cout << *(int*)nesvrstan << endl;
Prije ispisa, pokazivau na void treba dodijeliti tip pokazivaa na int, da bi prevoditelj
znao pravilno proitati sadraj memorije na koju nesvrstan pokazuje. Stoga na njega
primjenjujemo operator dodjele tipa (int*).
Zanimljivo je uoiti da pokazivai, unato tome to pokazuju na razliite tipove
podataka, na istom raunalu uvijek zauzimaju jednake memorijske prostore. U to se
moemo osvjedoiti pomou operatora sizeof: kako svi pokazivai zauzimaju jednaki
memorijski prostor, naredbe za ispis
6.2.1. Nul-pokazivai
Posebna vrijednost koji pokaziva moe imati jest nul-pokaziva (engl. null-pointer).
Takav pokaziva ne pokazuje nikuda pa pokuaj pristupa sadraju na koji takav
pokaziva pokazuje moe zavriti vrlo pogubno. Nema provjere tijekom izvoenja
programa je li vrijednost pokazivaa nul-pokaziva ili ne; na programeru je sva
odgovornost da do takve situacije ne doe. Pokaziva se moe inicijalizirati kao nul-
pokaziva tako da mu se jednostavno dodijeli nula:
int *x = 0;
Izuzetak od ovog pravila postoji ako se koriste razliiti modovi adresiranja memorije, kao to
je sluaj s DOS aplikacijama kod kojih postoje bliski (engl. near) i daleki (engl. far)
pokazivai koji su razliitih duljina.
120
int *x = NULL;
Postavlja se pitanje emu uope slue nul-pokazivai, kada njihova primjena moe
imati poguban uinak po izvoenje programa. Ako se pokaziva ne inicijalizira odmah
nakon deklaracije, moe mu se dodijeliti vrijednost nul-pokazivaa. Prije upotrebe,
provjerom vrijednosti pokazivaa mogue je ustanoviti je li on inicijaliziran ili ne.
float *ZvjezdicaUzIme;
float* ZvjezdicaUzTip;
float * ZvjezdicaOdmaknutaOdImenaITipa;
float*ZvjezdicaUzImeITip;
float* kazalo;
jer je znak za pokaziva * (zvjezdica) pridruen tipu, ime se tip podatka jae istie kao
pokaziva na float. Meutim, ovaj nain je nespretan pri viestrukim deklaracijama,
na primjer:
int* kazaljka;
int* kazaljka2;
Osim toga, takav nain pisanja dosljednije odraava prikaz pokazivaa u izrazima. To je
naroito izraeno u izrazima s mnoenjem. Na primjer, u sljedeem primjeru elimo
pomnoiti dvije varijable na koje pokazuju pokazivai poka i pokb. Preglednije je
pisati operatore dereferenciranja neposredno uz imena pokazivaa:
int a = 5;
int b = 10;
int *poka = &a;
int *pokb = &b;
ili, jo gore:
float x[5];
float a = x[2];
float
float **
Ova zadnja naredba moe se interpretirati na sljedei nain: uzmi adresu prvog lana
polja, poveaj ju za dva, pogledaj to se nalazi na toj adresi, te pridrui vrijednost na toj
lokaciji varijabli a. Valja uoiti da se pritom adresa ne poveava za dva bajta, ve za
dva segmenta u koje stanu podaci tipa float. Na primjer, ako se poetak polja x nalazi
na adresi 0x2000, a varijable tipa float zauzimaju 4 bajta, tada e pokaziva x + 2
pokazivati na adresu 0x2008. U ovom primjeru upoznali smo se s aritmetikim
operacijama s pokazivaima, o emu e ipak opirnije biti govora u sljedeem
potpoglavlju.
Zbog navedene veze izmeu pokazivaa i polja, ako se navede naziv polja bez
indeksa, on ima znaenje pokazivaa na prvi lan, te e sljedei kd ispisati iste
vrijednosti:
ispisati memorijsku adresu u kojoj je pohranjen lan polja b[1]. Oito je da su pristupi
lanovima polja preko indeksa ili preko pokazivaa potpuno ekvivalentni koji pristup
e se koristiti ovisi iskljuivo o ukusu programera.
Dovitljivi itatelj e na osnovi ove povezanosti pokazivaa i polja sada sam
zakljuiti zato je poetni indeks polja u jeziku C++ upravo nula, a ne 1 kao u nekim
drugim programskim jezicima.
No vano je ipak razumjeti osnovnu razliku izmeu pokazivaa i polja.
Deklaracijom
int x[5];
int x[5];
cout << "x: " << x << endl;
cout << "&x: " << &x << endl;
Polja i pokazivai su slini, no valja biti svjestan njihove razlike. Ime polja
je jednostavno sinonim za pokaziva na poetnu vrijednost, no sam
pokaziva nije nigdje alociran u memoriji.
rezultirat e preslikavanjem samo prvog lana a[0] u prvi lan b[0]. Ovakvo
djelovanje operatora pridruivanja na polja je logino, ako se razumije kako se polja
prikazuju na razini strojnog kda.
Promotrimo jo vezu izmeu pokazivaa i viedimenzionalnih polja.
Viedimenzionalna polja se pohranjuju u memoriju linearno (vidi sliku 6.7): polje
deklarirano kao
float a[3][4];
moe se stoga shvatiti kao niz od tri jednodimenzionalna polja, od kojih svako sadri
etiri elementa, sloenih jedno za drugim. Pritom su a[0], a[1] i a[2] pokazivai na
poetne lanove svakog od tih jednodimenzionalnih polja. U to se moemo osvjedoiti
sljedeim primjerom:
Naredbom za ispis dobit emo brojeve 1.1 i 2.1, tj. poetne lanove prva dva
jednodimenzionalna broja. Slijedi da se dvodimenzionalno polje moe interpretirati kao
polje pokazivaa, od kojih svaki pokazuje na poetak jednodimenzionalnog polja (slika
6.11).
float[3][4]
float[3][4] aa : a[0][0]: a[0][1]: a[0][2]: a[0][3]:
float
float float
float float
float float
float
1.1
1.1 1.2
1.2 1.3
1.3 1.4
1.4
float
float (*a)[0]
(*a)[0]
a[1][0]: a[1][1]: a[1][2]: a[1][3]:
float
float float
float float
float float
float
float
float (*a)[1]
(*a)[1] 2.1
2.1 2.2
2.2 2.3
2.3 2.4
2.4
int a;
int *poka = &a;
cout << poka << endl << poka - 3 << endl;
Ako na raunalu na kojem radimo tip int zauzima dva bajta, gornji programski
odsjeak e ispisati dvije memorijske adrese od kojih je druga za 2 * 3 = 6 manja od
prve. Openito, svaki izraz tipa
125
T *pok1, *pok2;
int i;
// ...
pok2 = pok1 + i;
Pri tome je T neki proizvoljan tip, a i neki cijeli broj. Gornja naredba se treba itati
ovako: Pretvori pok1 u pokaziva na char, zato jer je char tip duljine jednog bajta.
Dobivenu adresu uveaj za broj koji je jednak i-strukoj veliini tipa T. Tako dobivenu
adresu pretvori natrag u pokaziva na tip T.
Za pokazivae su dozvoljeni i skraeni aritmetiki operatori, kao inkrement,
dekrement te operatori obnavljajueg pridruivanja.
esto se aritmetika s pokazivaima koristi u vezi s poljima. Naime, kako su lanovi
polja sloeni linearno u memoriji, a ime polja predstavlja pokaziva na njegov prvi lan,
pomou pokazivake aritmetike je jednostavno mogue pristupati susjednim lanovima:
float x[10];
float *px = &x[3];
float x2 = *(px - 1); // x[2] prethodni lan
float x5 = *(px + 1); // x[4] sljedei lan
float x[10];
float *prviClanUNizu = x;
float *zadnjiClanUNizu = &x[9];
int razmak = zadnjiClanUNizu - prviClanUNizu; // = 9
razmak = prviClanUNizu - zadnjiClanUNizu; // = -9
#include <stdio.h>
int main() {
FILE *pok;
pok = fopen("datoteka.dat", "r");
if (pok == 0)
cout << "Nema traene datoteke.";
}
Gornji primjer poziva funkciju fopen() iz standardne biblioteke stdio.h koja otvara
neku datoteku. U sluaju da ta datoteka postoji, vratit e se pokaziva na objekt tipa
FILE (definiranog takoer u datoteci stdio.h), pomou kojeg se kasnije moe
pristupati sadraju datoteke. Ako operacija ne uspije, vratit e se nul-pokaziva. Gornju
provjeru moemo skraeno napisati i ovako:
if (!pok) // ...
// inicijalizirajmo varijable
int brClanova = 9;
int polje[20] = {10, 20, 30, 40, 50, 60, 70, 80, 90};
// pokaziva na lokaciju iza zadnjeg broja:
int *odrediste = polje + brClanova;
int noviBroj = 15;
// kree od zadnjeg (najveeg) broja
while (*(--odrediste) > noviBroj) {
if (odrediste < polje) // proao je poetak polja,
break; // pa prekida petlju
*(odrediste + 1) = *odrediste;
// pomie vee brojeve
}
*(odrediste + 1) = noviBroj; // umee novi broj
brClanova++;
Sr gornjeg programa ini while petlja koja brojeve koji su ve smjeteni u polju
usporeuje s noviBroj kojeg elimo umetnuti. Pretraivanje kree od zadnjeg
(najveeg) broja u nizu, a istovremeno s pretraivanjem pomiu se lanovi niza vei od
novopridolog. Moe se vidjeti kako je za pomicanje po elementima polja koriten
pokaziva odrediste koji se dekrementira u svakom prolasku kroz petlju. Tako se na
jednostavan nain omoguava prolazak kroz sve lanove polja.
127
*(odrediste + 1) = *odrediste;
Zadatak. U gornji kd ubacite provjeru broja unesenih lanova niza tako njegova
duljina ne nadmai duljinu polja. Takoer ubacite provjeru da li broj ve postoji u nizu,
te onemoguite viekratno pohranjivanje jednakih brojeva.
float nekiBroj;
float *pokazivacNaTajBroj = &nekiBroj;
*pokazivacNaTajBroj = 34.234;
128
float *pokaZivac;
pokaZivac = new float();
*pokaZivac = 10.5;
Operatorom new rezervira se potreban memorijski prostor za objekt tipa float. Iza
kljune rijei new navodi se tip podatka za koji se osigurava memorijski prostor, a zatim
(neobavezno) par okruglih zagrada. Ako je operacija alociranja uspjeno izvedena,
operator vraa adresu alociranog prostora koju pridruujemo naem pokazivau
pokaZivac. Ako postupak nije bio uspjean (na primjer, nema dovoljno memorije za
zahtijevani objekt), operator vraa nulu, odnosno nul-pokaziva. Zato bismo, radi
vlastite sigurnosti, trebali provjeriti rezultat operatora new te eventualno javiti pogreku
ako alokacija memorije ne uspije:
Unutar okruglih zagrada iza oznake tipa moe se definirati poetna vrijednost objekta,
to znai da smo prethodni primjer mogli pisati jo krae:
delete pokaZivac;
Iza kljune rijei delete navodi se ime pokazivaa na lokaciju koju treba osloboditi.
Naravno da pritom taj pokaziva mora biti dohvatljiv iako je prostor na koji pokaziva
pokazuje alociran dinamiki, sam pokaZivac je deklariran kao automatski objekt, te
mu se po izlasku iz bloka gubi svaki trag.
Valja uoiti da primjena operatora delete ne podrazumijeva i brisanje sadraja
objekta njime samo prostor koji je zauzimao objekt postaje slobodnim za odstrel, tj. za
druge dinamike objekte. Stoga je gotovo sigurno da e izvoenje naredbi:
delete [] sati;
To nije tono za sve prevoditelje jer mnogi prevoditelji automatski generiraju kd koji unitava
sve dinamike objekte prilikom okonanja programa. No na to svojstvo se ne treba oslanjati,
ve valja uredno osloboditi svu zauzetu memoriju runo.
130
Vrlo je lako zaboraviti umetnuti par uglatih zagrada prilikom oslobaanja polja.
Prevoditelj nee javiti nikakvu pogreku, pa ak niti upozorenje, no takav kd moe
prouzroiti probleme prilikom izvoenja. Operator delete oslobaa memoriju samo
jednog lana, dok operator delete [] prvo provjerava duljinu polja, te unitava svaki
element polja.
Kao primjer za alokaciju i dealokaciju polja posluit e nam program kojemu je svrha
za zadane toke izraunati koeficijente pravca koji se moe najbolje provui izmeu
njih. Zamislimo trkaa koji tri stalnom brzinom. Mjerenjem meuvremena na svakih
100 metara elimo odrediti prosjenu brzinu kojom on tri, te koliko e mu vremena
trebati da pretri stazu dugaku 800 metara. Rezultati mjerenja dani su u tablici 6.1.
Prikaemo li te podatke grafiki (slika 6.12), uoit emo da su mjereni podaci grupirani
oko pravca. Nagib tog pravca odgovara prosjenoj brzini trkaa, a ekstrapolacijom
pravca na 800 m moi emo procijeniti vrijeme potrebno za cijelu stazu.
Jednadbu pravca odredit emo postupkom najmanjih kvadrata. Koeficijenti pravca
y = ax + b
odreeni su izrazima
n-1
1
a =n-1 ( x i -x ) yi ,
( xi -x )2 i=0
i =0
b =y -a x ,
gdje su
131
800
600
s
m 400
200
0
0 50 100
t /s
1 n-1 1 n-1
x = x i , y = yi
n i =0 n i =0
srednje vrijednosti koordinata toaka. Pokuajmo napisati program koji koristi gore
opisanu metodu najmanjih kvadrata. Prvo treba uitati broj mjernih toaka i pripadajue
koordinate:
#include <iostream.h>
int main() {
cout << "Koliko toaka mogu oekivati? ";
int n; // broj toaka
cin >> n;
srednjiY /= n;
// nastavak slijedi...
Iza oznake tipa podatka, u uglatim zagradama naveden je broj lanova polja. Kao i kod
alokacije pojedinanih varijabli, operator new vraa kao rezultat pokaziva na poetak
polja ili nul-pokaziva, ovisno o uspjehu alokacije (Sa titom ili na titu!). Ovo je jedini
nain za alokaciju polja ija je duljina poznata prilikom izvoenja ako bismo koristili
automatska polja, bilo bi neophodno predvidjeti najvei mogui broj elemenata koji e
se smjetati u polje, te deklarirati takvo polje. U veini sluajeva bi to znailo uludo
troenje memorije, a s druge strane nemamo jamstva da e predviena duljina polja
uvijek biti dostatna. Ovako se alocira upravo potreban broj lanova, ime se
racionalizira potronja memorije.
Pri uitavanju koordinata u petlji, u varijable srednjiX i srednjiY se
akumuliraju njihove sume, a po izlasku iz petlje se iz njih raunaju srednje vrijednosti.
Slijedi petlja u kojoj se raunaju sume u brojniku i nazivniku izraza za koeficijent
smjera a:
// ... nastavak
double a = 0.0; // koeficijent smjera
double nazivnik = 0.0; // nazivnik izraza za k.s.
for (i = 0; i < n; i++) {
double deltaX = x[i] - srednjiX;
a += deltaX * y[i];
nazivnik += deltaX * deltaX;
}
a /= nazivnik;
double b = srednjiY - a * srednjiX;
// ima jo...
Pozdrav kojim su spartanski vojnici otpravljani u ratove. Znai: Vratite se kao pobjednici ili
nam se ne vraajte ivi pred oi!
133
delete [] x;
delete [] y;
ime se naznaava da se oslobaa polje. Unutar zagrada ne smije biti upisano nita
prevoditelj zna kolika je duljina polja, te e na osnovi toga osloboditi odgovarajui
prostor. U starijim inaicama C++ jezika, unutar uglatih zagrada trebalo je navesti
duljinu polja, meutim standard sada to ne zahtijeva i ne dozvoljava.
Spomenimo jo nekoliko vanih injenica vezanih uz alokaciju polja: za razliku od
alokacije prostora za pojedinane varijable, prilikom alokacije polja lanovi se ne mogu
inicijalizirati. Takoer, unato injenici da je polje alocirano tijekom izvoenja
programa, duljina tog polja se ne moe naknadno dinamiki mijenjati. Na primjer, to
napraviti ako broj lanova polja postane vei od duljine alociranog polja? Poneki
dovitljivi haker bi mogao doi na ideju za sljedei kd:
Operator new vraa pokazivae na poetke tih jednodimenzionalnih polja, tj. adresu
polja od 3 pokazivaa, od kojih svaki pokazuje na poetak jednodimenzionalnog polja s
4 lana (vidi sliku 6.11 na strani 124). Okrugla zagrada oko pokazivaa *a je
neophodna jer uglata zagrada ima vii prioritet od znaka za pokaziva. Izostavljanjem
zagrada:
delete [] a;
dozvoljena. Razlog tome lei u nainu na koji generirani strojni kd pristupa poljima.
Dvodimenizonalno polje se slae u memoriju tako da se reci upisuju uzastopno jedan iza
drugoga. Da bi mogao odrediti poetak svakog pojedinog retka, prevoditelju je potreban
podatak o duljini redaka, a to je broj stupaca.
#include <iostream.h>
int main() {
float **dvaDpolje;
int i, redaka, stupaca;
Deklaracijom
float **dvaDpolje;
Prvo se u petlji oslobaaju prostori za polja pojedinih redaka, a tek potom se uklanja
pokaziva (na pokaziva) dvaDpolje. Redoslijed oslobaanje je vrlo vaan mora se
obavljati obrnutim redoslijedom od onoga pri pridruivanju. Naime, ako bi se prvo
uklonio poetni pokaziva na pokaziva dvaDpolje, izgubila bi se veza prema
pokazivaima na poetke pojedinih redaka, pa ih ne bi bilo mogue vie dohvatiti. Time
bi bila onemoguena dealokacija prostora za pojedine retke.
float**
float**
float**
float** dvaDpolje
dvaDpolje :
float r = 23.4;
pipok = &r; // OK
Promjena varijable u tom sluaju mogua je samo izravno, ali ne i preko pokazivaa na
konstantu:
r = 45.2; // OK
*pipok = 9.23 // pogreka
Objekt na koji pokaziva pokazuje moe se po volji mijenjati, izravno ili preko
pokazivaa:
nekiBroj = 1993.1009; // OK
*neMrdaj = 1992.3001; // OK
pokKonst = pokPromj;
139
Gornja dodjela je dozvoljena jer time integritet objekta na koji pokPromj pokazuje
nee biti naruen: jedino se vrijednost objekta nee moi promijeniti preko pokazivaa
pokKonst. No obrnuta dodjela nee biti dozvoljena:
Gornja naredba sada prevoditelju daje na znanje da programer na sebe preuzima svu
odgovornost za eventualnu promjenu konstantnog objekta.
Sve to je reeno za nepromjenjive pokazivae i pokazivae na nepromjenjive
objekte vrijedi i za volatile pokazivae i pokazivae na volatile objekte. Tako je
mogue deklarirati razliite pokazivae:
char[]
char[]
: 'F'
'F' 'a'
'a' 'u'
'u' 's'
's' 't'
't' '' '' 'V'
'V' 'r'
'r' 'a'
'a' 'n'
'n' ''
'' 'i'
'i' ''
'' '\0'
'\0'
padobran
padobran
Svaki pravilno inicijalizirani znakovni niz sadri i zakljuni znak '\0' (nul-
znak, engl. null-character). Njega kod inicijalizacije nije potrebno eksplicitno
navesti, ali treba voditi rauna da on zauzima jedno znakovno mjesto.
F 70
a 97
u 117
s 115
t 116
32
V 86
r 114
a 97
n 110
232
i 105
230
0
J 74
u 117
l 108
i 105
j 106
e 101
Paljiv itatelj je zasigurno primijetio da iako smo ispisivali znakovni niz padobran,
dobili smo i dio niza minijature. Naime, prevoditelj kojeg smo koristili za izvoenje
gornjeg primjera poredao je u memoriju znakovne nizove u obrnutom redoslijedu od
redoslijeda deklaracije. Znakovni niz minijature smjeten je neposredno iza niza
padobran. Kako smo u for-petlji ispisivali dvadeset znakova, to je vie nego to ih
ima padobran, zagazili smo i u niz minijature. Ovakvo ponaanje je vrlo ovisno o
141
prevoditelju: neki drugi prevoditelj e moda smjestiti podatke na drukiji nain te ete
moda dobiti drukiji ispis.
Nul-znak izmeu dva niza umetnuo je prevoditelj da bi funkcijama koje e
dohvaati i obraivati znakovni niz dao do znanja gdje on zavrava. Tako e izlazni tok
za ispis:
tono znati da je slovo '' ispred nul-znaka zadnje u nizu, te e pravilno ispisati sadraj
niza padobran samo do tog slova:
Faust Vrani
padobran[13] = '&';
tj. ispisuju se svi znakovi do prvog nul-znaka. Ako bismo prepisali i nul-znak koji
zakljuuje drugi niz, rezultat ispisa bi bio nepredvidiv.
Na osnovi ovog razmatranja slijedi da smo znakovne nizove mogli deklarirati i
inicijalizirati identino kako smo to radili i kod jednodimenzionalnih polja, ali uz
obaveznu eksplicitnu inicijalizaciju zakljunog nul-znaka:
Naizgled, ova inicijalizacija nee proi, jer kada smo slinu stvar pokuali s brojevima:
Koliko je vano paziti da li se znak ili znakovni niz omeuju jednostrukim ili
dvostrukim navodnicima najbolje ilustrira sljedea (vrlo tipina) pogreka:
char Slovo;
cin >> Slovo;
if (Slovo == "a") // pogreka: usporedba znaka i niza
// ...
143
#include <iostream.h>
int main() {
char dani[7][12] = {"nedjelja",
"ponedjeljak",
"utorak",
"srijeda",
"etvrtak",
"petak",
"subota" };
cout << "Kad e taj " << dani[5] << endl;
return 0;
}
char *dani[] = {
"nedjelja",
"ponedjeljak",
"utorak",
"srijeda",
"etvrtak",
"petak",
"subota" };
144
char* days[] = {
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday" };
for (int i = 0; i < 7; i++)
dani[i] = days[i];
6.5. Reference
Reference su poseban tip podataka. One djeluju slino pokazivaima, te su u sutini
drugo ime za objekt odreenog tipa. Razmotrimo sljedei primjer:
#include <iostream.h>
int main() {
int i = 5; // varijabla
int &iref = i; // referenca na varijablu i
cout << "i = " << i << " iref = " << iref << endl;
int &iref = i;
Time se zapravo uvodi novo ime za ve postojeu varijablu i (vidi sliku 6.15).
Promjenom reference mijenja se u stvari izvorna varijabla, u to nas uvjerava ponovni
ispis varijabli u gornjem programu.
145
double x;
const double& xref = x;
Pokuaj promjene konstantne reference xref prevoditelj e prijaviti kao pogreku, ali
se varijabla x moe mijenjati bez ikakvih ogranienja.
Reference najveu primjenu imaju kao argumenti i povratne vrijednosti funkcija, pa
emo ih u poglavlju o funkcijama pomnije upoznati.
int i, j;
int &ref = j;
*(&ref) = &j; // bezuspjean pokuaj preusmjeravanja
// reference
146
Pri tome haker-hlebinac moe pomisliti da e &ref dati adresu na kojoj se zapravo
nalazi pokaziva te *(&ref) omoguava upisivanje adrese varijable j na to mjesto.
Gornji kd se nee uope niti prevesti, jer e prevoditelj dojaviti da ne moe pretvoriti
int * u int. O emu se zapravo radi?
Iako su reference u biti pokazivai, jezik C++ ne dozvoljava izravan pristup adresi
reference; to se njega tie, referenca je samo sinonim za neki drugi objekt. Zbog toga
e &ref vratiti adresu objekta na koji referenca pokazuje, a ne adresu reference. U to se
moemo osvjedoiti sljedeim primjerom:
int main() {
char a;
char &ref = a;
cout << "Adresa od a: " << &a << endl;
cout << "Adresa reference: " << &ref << endl;
return 0;
}
Nakon izvoenja, dobit emo ispis u kojemu su obje adrese identine. Takoer, sizeof
operator nee vratiti veliinu reference (koja je jednaka veliini pokazivaa), nego e
vratiti veliinu referiranog objekta:
int main() {
char a;
char &ref = a;
cout << "Veliina od a: " << sizeof(a) << endl;
cout << "Veliina reference: " << sizeof(ref) << endl;
return 0;
}
Nakon izvoena, dobit emo da je veliina i varijable a i reference ref jednaka jedan.
No referencu se ipak moe preusmjeriti prljavim trikovima. Donji program uspjeno je
preveden i izveden pomou Borland C++ 4.5 prevoditelja na Pentium raunalu te ne
garantiramo uspjeno izvoenje na nekoj drugoj kombinaciji prevoditelja i/ili raunala.
int main() {
char zn1 = 'a',zn2 = 'b', &ref = zn1;
// Join us on the following page as Dirty Harry strikes...
char **pok = (char **)(&zn2 - sizeof(char*));
*pok = &zn2;
cout << ref << endl;
return 0;
}
Nakon izvoenja ispisao se znak 'b', ime smo se osvjedoili da ref vie ne referira
varijablu zn1, nego zn2. Ideja je sljedea: prevoditelj sve lokalne varijable smjeta u
kontinuirano podruje memorije. Usporedbom adresa varijabli zn1 i zn2 ustanovili smo
da e na prevoditelj prvo smjestiti varijablu ref, zatim zn2, pa onda zn1, (dakle, u
147
obrnutom redoslijedu od slijeda deklaracije). Zbog toga smo s &zn2 dobili adresu koju
smo umanjili za veliinu pokazivaa. Rezultat je adresa reference koju smo dodijelili
pokazivau pok. On je nakon dodjele tipa pokazivao na referencu (vidimo da je to
pokaziva na pokaziva, to je uinjeno i u deklaraciji). Zatim je sadraj tog pokazivaa
promijenjen, to je rezultiralo i promjenom sadraja reference. Na kraju jo slijedi
slavodobitan ispis. (Imamo referencu!)
Gornji program se koristi prljavim trikovima koji izlaze izvan definicija jezika C++
te ovise o implementaciji prevoditelja. Zbog toga pisanje i izvoenje takvih programa
provodite na vlastitu odgovornost. (Mi smo u ovom sluaju Poncije Pilati.) Ako takav
program i radi na jednoj vrsti raunala, vjerojatno nee raditi ako se promijeni
prevoditelj ili se program prenese na drugi tip raunala.
int main() {
int *pok; // pokaziva
int i = 10;
{
int j = 100; // lokalna varijabla u bloku
pok = &j; // pokaziva usmjeravamo na nju
}
// ...
// varijabla j vie ne postoji!
cout << *pok << endl; // upitan ispis
return 0;
}
Izlaskom iz bloka varijabla j prestaje postojati, ali i dalje postoji pokaziva pok koji
pokazuje na mjesto gdje je varijabla j bila pohranjena. Prevoditelj e to osloboeno
podruje vrlo vjerojatno upotrijebiti za pohranu neke druge varijable, pa e ispis
sadraja preko pokazivaa davati nepredvidivi rezultat. Jo je gora mogunost da preko
pokazivaa pok pohranimo neku vrijednost i prepiemo sadraj novostvorene varijable.
Ovakve mrtve due mogu zadavati velike glavobolje programerima budui da je
takvim pogrekama dosta teko naknadno ui u trag. Pokazivai koji pokazuju na
148
pok = 0;
//...
if (pok)
cout << *pok << endl;
else
cout << "nul-pokaziva!" << endl;
//...
Slika 6.16. Visei
pokaziva
Deklaracija typedef se obavlja tako da se iza kljune rijei typedef navede obina
deklaracija. Naziv sinonima se umee na mjesto gdje bi se inae nalazio naziv objekta
koji se deklarira: u gornjem primjeru, ako bismo kljunu rije typedef izostavili, dobili
bismo deklaraciju varijable pokZnak. Kako je ispred varijable stavljena kljuna rije
typedef, deklarira se sinonim pokZnak koji oznaava tip jednak tipu objekta koji bi se
deklarirao u sluaju bez kljune rijei typedef. Deklaracija typedef se moe nalaziti
samo u globalnom podruju imena i, za razliku od deklaracija klasa, ne smije se nalaziti
unutar definicije funkcije.
149
char *telefonskiImenik[100];
poljeZnakovnihNizova telefonskiImenik;
Sada ne moramo razbijati glavu time ima li zvjezdica vii ili nii prioritet od uglatih
zagrada, te jesmo li moda zapravo deklarirali pokaziva na polje od sto znakova.
150
7. Funkcije
funkcijaPrva(){
// ...
int main(){ return;
// ... }
funkcijaPrva();
// ...
// ... funkcijaDruga(){
funkcijaDruga(); // ... funkcijaTreca(){
// ... funkcijaTreca(); // ...
// ... // ... return;
funkcijaDruga(); return; }
// ... }
funkcijaCetvrta();
// ...
return 1;
} funkcijaCetvrta(){
// ...
return;
}
p p!
= .
r r !( p r )!
#include <iostream.h>
int main() {
int i, p, r;
cout << "p = ";
cin >> p;
cout << "r = ";
cin >> r;
long brojnik = 1;
for (i = p; i > 1; i--) // rauna p!
brojnik *= i;
long nazivnik = 1;
for (i = r; i > 1; i--) // rauna r!
nazivnik *= i;
for (i = p - r; i > 1; i--) // rauna (p-r)!
nazivnik *= i;
Odmah su uoljive tri vrlo sline petlje u kojima se raunaju faktorijele u brojniku,
odnosno nazivniku binomnog koeficijenta.
Pretpostavimo da imamo na raspolaganju gotovu funkciju faktorijel() koja
rauna faktorijelu broja. U tom sluaju gornji kd bismo mogli napisati ovako:
#include <iostream.h>
int main() {
int p, r;
cout << "p = ";
cin >> p;
cout << "r = ";
cin >> r;
Tip ispred imena funkcije odreuje kakvog e tipa biti podatak kojeg e funkcija
vraati pozivatelju kao rezultat svoga izvoenja. Argumenti unutar zagrada iza imena
Istina, kd bi se, kraenjem dijela umnoka u brojniku i nazivniku, mogao napisati efikasnije,
sa samo dvije for petlje, ali bi to umanjilo efekt primjera!
153
funkcije su podaci koji se predaju funkciji prilikom njena poziva. Broj i tip tih
argumenata moe biti proizvoljan, s time da funkcija moe biti i bez argumenata, a
mogu se deklarirati i funkcije s neodreenim brojem argumenata. Broj argumenata,
njihov redoslijed i tip nazivaju se potpisom funkcije (engl. function signature).
Kao jednostavan primjer uzmimo funkciju koja rauna kvadrat broja. Nazovimo tu
funkciju kvadrat. Argument te funkcije bit e broj x koji treba kvadrirati; uzmimo da
je taj argument tipa float. Funkcija e kao rezultat vraati kvadrat broja;
pretpostavimo da je to broj tipa double. Deklaracija funkcije kvadrat() izgledat e u
tom sluaju ovako:
Ovo je samo prototip funkcije. Za sada nismo definirali funkciju nismo napisali kd
same funkcije, odnosno nismo odredili to i kako funkcija radi. Definicija funkcije (engl.
function definition) kvadrat() izgledala bi:
double kvadrat(float x) {
return x * x;
}
Naredbe koje se izvode prilikom poziva funkcije ine tijelo funkcije (engl. function
body). Tijelo funkcije uvijek poinje prvom naredbom iza lijeve vitiaste zagrade {, a
zavrava pripadajuom desnom zagradom }. U gornjem primjeru tijelo funkcije sastoji
se samo od jedne jedine naredbe koja kvadrira x. Kljuna rije return upuuje na to
da se umnoak x * x prenosi kao povratna vrijednost u dio programa koji je pozvao
funkciju.
Deklaracija i definicija funkcije mogu biti razdvojene i smjetene u potpuno
razliitim dijelovima izvornog kda. Deklaracija se mora navesti u svakom
programskom odsjeku gdje se funkcija poziva, prije prvog poziva. To je potrebno kako
bi prevoditelj znao generirati strojni kd za poziv funkcije (taj kd ovisi o potpisu
funkcije). Definicija funkcije, se naprotiv, smjeta u samo jedan dio kda. Svi pozivi te
funkcije e se prilikom povezivanja programa usmjeriti na tu definiciju.
Funkcija mora biti deklarirana u izvornom kdu prije nego to se prvi puta
pozove. Definicija funkcije oblikom mora u potpunosti odgovarati
deklaraciji.
double kvadrat(float);
154
#include <iostream.h>
#include <iomanip.h>
int main() {
for(int i = 1; i <= 10; i++)
cout << setw(5) << i
<< setw(10) << kvadrat(i) << endl;
return 0;
}
Zanimljivo je spomenuti da je ovaj princip prvo uveden u jezik C++, da bi tek potom bio
prihvaen u jeziku C i ukljuen u ANSI C standard [Stroustrup94].
156
int main() {
// ...
funkcijaPrva();
// ...
funkcijaDruga();
}
Sada emo taj kd razmjestiti u tri odvojena modula, koje emo nazvati
poglavni.cpp, funk1.cpp i funk2.cpp; u prvi modul emo smjestiti glavnu
(main()) funkciju, a u potonje module definicije funkcija. Pogledajmo sadraje
pojedinih modula:
poglavni.cpp
int main() {
// ...
funkcijaPrva();
// ...
funkcijaDruga();
}
157
funk1.cpp
funk2.cpp
#include "funk1.h"
funk1.h
funk2.h
float apsolutno(float x) {
return (x >= 0) ? x : -x;
}
Uvjetni operator radi na sljedei nain: ako je argument x vei ili jednak nuli, rezultat
operatora je jednak vrijednosti argumenta, a u protivnom sluaju rezultat je argument s
promijenjenim predznakom (odnosno apsolutna vrijednost). Naredbom return se
rezultat operatora proglaava za povratnu vrijednost, izvoenje funkcije se prekida te se
vrijednost vraa pozivajuem programu. Parametar naredbe return openito moe
biti bilo kakav broj, varijabla ili izraz koji se izraunava prije zavretka funkcije. Pri
tome tip rezultata izraza mora odgovarati tipu funkcije.
Ako je rezultat izraza naredbe return razliitog tipa od tipa funkcije, rezultat se
(ugraenim ili korisniki definiranim) pravilima konverzije svodi na tip funkcije. Stoga
e funkcija apsolutno() deklarirana kao:
double apsolutno(float x) {
return (x >= 0) ? x : -x;
}
vraati rezultat tipa double, unato tome to je argument, odnosno rezultat return
naredbe tipa float Naravno da nema previe smisla ovako definirati funkciju tipa
159
long apsolutnoCijelo(float x) {
return (x >= 0) ? x : -x;
}
koja e (ako argument nije prevelik) vraati cjelobrojni dio argumenta. Rezultat
uvjetnog pridruivanja je opet float, ali kako je funkcija deklarirana kao long,
pozivajuem kdu e biti vraen samo cjelobrojni dio argumenta.
Funkcija kao rezultat moe vraati podatke bilo kojeg tipa, izuzev polja i
funkcija (iako moe vraati pokazivae i reference na takve tipove).
U pozivajuem kdu rezultat funkcije moe biti dio proizvoljnog izraza. Konkretno,
prije definiranu funkciju kvadrat() moemo koristiti u aritmetikim izrazima s desne
strane operatora pridruivanja, tretirajui ju kao svaki drugi double podatak. Na
primjer:
Rezultat funkcije se moe i ignorirati. Funkciju kvadrat() smo mogli pozvati i ovako:
kvadrat(5);
Iako je smislenost gornje naredbe upitna, poziv je sasvim dozvoljen. Ako funkcija ne
treba vratiti vrijednost, to se moe eksplicitno naznaiti tako da se deklarira tipom void.
Obino su to funkcije za ispis poruka ili rezultata ovisnih o vrijednostima argumenata.
Na primjer, zatreba li nam funkcija koja e samo ispisivati kvadrat broja, a sam kvadrat
nee vraati pozivnom kdu, deklarirat emo i definirati funkciju:
void ispisiKvadrat(float x) {
cout << (x * x) << endl;
return;
}
Zadatak. Napiite program za odreivanja dana u tjednu koritenjem dviju funkcija ije
su deklaracije:
160
Prva funkcija neka pretvara zadani datum u cijeli broj (izmeu 0 i 6), a druga funkcija
shodno tom broju neka ispisuje tekst naziva dana u tjednu.
Budui da funkcija tipa void nita ne vraa pozivnom kdu, naredba return ne smije
sadravati nikakav podatak. tovie, u ovakvim sluajevima se naredba return moe i
izostaviti.
ispisiKvadrat(10);
Povratak iz funkcije je mogu s bilo kojeg mjesta unutar tijela funkcije. Stoga se
naredba return moe pojavljivati i na vie mjesta. Takav primjer imamo u sljedeoj
funkciji:
int main() {
// ...
return 0;
}
int main(void) {
// ...
}
poglavlju. Meutim, vrlo je vano uoiti da prilikom takvog poziva funkcije formalni
argument i vrijednost koja se prenosi nisu meusobno povezani. Formalni argument
ivi samo unutar funkcije te je njegova vrijednost po izlasku iz funkcije izgubljena.
Ako u funkciji mijenjamo vrijednost argumenta, promjena se nee odraziti na objekt
koji smo naveli u listi prilikom poziva. Pogledajmo na jednom banalnom primjeru
kakve to ima praktine posljedice:
#include <iostream.h>
int DodajSto(float i) {
i += 100;
return i;
}
int main() {
int n = 1;
DodajSto(n);
cout << "Radio " << n << endl; // ispisuje 1
n = DodajSto(n);
cout << "Radio " << n << endl; // ispisuje 101
return 0;
}
Iako funkcija DodajSto() u svojoj definiciji uveava vrijednost argumenta, ona barata
samo s lokalnom varijablom koja se prilikom poziva inicijalizira na vrijednost stvarnog
argumenta. Pojednostavljeno reeno, funkcija je napravila kopiju argumenta te cijelo
vrijeme radi s njom. Prilikom izlaska se kopija unitava, jer je ona definirana samo
unutar funkcijskog bloka. Zbog toga e nakon prvog poziva funkcije varijabla n i dalje
imati istu vrijednost kao i prije poziva. Ovakav prijenos vrijednosti funkciji naziva se
prijenos po vrijednosti (engl. pass by value).
Kako se uveana vrijednost vraa kao rezultat funkcije, tek nakon drugog poziva
funkcije, tj. pridruivanja povratne vrijednosti varijabli n, varijabla n e doista biti
uveana.
itatelju naviknutom na osobine nekih drugih programskih jezika (npr. BASIC,
FORTRAN) init e se ovakvo ponaanje argumenata funkcije vrlo nespretnim i
neshvatljivim. Meutim, ovakav pristup ima jednu veliku odliku: zatitu podataka u
pozivajuem kdu. Radi ilustracije, pretpostavimo da se prilikom izvoenja funkcije
zaista mijenja i vrijednost stvarnog argumenta (nazovimo to BASIC-pristup). Uz takvu
pretpostavku, gornji program bi ispisao brojeve 101 i 201. Razmotrimo sada kakve bi
posljedice u takvom sluaju imala naizgled banalna promjena definicije funkcije
DodajSto(). Na primjer, definiciju funkcije e netko napisati krae kao:
int DodajSto(int i) {
return i + 100;
}
163
Ovime se smisao funkcije nije promijenio ona i nadalje kao rezultat vraa broj jednak
argumentu uveanom za 100. Meutim, u tijelu funkcije se vrijednost formalnog
argumenta vie ne mijenja, tako da bi nakon ovakve preinake uz BASIC-pristup konani
ishod programa bio drukiji. Naprotiv, ispis uz ne-BASIC-pristup e biti uvijek isti,
togod mi radili s argumentom unutar funkcije, uz pretpostavku da je povratna
vrijednost u naredbi return pravilno definirana.
Ovakvim pristupom u jeziku C++ u velikoj su mjeri izbjegnute popratne pojave
(engl. side-effects) koje mogu dovesti do neeljenih rezultata, budui da je koliina
podataka koji se izmjenjuju izmeu pozivnog kda i funkcije svedena na najmanju
moguu mjeru: na argumente i povratnu vrijednost.
int i = 0;
while (i < 10) cout << kvadrat(++i) << endl;
int n = 2;
cout << pow(++n, n) << endl; // promjenjivi rezultat!
Ako prethodni primjer drukije napiemo, dobit emo kd koji e imati isti rezultat
neovisan o implementaciji pojedinog prevoditelja:
int n = 2;
++n;
cout << pow(n, n) << endl; // ispisuje 27
segrt = prvi;
prvi = drugi;
drugi = segrt;
}
Prilikom poziva funkcije zamijeni(a, b), unutar funkcije vrijednosti varijabli prvi i
drugi e biti zamijenjene. Dakle, algoritam je sutinski korektan. Meutim, po izlasku
iz funkcije ta zamjena nema nikakvog efekta, jer je funkcija baratala s preslikama
vrijednosti varijabli a i b, a ne s izvornicima.
Jedno mogue rjeenje je upotreba pokazivaa prilikom poziva funkcije. Umjesto
vrijednosti, funkciji emo proslijediti pokazivae na objekte. Tada emo funkciju
definirati ovako:
Prilikom poziva, umjesto stvarnih vrijednosti a i b proslijedit emo adrese tih objekata:
zamijeniPok(&a, &b);
165
Poziv funkcije sada nije potrebno komplicirati operatorom &, ve je dovoljno napisati:
zamijeniRef(a, b);
Sutinski gledano, nema razlike izmeu pristupa preko pokazivaa ili referenci: rezultat
je isti. Reference su u biti pokazivai koje nije potrebno dereferencirati prilikom
koritenja pa se mehanizam prenoenja se u jednom i u drugom sluaju na razini
generiranog strojnog kda odvija preko pokazivaa. Meutim, ve je na prvi pogled
uoljiva vea jednostavnost kda ako koristimo reference. To se odnosi na definiciju
funkcije gdje nije potrebno koristiti operator dereferenciranja *, a naroito na poziv
funkcije, jer ne treba navoditi operator adrese &. Zato itatelju najtoplije preporuujemo
koritenje referenci. Posebno se to odnosi na C-gurue naviknute iskljuivo na
pokazivae, budui da u programskom jeziku C reference ne postoje kao tip podataka.
Podsjetimo se da se pokazivai i reference na razliite tipove podataka ne mogu
pridruivati. Pokuamo li funkciju zamijeniPok() pozvati tako da joj prenesemo kao
argument pokaziva na neto to nije int, prevoditelj e javiti pogreku prilikom
prevoenja:
float a = 10.;
int b = 3;
zamijeniPok(*a, *b); // pogreka: a je float
float a = 10.;
int b = 3;
zamijeniRef(a, b); // oprez!
int main() {
char *korisnik;
UnesiIme(korisnik); // lo poziv
cout << korisnik << endl;
delete [] korisnik;
}
Na veliko razoaranje C++ utokljunaca, gornji primjer nee ispisati uneseno ime, ve
e po svoj prilici izbaciti neki nesuvisli niz znakova. U emu je problem?
Do kljua za razumijevanja gornjeg problema doi emo ako se podsjetimo da se
parametri u sve funkcije prenose po vrijednosti. To znai da se parametar naveden u
pozivu funkcije kopira u privremenu varijablu koja ivi iskljuivo za vrijeme izvoenja
funkcije. Parametar moemo slobodno mijenjati unutar funkcije, a promjene se nee
odraziti na stvarni parametar naveden u pozivajuem kdu. Upravo se to dogaa s naim
parametrom korisnik prilikom poziva se vrijednost pokazivaa korisnik kopira u
privremenu varijablu ime. Funkcija UnesiIme() barata s tom privremenom vrijednosti,
a ne sa sadrajem varijable korisnik. Adresa memorijskog bloka alociranog
operatorom new se pridruuje lokalnoj varijabli, te se na to mjesto uitava znakovni niz.
Nakon to funkcija zavri, vrijednost varijable ime se gubi, a varijabla korisnik ostaje
nepromijenjena.
Rjeenje ovog problema nije tako sloeno: umjesto prosljeivanja vrijednosti
varijable korisnik, potrebno je funkciji proslijediti njenu adresu. Parametar funkcije
e sada postati pokaziva na pokaziva na znak: sama varijabla korisnik je tipa
pokaziva na znak, a njena adresa je pokaziva na pokaziva. Unutar funkcije
UnesiIme() takoer treba voditi o tome rauna, tako da se vrijednosti pokazivaa
pristupi pomou *ime. Evo ispravnog programa:
int main() {
char *korisnik;
UnesiIme(&korisnik); // prosljeuje se adresa
cout << korisnik << endl;
delete [] korisnik; // uvijek poistite za sobom!
}
int main() {
char *korisnik;
UnesiIme(korisnik); // prosljeuje se adresa,
// ali to nije potrebno
// eksplicitno navesti
cout << korisnik << endl;
delete [] korisnik;
}
int main() {
int b[] = {5, 10, 15};
Pocisti(b, 3);
cout << b[0] << endl
<< b[1] << endl
<< b[2] << endl;
return 0;
}
lan. Stoga funkcija preko tog pokazivaa rukuje s izvornikom, a ne preslikom tog polja.
Shodno tome, gornju funkciju smo mogli potpuno ravnopravno deklarirati i na sljedei
nain:
Iako postoji standardna funkcija strlen() koja rauna duljinu znakovnog niza,
napisali smo svoju inaicu takve funkcije. Uoimo u uvjetu while petlje kako je zbog
vieg prioriteta operatora * bilo neophodno staviti zagrade oko operacije pribrajanja
pokazivau. Da te zagrade nema, uvjet petlje bi dohvaao prvi lan polja, te ono to se
tamo nalazi (kd znaka) uveao za broja i.
170
#include <iostream.h>
#include <iomanip.h>
int main() {
float matrica[redaka][stupaca] =
{ { 1.1, 1.2, 1.3 },
{ 2.1, 2.2, 2.3 } };
ispisiMatricu(matrica);
return 0;
}
Iz poziva funkcije je jasno da se matrica prenosi preko pokazivaa na prvi lan. Stoga je
navedeni zapis u deklaraciji funkcije ovakav samo radi bolje razumljivosti. Varijable
redaka i stupaca deklarirane su ispred tijela funkcija ispisiMatricu() i main(),
tako da su one dohvatljive iz obiju funkcija (o podruju dosega imena govorit emo
opirnije u zasebnom poglavlju).
Zadatak. Napiite funkciju ispisiMatricu() koristei pokazivae i aritmetiku s
pokazivaima. U funkciju prenesite samo pokaziva na prvi lan.
Vidjeli smo da se viedimenzionalna polja pohranjuju u memoriju kao nizovi
jednodimenzionalnih polja. Stoga je prva dimenzija nebitna za pronalaenje odreenog
lana u polju, pa ju nije obavezno ukljuiti u deklaraciju parametra:
Gornja funkcija e raditi za proizvoljan broj redaka, pa smo podatak o broju redaka
prenijeli kao poseban argument. Potekoe iskrsavaju ako dimenzije polja nisu zadane u
171
Argument m[][] u deklaraciji funkcije nije dozvoljen, jer u trenutku prevoenja mora
biti poznata druga dimenzija (za viedimenzionalna polja: sve dimenzije osim prve).
U prethodnom poglavlju smo pokazali kako se lanovi dvodimenzionalnog polja
mogu dohvaati preko pokazivaa na pokazivae. Iskoristimo tu injenicu pri pozivu
funkcije za ispis lanova matrice:
#include <iostream.h>
#include <iomanip.h>
int main() {
int redaka = 2;
const int stupaca = 3;
Oito je ovakva notacija teko itljiva. Daleko elegantnije rjeenje pruaju korisniki
definirani tipovi klase, koje se mogu tako definirati da sadre podatke o pojedinim
dimenzijama te da sadre funkcije za ispis lanova. Klase e biti opisane u narednim
poglavljima, a za sada uoimo jo samo da je u gornjem kdu stupaca bilo neophodno
172
deklarirati kao const, budui da druga dimenzija polja mora biti poznata u trenutku
prevoenja koda.
Svaki pokuaj promjene sadraja tog niza unutar funkcije prouzroit e pogreku
prilikom prevoenja:
Sada vie nije mogue prosljeivati kao parametar pokazivae na konstantne objekte,
jer postoji opasnost da e se vrijednost konstantnog objekta promijeniti u funkciji:
int main() {
const char* TkoPjeva = "Franjo afranek";
cout << DuljinaNiza(TkoPjeva); // pogreka
return 0;
}
raunanje umnoka svih cijelih brojeva izmeu dva zadana (u brojniku). Time e se
ukupni broj mnoenja u raunu doista smanjiti, ali e se poveati broj funkcija u
izvornom kdu.
Obje tako definirane funkcije u sutini bi provodile isti postupak: mnoenje
uzastopnih cijelih brojeva. Umjesto da definiramo dvije odvojene funkcije, moemo
poopiti funkciju faktorijel() tako da proirimo listu argumenata dodatnim
parametrom stojBroj koji e ograniavati broj mnoenja. Za uobiajeni faktorijel taj
argument bi imao vrijednost 1. Da bismo otklonili potrebu za eksplicitnim navoenjem
te vrijednosti kod svakog poziva funkcije, definirat emo podrazumijevanu vrijednost
(engl. default argument value) tom dodatnom argumentu:
#include <iostream.h>
na poziv funkcije znati da ona moe imati podrazumijevane argumente. Zbog toga se
podrazumijevane vrijednosti argumenata moraju navesti u deklaraciji funkcije.
Ovo je naravno vano ako su deklaracija i definicija funkcije razdvojene. Treba li u
tom sluaju ponoviti podrazumijevanu vrijednost u definiciji funkcije? Ne, jer
prevoditelj ne usporeuje meusobno vrijednosti koje se pridruuju argumentu u
deklaraciji, odnosno definiciji, tako da e pokuaj pridruivanja u definiciji interpretirati
kao pokuaj promjene podrazumijevane vrijednosti (ak i ako su one jednake):
// ...
// deklaracija funkcije:
long faktorijel(int, int = 1);
// ...
// definicija funkcije:
long faktorijel(int n, int stojBroj = 1) { // pogreka
// redeklaracija podrazumijevane vrijednosti
// ...
// deklaracija funkcije:
long faktorijel(int, int = 1);
// definicija funkcije:
long faktorijel(int n, int stojBroj) {
// podrazumjevana vrijednost je
// ve pridruena u deklaraciji
// ...
}
jer pridruivanje argumenata pri pozivu funkcije tee s lijeva na desno, pa e u pozivu
funkcije samo s jednim argumentom drugi argument ostati neinicijaliziran. Ako se u
deklaraciji funkcije podrazumijevane vrijednosti pridruuju nekolicini argumenata, tada
svi oni moraju biti grupirani na kraj liste parametara:
Ovime je odreeno da funkciji printf() prilikom njena poziva treba prenijeti barem
jedan argument tipa char * (pokaziva na znakovni niz). Ostali argumenti nisu
neophodni i mogu biti proizvoljnog tipa. Zarez iza zadnjeg deklariranog argumenta nije
neophodan, tako da smo funkciju printf() mogli deklarirati i kao:
#include <iostream.h>
#include <stdarg.h>
int i = 0;
while (*(format + i)) {
if (*(format + i) == '%') {
switch (*(format + (++i)++)) {
case 'd':
case 'i': {
int br = va_arg(vl, int);
cout << br;
break;
}
case 's': {
char *zn_niz = va_arg(vl, char*);
cout << zn_niz;
break;
}
case 'c': {
char zn = va_arg(vl, char);
cout << zn;
break;
}
default:
cerr << endl
<< "Nedefinirani tip podataka! "
<< endl;
va_end(vl);
return 0;
}
}
else
cout << (*(format + i++));
};
va_end(vl);
return 1;
}
Prvo je deklariran objekt vl tipa va_list koji je definiran unutar datoteke zaglavlja
stdarg.h. Taj objekt na neki nain sadri sve argumente funkcije, te emo pomou
njega pristupati pojedinom argumentu (sama struktura objekta korisniku nije vana on
179
Za one koji ipak nee moi zaspati dok ne spoznaju to je va_list to je pokaziva na stog.
Makro funkcija va_start() inicijalizira taj pokaziva tako da pokazuje iza zadnjeg
prenesenog parametra, dok va_arg() preko njega pristupa pojedinom argumentu.
180
nagib(2., 5.);
Potekoe, a esto i pogreke nastaju kada se iz funkcije eli prenijeti lokalni objekt
generiran pri pozivu funkcije. Uzmimo da smo poeljeli funkciju koja e sadraj jednog
znakovnog niza nalijepiti na drugi znakovni niz i novonastali niz vratiti kao rezultat.
Nazovimo tu funkciju dolijepi(); argumenti te funkcije bit e pokazivai na nizove
koje elimo spojiti, a povratna vrijednost e biti referenca na novostvoreni niz.
Deklaracija funkcije bi prema tome izgledala kao:
Unutar funkcije se generira lokalno polje spojeni u koje se stapaju znakovni nizovi na
koje pokazuju argumenti funkcije. Pri izlasku iz funkcije prenosi se pokaziva na to
polje pozivnom kdu. Meutim, kako se lokalno polje spojeni izlaskom iz funkcije
gubi iz vidokruga, rezultat je nepredvidiv.
elimo li ipak da gornja funkcija kao rezultat vrati znakovni niz, morat emo kao
parametar proslijediti pokaziva na memorijsko podruje u koje elimo smjestiti
rezultat:
Time smo vrue kestenje prebacili na pozivajui kd, koji sada preuzima brigu o
alociranju i oslobaanju prostora za znakovni niz, a pozvana funkcija e samo mijenjati
sadraj alociranog prostora.
Zadatak: Napiite kd za ovako deklariranu funkciju dolijepi(). Obratite panju na
vraanje vrijednosti.
njihov ivot vezan za izvoenje funkcije. Takvi objekti se zovu lokalni objekti, jer
vrijede lokalno za samu funkciju. Objekt nije vezan striktno za funkciju, ve za blok u
kojemu je deklariran (vidi poglavlje 3.1), no funkcija u biti nije nita drugo nego blok.
Na primjer:
U gornjem primjeru varijable i i j su brojai koji nemaju smisla izvan funkcije. Zbog
toga se oni deklariraju kao lokalni. Memorija se dodjeljuje na ulasku u funkciju i
oslobaa na izlasku iz nje. Varijabla priv je takoer lokalna, s time da je ona
deklarirana u ugnjeenom bloku. Ona ivi samo unutar tog bloka, stvara se na poetku
bloka i unitava na kraju.
Za lokalne varijable se kae da imaju automatsku smjetajnu klasu (engl. automatic
storage class) koja se ispred deklaracije moe eksplicitno navesti kljunom rijei auto:
int main() {
int dan = 30;
int mjesec = 1;
int godina = 1997;
int main() {
dan = 30;
mjesec = 1;
godina = 1997;
mjesec = noviMjesec();
return 1;
}
int noviMjesec() {
if (++mjesec > 12) {
mjesec = 1;
godina++;
}
return mjesec;
}
Da smo kojim sluajem u gornjem kdu, deklaracije varijabli datumskih varijabli stavili
iza funkcije main(), ona ih ne bi mogla dohvaati, pa bi prevoditelj javio pogreku da
te varijable nisu deklarirane u funkciji main().
Globalna deklaracija ujedno je i definicija. Naime, svi globalni objekti se umeu u
izvedbeni kd. Oni ive od poetka izvoenja programa do njegovog kraja. Prilikom
deklaracije objekta mogue je odmah provesti i inicijalizaciju po pravilima koja smo do
sada nauili.
#include <iostream.h>
int main() {
int lokalna;// neinicijalizirana lokalna varijabla
185
prva naredba za ispis na zaslonu ispisati nulu, a druga neki neodreeni broj koji e
varirati ovisno o uvjetima pod kojima je program preveden i pokrenut.
Zadatak: Razmislite i provjerite to e biti ispisano u gornjem primjeru, ako se
globalna i lokalna deklariraju kao pokazivai na znakovne nizove.
Oito se deklariranjem varijabli kao globalnih pojednostavnjuje pristup njima. Meutim,
time se one izlau opasnosti od nekontroliranih promjena unutar razliitih funkcija.
Podaci koji trebaju biti dostupni svim funkcijama, ali se ne smiju mijenjati (npr.
univerzalne matematike ili fizike konstante) deklariraju se kao globalne konstante,
dodavanjem modifikatora const ispred deklaracije tipa.
Globalni objekti mogu imati dvije smjetajne klase: vanjske ili statike. Ako se kd
protee preko nekoliko modula, globalni objekti deklarirani na gore opisani nain bit e
vidljivi u svim modulima. Vie modula moe koristiti isti objekt, s time da tada objekt
smije biti definiran iskljuivo u jednom modulu. U preostalim modulima objekt je
potrebno samo deklarirati, ali ne i definirati. Deklaracija se provodi tako da se ispred
naziva objekta stavi kljuna rije extern:
int main() {
float a = 50; // lokalna varijabla
cout << a << endl; // ispisuje 50
}
int main() {
float a = 50; // lokalna varijabla
cout << a << endl; // lokalna varijabla: 50
cout << ::a << endl; // globalna varijabla: 10
}
#include <iostream.h>
#include <stdlib.h>
void VoliMeNeVoli() {
static bool VoliMe = false; // statiki objekt
VoliMe = !VoliMe;
if (VoliMe)
cout << "Voli me!" << endl;
else
cout << "Ne voli!" << endl;
187
int main() {
int i = rand() % 10 + 1; // sluajni broj od 1 do 10
while (i--)
VoliMeNeVoli();
}
Voli me!
Ne voli!
Voli me!
Ne voli!
Voli me!
Voli me!
Voli me!
Voli me!
Statiki objekti u funkcijama ive za vrijeme cijelog izvoenja programa, ali su dostupni
samo unutar funkcije u kojoj su deklarirani. Te statike objekte moemo shvatiti kao da
su deklarirani izvan funkcije koristei smjetajnu klasu static, s time da im se moe
pristupiti samo iz funkcije u kojoj su deklarirani. Zato se i koristi ista kljuna rije
static: smjetaj ovakvih objekata i globalnih statikih objekata se provodi na isti
nain (u podatkovni segment programa), pa e i statiki objekti imati poetnu vrijednost
0 ako se ne inicijaliziraju drukije.
double kvadrat(float x) {
return x * x;
}
188
Izvoenje prevedenog kda bilo bi bre kada bi svaki poziv funkcije kvadrat() u
programu bio jednostavno zamjenjen naredbom za meusobno mnoenje dva ista broja.
Dodavanjem kljune rijei inline ispred definicije funkcije daje se naputak
prevoditelju da pokua svaki poziv funkcije nadomjestiti samim kdom funkcije. Takve
se funkcije nazivaju umetnute funkcije (engl. inline function). Evo primjera:
float kvadrat(float x) {
return x * x;
}
int main() {
int i = 16;
i = kvadrat(i);
}
int kvadrat(int x) {
return x * x;
}
No to uiniti ako su nam u programu potrebna oba oblika funkcije kvadrat(), tj. kada
elimo kvadrirati i cijele i decimalne brojeve? float inaica funkcije e podravati oba
tipa podatka, ali e, kao to smo ve primijetili, iziskivati nepotrebne konverzije tipa.
Programski jezik C++ omoguava preoptereenje funkcija (engl. function
overloading) koritenje istog imena za razliite inaice funkcije. Pritom se funkcije
moraju meusobno razlikovati po potpisu, tj. po tipu argumenata u deklaraciji funkcije.
Prevoditelj e prema tipu argumenata sam prepoznati koju inaicu funkcije mora za
pojedini poziv upotrijebiti. Tako e u kdu:
int main() {
float x = 0.5;
int n = 4;
190
int main() {
double dupliX = 3456.54321e49;
cout << kvadrat(dupliX) << endl; // pogreka!
}
long kvadrat(long);
float kvadrat(float);
int main() {
kvadrat(42); // pogreka: nedoumica je li
// kvadrat(long) ili kvadrat(float)
kvadrat(3.4); // isti problem
}
191
int main() {
float x = -23.76;
cout << apasalutno(x) << endl;
cout << apasalutno(-473) << endl;
}
Budui da bilo koji tip T (na primjer int) i referenca na taj tip T & (na primjer
int &) prihvaaju jednake inicijalizirajue vrijednosti, funkcije iji se argumenti
razlikuju samo po tome radi li se o vrijednosti ili referenci na nju, ne mogu se
preoptereivati. Zato e prevoditelj prilikom prevoenja sljedeeg kda prijaviti
pogreku:
void f(int i) {
//...
}
#include <iostream.h>
void f() {
cout << "Nita!" << endl;
}
int main() {
f(); // pogreka: f(int) ili f()?
long kvadrat(int);
long kvadrat(long);
#include <iostream.h>
#include <math.h>
7.9. Rekurzija
Pozivom funkcije formiraju se unutar nje lokalne varijable koje su nedohvatljive izvan
same funkcije. Neovisnost lokalnih varijabli od okolnog programa omoguava da
funkcija poziva samu sebe ponovnim pozivom se stvaraju nove lokalne varijable
potpuno neovisne o varijablama u pozivajuoj funkciji. Ovakav proces uzastopnog
pozivanja naziva se rekurzija (engl. recursion). Razmotrimo rekurziju na jednostavnom
primjeru: funkciji za raunanje faktorijela:
int faktorijel(int n) {
return (n > 1) ? (n * faktorijel(n - 1)) : 1;
}
int nf = faktorijel(3);
faktorijel(3);
n = 3;
faktorijel(n-1);
n = 2;
faktorijel(n-1);
n = 1;
return 1;
return n*faktorijel(n-1);
// vraa 2
return n*faktorjel(n-1);
// vraa 6
nf = faktorjel(3);
takve formule implementirati jednostavnim petljama. Rekurzije mogu biti vrlo efikasno
sredstvo za rukovanje posebnim strukturama podataka.
Ako to nije mogue, bit emo prisiljeni za razliite podintegralne funkcije svaki puta
mijenjati izvorni kd funkcije Integral().
Rjeenje za probleme ovakvog tipa pruaju pokazivai na funkcije. Osim to
funkcija moe primati odreene argumente i vraati neki rezultat, ona u izvedbenom
kdu programa zauzima i odreeni prostor u memoriji raunala. Kada pozivamo neku
197
Prvi argument funkcije Integral() je pokaziva na funkciju f() tipa double koja
kao argument prihvaa samo jedan podatak tipa double. Ispred imena parametra u listi
argumenata nalazi se operator dereferenciranja * koji ukazuje na to da emo funkciji
Integral() prenijeti pokaziva, a ispred operatora dereferenciranja je identifikator
povratnog tipa (double) funkcije f(). Iza pokazivaa na funkciju f() unutar zagrada
se navode tipovi argumenata funkcije. Vjerojatno ne treba naglaavati da se ti argumenti
moraju slagati s argumentima funkcije koja e se pozivati po broju i tipu (ili se stvarni
argumenti moraju dati svesti na formalne ugraenim ili korisniki definiranim
pretvorbama).
Uoimo zagrade oko oznake za pokaziva f na funkciju. One su neophodne, jer bi u
protivnom:
zbog vieg prioriteta zagrada nad operatorom *, prvi argument bio interpretiran kao
deklaracija funkcije f(double) koja kao rezultat vraa pokaziva na double.
Posvetimo se sada definiciji funkcije Integral(). Za poetak se podsjetimo
ukratko o emu se u trapeznom pravilu radi. Interval x0, xn unutar kojeg se funkcija
f (x) eli integrirati podijeli se na n jednakih intervala, izmeu kojih se funkcija
aproksimira pravcima (slika 7.3). Integral funkcije (tj. povrina ispod funkcije) priblino
je jednaka zbroju povrina tako dobivenih trapeza (rafirane plohe na slici):
xn
f ( x0 ) f ( xn )
f ( x) dx =&h
x0
2
+ f ( x1 ) + f ( x2 ) + + f ( xn 1 ) +
2
f ( x0 ) f ( x1 )
I 0 = h0 + , h0 = xn x0
2 2
a zatim e broj intervala udvostruavati, raunajui sve tonije aproksimacije integrala:
h0 f ( x0 ) f ( x2 )
I1 = + f ( x1 ) +
2 2 2
h0 f ( x0 ) f ( x4 )
I2 = + f ( x1 ) + f ( x2 ) + f ( x3 ) +
4 2 2
...
Budui da se broj podintervala podvostruuje, u svakom koraku se raunaju vrijednosti
podintegralne funkcije i za sve toke obraene u prethodnom koraku. Da se izbjegne
nepotrebno ponavljanje rauna, podintegralnu funkciju je dovoljno raunati samo za
nove toke i tu novu sumu (nazvat emo ju sumaNova) pridodati sumi iz prethodnog
koraka (sumaStara). Postupak e se ponavljati sve dok su zadovoljena dva uvjeta:
1. relativna promjena sume je vea od unaprijed dozvoljene vrijednosti koju funkciji
predajemo kao parametar relGrijeh, te
2. broj podintervala je manji ili jednak od broja CircusMaximus, kojeg takoer
prenosimo funkciji kao parametar.
Kako korisnik ne bi morao svaki puta prenositi vrijednosti ova dva parametra, pridruit
emo im podrazumijevane vrijednosti. Pogledajmo cjelokupni izvorni kd:
#include <iostream.h>
#include <math.h>
y
f ( x)
x0 x1 x2 xn1 xn x
int main() {
cout << Integral(sin, 0, 3.14159) << endl;
cout << Integral(Podintegralna, 0, 1.) << endl;
return 0;
}
double Podintegralna(double x) {
return sin(x) * sin(x);
}
mogue zamke u uvjetu (3). Naime, ako se kojim sluajem dogodi da sumaNova bude
jednaka 0, dijeljenje u treem uvjetu uzrokovat e prekid programa (dijeljenje s
nulom). S novododanim uvjetom (2) to se nee dogoditi, jer u sluaju da on nije
zadovoljen, nee se niti poeti ispitivati trei uvjet. Izvoenje petlje e se prekinuti, a
itatelju ostavljamo na razmiljanje hoe li rezultat funkcije biti ispravan.
Radi bolje ilustracije, pogledajmo uvjet u sljedeem if-grananju (log() je
funkcija koja vraa prirodni logaritam ln argumenta):
Iako funkcija logiki-i ima svojstvo komutacije, tj. logiki rezultat ne ovisi o tome koji
je operand s lijeve, a koji s desne strane, samo e gornji redoslijed operanada uvijek
osiguravati pravilno izvoenje gornjeg kda. Ako nije ispunjen prvi uvjet (tj. ako je x
negativan ili jednak 0), daljnje ispitivanje nema smisla i ono se ne provodi, tako da se uz
navedeni redoslijed ne moe funkciji log() prenijeti negativni argument.
Ekvivalentna situacija je i kada u uvjetu imamo logiki-ili. Izvrnimo uvjet u
gornjem if-grananju i napiimo kd:
U ovom sluaju, im je zadovoljen prvi uvjet (x <= 0), bit e zadovoljen cijeli uvjet,
tako da se daljnje ispitivanje ne provodi opet je izbjegnuta mogunost da se funkciji
log() prenese negativni argument.
Iz gornjeg primjera je vidljivo da je sintaksa za deklaraciju pokazivaa na funkcije
dosta zamrena. Ako se pokaziva na funkciju esto koristi, jednostavnije je definirati
novi sinonim za taj tip pomou kljune rijei typedef:
pokFunkcija mojaFunkcija
Funkcija se poziva preko pokazivaa tako da se navede naziv pokazivaa iza kojega se u
zagradama specificiraju parametri. Pri tome pokaziva se moe prije poziva
dereferencirati operatorom *, ali i ne mora (dereferenciranje se podrazumijeva):
mojaFunkcija(5.);
(*mojaFunkcija)(5.); // isti poziv kao i redak iznad
Potpun tip pokazivaa na funkciju odreuju povratna vrijednost funkcije te broj i tip
argumenata. Zbog toga nije mogue pokazivau dodijeliti adresu funkcije koja ima
drukiji potpis:
Pri tome, ako sada funkciju elimo pozvati pomou pokazivaa pok podrazumijevane
parametre ne moemo koristiti:
pok(60); // OK
pok(); // pogreka: pok nema podrazumijevanih
// parametara
int main() {
//...
}
koja ima praznu listu argumenata (ovaj oblik smo do sada iskljuivo i koristili), ili
U ovom drugom obliku, prvi argument argc jest broj parametara koji se prenose
programu iz radne okoline unutar koje je program pokrenut. Ako je argc razliit od
nule, tada se parametri koji se prenose mogu dohvatiti preko lanova polja argv, od
argv[0] do argv[argc - 1]. Ti lanovi su pokazivai na znakovne nizove koji sadre
pojedine parametre. Tako je argv[0] pokaziva na poetak niza koji sadri ime kojim
je program pozvan ili prazan niz (""). argv[argc] je uvijek nul-pokaziva.
Ilustrirajmo dohvaanje argumenata funkcije main(), jednostavnim programom
koji ispisuje rezultat aritmetike operacije na dva broja, zadane kao parametra iza
poziva programa:
#include <iostream.h>
#include <math.h>
case('/'):
cout << (operand1 / operand2) << endl;
break;
default:
cerr << "Nepoznati operator " << operacija << endl;
return 1;
}
return 0;
}
racunaj 2 / 3
argc = 4
argv[0] = "racunaj.exe"
argv[1] = "2"
argv[2] = "/"
argv[3] = "3"
argv[4] = 0
Prvi argument argc je jednak broju parametara koji slijede iza naredbe kojom je
program pokrenut, uveanom za 1. argv[0] jest pokaziva na znakovni niz s imenom
kojim je program pokrenut. Ponekad taj znakovni niz sadri cijelu stazu do izvrnog
programa (npr. "C:\\knjiga.cpp\\primjeri\\racunaj.exe"). Slijede
pokazivai na parametre, a niz se zakljuuje nul-pokazivaem argv[4].
Funkcija main() u pravilu zavrava naredbom return (koja mora sadravati
cjelobrojni parametar) koja uzrokuje unitenje svih automatskih objekata. Operacijskom
sustavu se prenosi vrijednost navedena u return naredbi. Ako je izvoenje programa
zavrilo ispravno, podrazumijeva se da povratna vrijednost bude 0; u protivnom se vraa
neka vrijednost razliita od 0 (vrijednost ovisi o operacijskom sustavu). Ako izvoenje
funkcije main() nije zakljueno naredbom return (primjerice ako ju zaboravimo
napisati), tada e ona skonati kao da se na kraju nalazi naredba
return 0;
204
Budui da u svakom C++ programu smije biti samo jedna main() funkcija, njeno se
ime ne moe preoptereivati.
Ako ne elimo posebno obavjetavati operacijski sustav o uspjenosti izvoenja
naeg programa, funkciju main() moemo deklarirati tipa void:
void main() {
// ...
}
r = x2 + y2
y
= arctan
x
koju ukljuujemo na poetku kda. Budui da rezultat elimo ispisati ukljuit emo i
zaglavlje iostream.h u kojem su deklarirani ulazni i izlazni tok te preoptereeni
operator <<.
#include <iostream.h>
#include <math.h>
int main() {
double x = -1;
double y = -1;
double r = sqrt(x * x + y * y);
double fi = atan2(x, y);
cout << "r =" << r << " fi =" << fi << endl;
return 0;
}
Valja uoiti da postoji i funkcija atan(double) koja kao argument prihvaa tangens
kuta koji treba izraunati. Meutim, funkcija atan2() je prikladnija, jer daje toan kut
u sva etiri kvadranta (funkcija atan() daje rezultate samo u prvom i etvrtom
kvadrantu), te za kuteve vrlo blizu /2 i /2, tj. kada je x = 0.
Ako se neka biblioteka zaboravi ukljuiti, prevoditelj e javiti pogreku tijekom
prevoenja. Ukljuivanje suvinih biblioteka (tj. biblioteka iz koje nisu koritene
funkcije) nee imati nikakve kobne posljedice na izvrni kd, jedino e se sam program
dulje prevoditi. Mnogi dananji povezivai (linkeri) provjeravaju koje funkcije se
pozivaju u programu, te samo njih ukljuuju u izvrni kd.
U tablici 7.1 su navedene neke od standardiziranih biblioteka te opis funkcija koji
se u njima nalaze. Vjerujemo da su veini itatelja najzanimljivije matematike funkcije
i funkcije za obradu znakovnih nizova, deklarirane u math.h, odnosno string.h. U
prilogu B na kraju knjige zainteresirani itatelj moe nai popis najee koritenih
funkcija, te njihovo kratko objanjenje.
Gornji odsjeak e samo preusmjeriti pokaziva zn2 na poetak znakovnog niza zn1.
Budui da se pritom nee stvoriti novi znakovni niz, svaka promjena na nizu zn2 e se
odraziti i na nizu zn1, to vjerojatno nije ono to bismo oekivali od operatora
pridruivanja. Takoer, jednostavnim operatorom zbrajanja ne moemo nadovezati dva
znakovna niza:
#include <iostream.h>
#include <string.h>
int main() {
char *prvi = "mali";
char *drugi = "princ";
char *praznina = " ";
strcat(oba, praznina);
strcat(oba, drugi);
int usporedba = strcmp(oba, prvi);
if (usporedba) {
if (usporedba > 0)
cout << "\"" << oba << "\" je vei od \""
<< prvi << "\"" << endl;
else
cout << "\"" << prvi << "\" je vei od \""
<< oba << "\"" << endl;
}
else
cout << " \"" << prvi << "\" i \"" << oba
<< "\" su jednaki" << endl;
return 1;
}
Funkcija strlen() kao rezultat vraa duljinu niza bez zakljunog nul-
znaka. Ukupno zauzee memorije jest za jedan znak vee.
Zato je bilo neophodno prilikom alokacije prostora za znakovni niz oba zbroju duljina
znakovnih nizova prvi, drugi i praznina pridodati 1.
Zadatak. Razmislite i provjerite to e ispisati sljedea naredba:
char *oba
char *prvi m a l i \0
strcpy(oba, prvi);
char *oba m a l i \0
char *oba m a l i \0
char *praznina \0
strcat(oba, praznina);
char *oba m a l i \0
char *replika;
strcpy(replika, "terminator"); // opasno!
e biti preveden, ali e prilikom izvoenja funkcija strcpy() preslikati znakovni niz
u dio memorije koji ne pripada (samo) nizu replika, to e zasigurno poluiti
neeljene efekte.
Zadatak. Razmislite to ne valja u sljedeem kdu:
char broj[3];
strcpy(broj, "pet"); // potencijalna opasnost!
Ako niste sigurni u odgovor, isprobajte ovaj kd dodajui naredbu za ispis niza broj
nakon poziva funkcije strcpy(). Sam primjer je ve sam za sebe potencijalno
opasan, ali ak i ako se preivi kopiranje, imat emo problema ako niz broj kasnije
pokuamo preslikati u neki drugi niz (razmislite zato). Ovakva pogreka je vrlo esta u
C++ poetnika.
Funkcija strcat() se koristi za nadovezivanje sadraja dva znakovna niza. Ona
ima deklaraciju sljedeeg oblika:
Djeluje tako da nadovezuje znakovni niz straga na znakovni niz sprijeda. Prilikom
preslikavanja niza straga preslikava se i njegov zakljuni nul-znak (vidi sliku 7.5).
Kao i kod funkcije strcpy(), programer mora paziti je li za rezultantni niz
(zajedno sa zakljunim nul-znakom) osiguran dovoljan prostor, kako se preslikavanjem
ne bi zalo u memorijski prostor rezerviran za druge varijable ili izvedbeni kd. Stoga
nije na odmet jo jednom naglasiti:
209
char deponij[25];
strcpy(deponij, "kanal");
strcpy(deponij + 3, "tata u F-duru");
cout << deponij << endl;
Ona obavlja abecednu usporedbu sadraja dvaju niza, znak po znak sve dok su
odgovarajui znakovi u oba niza meusobno jednaki ili dok ne naleti na zakljuni nul-
znak u jednom od nizova. Ako je prvi niz vei (u abecednom slijedu dolazi iza
drugoga), funkcija kao rezultat vraa cijeli broj vei od nule; ako su nizovi potpuno
jednaki vraa se nula, a ako je drugi niz vei, vraa se negativni cijeli broj.
Funkcija strcmp() razlikuje velika i mala slova. Preciznije, usporedba se obavlja
prema ASCII nizu znakova u kojem sva velika slova prethode malim slovima. Zbog
toga e usporedba nizova u sljedeem primjeru:
javiti da po abecednom slijedu niz "Tata" prethodi nizu "mama", a to vjerojatno nije
ono to bi korisnik oekivao. Da bi se to izbjeglo, valja koristiti funkciju strlwr(),
koja sva slova u nizu pretvara u mala, omoguavajui korektnu abecednu usporedbu.
Kako bi se pojednostavnilo rukovanje znakovnim nizovima, pogodno je znakovni
niz opisati pomou objekta. Za taj objekt tada moemo definirati operatore koji bitno
pojednostavnjuju gore navedene operacije. Tako se usporedba moe obavljati
standardnim poredbenim operatorima (<, <=, ==, !=, >=, >), dodjela operatorom = i
slino. tovie, dio C++ standarda je i klasa String koja ve posjeduje svu navedenu
funkcionalnost. Vjerujemo da e itatelju to biti puno jasnije nakon to upozna osnove
objektnog programiranja u narednim poglavljima.
210
#include <iostream.h>
#include <math.h>
#include <stdlib.h>
int main() {
cout << ln(2) << endl;
cout << ln(0) << endl; // ciao da "program main"
cout << ln(5) << endl;
return 0;
}
Pozivom funkcije exit() se zatvaraju sve datoteke koje su bile otvorene tijekom
izvoenja programa i unitavaju svi statiki objekti, redoslijedom obrnutim od
redoslijeda njihova stvaranja.
return x * x;
}
float f = 1.2;
int i = 9;
double d = 3.14159;
cout << kvadrat(f) << endl; // float kvadrat(float)
cout << kvadrat(i) << endl; // int kvadrat(int)
cout << kvadrat(d) << endl; // double kvadrat(double)
cout << kvadrat(1.4) << endl; // double kvadrat(double)
Uoimo da se u posljednjem pozivu realna konstanta implicitno tretira kao podatak tipa
double, te se shodno tome poziva funkcija s takvim tipom argumenta.
Argumenti predloka se mogu koristiti viekratno kao argumenti u deklaraciji
funkcije. Primjerice, sasvim openitu funkciju zamijeni() kojoj je zadaa zamjena
sadraja dvaju objekata, moemo deklarirati i definirati kao
kraticom LIFO, od engl. Last In, First Out posljednji unutra, prvi van. Pokaziva
stoga pokazuje na zadnji umetnuti podatak, tj. na vrh stoga. Kada procesor u strojnom
kdu naleti na poziv funkcije, on e pohraniti adresu iz pokazivaa instrukcija (to je
adresa naredbe na koju se izvoenje mora vratiti po povratku iz funkcije) na adresu na
koju pokazuje pokaziva stoga, te e sadraj pokazivaa stoga poveati. Ako se unutar
funkcije pozove neka druga funkcija, postupak e se ponoviti povratne adrese se
postepeno slau u stog, a pokaziva stoga se pritom uveava. Pri povratku iz zadnje
funkcije, preko pokazivaa stoga se oita posljednja povratna adresa, te se pokaziva
smanji. Postupak se ponavlja pri svakom povratku iz funkcija u nizu, te se stog prazni.
Budui da su podaci na stogu dohvatljivi iskljuivo preko pokazivaa stoga, treba
voditi rauna o redoslijedu punjenja i pranjenja stoga. Sreom, sve operacije vezane uz
stog sreuje prevoditelj, oslobaajui programera od prizemnog posla. Meutim, ono o
emu autor programa mora voditi rauna jest da se taj stog ne prepuni, jer e to izazvati
prekid rada programa. Na prvi pogled programer nema ni tu velikog utjecaja budui da
veliinu stoga odreuje prevoditelj. Istina, svaki prevoditelj ima mogunost da se
veliina stoga za neki program povea, ali to nije univerzalni lijek. Ipak, iz gornjih
razmatranja je jasno da mnogobrojni uzastopni pozivi funkcija optereuju stog. To se
posebno odnosi na glomazne rekurzije, gdje broj poziva funkcije moe postati vrlo
velik. Uz to valja znati da se osim povratnih vrijednosti, na stog pohranjuju i argumenti
funkcija (to je uostalom i razlogom zato se polja ne prenose po vrijednosti) i lokalni
objekti. Ako su objekti koji se prenose funkcijama veliki, stog e vrlo brzo biti ispunjen
do vrha.
215
8. Klase i objekti
Jezik C++ s kakvim smo se do sada upoznali ne razlikuje se znatno od bilo kojeg
drugog proceduralnog jezika. Ono to ovaj jezik ini posebno istaknutim jest
mogunost definiranja novih korisnikih tipova klasa, ime se uvodi koncept
objektnog programiranja.
U nastavku e biti objanjen nain na koji se klase deklariraju. Bit e objanjeni
pojmovi kao to su: podatkovni i funkcijski lanovi, prava pristupa, konstruktor,
destruktor. Takoer, bit e razjanjeni kljuni koncepti objektnog programiranja, kao to
su javno suelje i implementacija klase.
Kada je klasa jednom definirana, moe se pomou nje konstruirati neogranien broj
objekata koji se zatim mogu koristiti. Kako sve stvari koje nas okruuju imaju svoj
poetak i kraj, i ivot jednog objekta ima svoj poetak i svretak. Kada se objekt stvori,
potrebno mu je dati poetni oblik. Postupak koji opisuje kako e se to napraviti dan je u
specifikaciji klase. Funkcija koja inicijalizira objekt naziva se konstruktor (engl.
constructor). Kada objekt vie nije potreban, posebna funkcija klase pod imenom
destruktor (engl. destructor) uredno e unititi objekt.
Kako bismo ilustrirali injenicu da objekti nisu samo tvorevine raunalskih
strunjaka koji smiljaju naine kojima e zagoravati programerima ivot, evo primjera
koji pokazuje da se itav ivot uistinu odvija kroz interakciju objekata.
Zamislite maestra koji na klaviru izvodi Beethovenovu Mjeseevu sonatu. Takva
naoko jednostavna i svakidanja situacija zapravo je primjer jedinstvene suradnje
nekoliko stotina objekata. Kao prvo, tu je muziar koji izvodi glazbu, zatim tu su klavir i
note po kojima se svira. Pokuajmo poblie identificirati klase na temelju kojih su
stvoreni ti objekti. Muziar je primjerak klase ljudi. Definiciju ovjeka emo prepustiti
filozofima i teolozima, koji se dodue jo danas spore o tome to zapravo ini ovjeka i
kakvo je njegovo javno suelje, no s klavirom je stvar puno jasnija. Uzmimo da se radi o
Steinwayu. On je samo predstavnik ope klase klavira te im identificiramo Steinway
kao klavir odmah znamo ugrubo kako on izgleda. Osnovna svrha klavira je muziciranje
(iako e se moda miljenja ponekih sretnih vlasnika ovdje razlikovati). Njegovo javno
suelje je klavijatura pomou koje korisnik ostvaruje svrhu klavira. Javno suelje
klavira sastoji se od daljnjeg skupa objekata: tipki. Openita klasa tipki definira da
svaka tipka moe biti pritisnuta te da pritisak na nju proizvodi tono odreeni ton.
Nizovi tonova koje klavir proizvodi takoer spadaju u javno suelje klavira.
No, kako klavir ostvaruje svoje javno suelje? Da bismo doznali odgovor na to
pitanje, moramo zaviriti pod poklopac u implementaciju. To je podruje koje ne spada
u javno suelje jer je teko zamisliti da bi imalo ozbiljan muziar pokuao odsvirati
Mjeseevu sonatu direktno koristei implementacijske detalje, runo udarajui
batiima po icama. No i unutar implementacije se i dalje nalazimo u svijetu sainjenom
od objekata. Unutranjost klavira sastoji se od ica, koje opet mogu pripadati odreenoj
klasi ica, te od batia. Funkcioniranje implementacije zasniva se na interakciji ica i
batia. Tako bismo mogli nastaviti s nabrajanjem rastavljajui pojedine objekte na sve
manje i manje detalje, do kvarkova i gluona. to je dalje u implementaciji prepustit
emo fiziarima i njihovim akceleratorima estica.
Valja uoiti da neki drugi objekti ipak mogu imati pristup implementaciji klavira.
Zamislimo da klavir nije dobro ugoen. Loe sviranje klavira u tom sluaju je posljedica
pogreke u implemetaciji. Muziar e, nakon to preko njemu dostupnog javnog suelja
ustanovi da klavir ne radi kako treba, vjerojatno vrlo iznerviran, dignuti ruke i pozvati
ugaaa klavira ili to bi to purgeri rekli, klavir-timera. Muziar nita ne zna o
ustrojstvu klavira, te bi svojim mijeanjem u implementaciju vjerojatno samo unio
dodatni nered. No klavir-timer je osoba koja ima pristup unutranjosti (slika 8.1), te se
u terminologiji C++ jezika ona naziva prijateljem klase klavira (engl. friend of a class).
217
time proizvesti neki ton. Mogunost promatranja objekta kroz razliite klase u stablu
nasljeivanja se zove polimorfizam (engl. polymorphism) i tree je vrlo vano svojstvo
svih objektno orijentiranih jezika.
Nakon to smo definirali osnovna svojstva javnog suelja klavira, uoavamo da
smo iz cijele prie izostavili vane elemente javnog suelja, a to su pedale za
produljenje tona. Objektno orijentirano programiranje je vrlo koristan alat koji moe
znaajno poveati produktivnost programera, ali prije nego to se prijee na samo
kdiranje klase, za njegovu uspjenu primjenu potrebno je vie pripremnog rada.
Apstraktni model klavira je potrebno detaljno prouiti prilikom definicije klase kako bi
se precizno opisalo njegovo javno suelje i izbjegle pogreke u projektiranju. Na
primjer, ako na poetku zamislimo klavir tako da mu tipke smjestimo pod poklopac,
teko emo kasnije uspostaviti zadovoljavajue javno suelje. Ispravljanje ovakvih
fundamentalnih konstrukcijskih pogreaka u C++ jeziku je znatno sloenije nego u
klasinim proceduralnim jezicima.
Promotrimo ukratko akcije koje je potrebno provesti prilikom konstrukcije
pojedinog objekta. I dok opet nailazimo na probleme definiranja konstruktora klase
muziara, odnosno ljudi, s klavirima je stvar jasnija. Kada se klavir napravi, potrebno je
dovesti njegove implementacijske detalje u poetno stanje, odnosno pozvati klavir-
timera koji e ga odmah ugoditi. Ta akcija se definira kao konstruktor klase klavira.
Vjerujem da e matovit itatelj sam pronai mnogo ilustrativnih primjera za proceduru
koja unitava klavir kada on vie nije potreban, odnosno njegov destruktor. Postupak
koji e biti izabran kao najprikladniji vjerojatno ovisi o tome koliko je (ne)rado dotini
itatelj iao u muziku kolu.
Tri osnovna svojstva objektno orijentiranih jezika enkapsulacija, nasljeivanje i
polimorfizam vrlo su uspjeno ostvarena u C++ jeziku. Program se gradi od niza
objekata koji meusobno surauju, ba kao u naem primjeru s klavirom. Pri tome se
programer mora voditi odreenim formalizmom koji namee sam jezik, no osnovna bit
modeliranja pomou klasa je dosljedno provedena kroz sve elemente jezika.
class Klavir {
// ...
};
Naziv klase je identifikator koji mora biti jedinstven u vaeem podruju imena (engl.
name scope - bit e objanjeno kasnije). Objekti se mogu stvoriti tako da se navedu ime
klase iza kojeg se navedu imena objekata odvojena zarezima:
U ovom primjeru definirana su dva objekta Steinway i Petroff klase Klavir. Tijelo
klase moe sadravati podatkovne lanove, funkcijske lanove, deklaracije ugnijeenih
klasa i specifikacije prava pristupa.
class Prozor {
int koordX1, koordY1, koordX2, koordY2;
char *naziv;
Prozor *vlasnik;
};
Ova klasa se sastoji od etiri cjelobrojna lana koji pamte koordinate gornjeg lijevog i
donjeg desnog kuta prozora, zatim od pokazivaa na naziv prozora te od pokazivaa na
neki drugi objekt klase Prozor. Taj pokaziva pokazuje na objekt Prozor koji je
vlasnik promatranog prozora.
Svi su tipovi dozvoljeni prilikom deklaracije klase, s time da oni moraju biti
deklarirani do tog mjesta u programu. Nije mogue deklarirati kao lan klase objekt
klase koja se upravo definira, na primjer:
class Prozor {
int koordX1, koordY1, koordX2, koordY2;
char *naziv;
Prozor vlasnik; // neispravno
};
220
Ovakva rekurzivna deklaracija definira klasu Prozor koji sadri objekt klase Prozor
koji sadri objekt klase Prozor koji sadri... Oito se definicija protee u beskonanost
pa stoga takav objekt nije mogue prikazati u (jo uvijek) konanoj memoriji raunala.
Klasa ne moe sadravati samu sebe, ali moe sadravati pokaziva ili
referencu na objekt iste klase.
Klasa moe sadravati pokaziva ili referencu na element iste klase, kao u prvom
primjeru.
Ponekad je potrebno napraviti klasu koja e sadravati pokaziva na drugu klasu, dok
e ta druga klasa sadravati pokaziva na poetnu klasu. Proirimo primjerice klasu
Prozor pokazivaem na izbornik (engl. menu) koji je povezan s prozorom. Da bi klasa
izbornika funkcionirala, mora sadravati pokaziva na prozor koji posjeduje izbornik.
To se moe napraviti tako da se samo definira ime klase izbornika, zatim se definira
klasa Prozor, a potom se definira klasa Izbornik sa svim pripadajuim lanovima.
Takva vrsta deklaracije zove se deklaracija unaprijed (engl. forward declaration). Evo
primjera:
class Prozor {
// ovo je deklaracija klase Prozor
Izbornik *pokIzbornik; // pozivanje na deklaraciju
// unaprijed
// ...
};
class Izbornik {
// ovo je potpuna definicija klase Izbornik gdje se
// unaprijed definiranom imenu dodaju detalji
Prozor *pokProzor;
// ...
};
Kada je neka klasa samo deklarirana unaprijed, mogu se definirati iskljuivo reference i
pokazivai na objekte klase, ali ne i objekti klase. Naravno, u programu koji slijedi iza
potpune definicije klase mogu se definirati i objekti te klase. To znai da bi pokuaj
deklariranja objekta Izbornik u sklopu klase Prozor izazvao pogreku prilikom
prevoenja.
Prije nego to pristupimo lanu nekog objekta, moramo znati ijim lanovima
elimo pristupati. Neki objekt se referira pomou naziva objekta, reference ili
pokazivaa na objekt.
Kada objekt identificiramo pomou naziva objekta ili reference, za pristup
lanovima se koristi operator . (toka), tako da se s lijeve strane operatora navede naziv
objekta ili referenca, a s desne strane ime lana kojemu se pristupa. Deklarirat emo
klasu Racunalo te objekt i referencu tog tipa:
class Racunalo {
public:
int kBMemorije;
int brojDiskova;
int megahertza;
};
Racunalo mojKucniCray;
Racunalo &refRacunalo = mojKucniCray;
U gornjoj deklaraciji kljuna rije public oznaava da e lanovi klase biti javno
dostupni (o tome vie rijei u odsjeku 8.2.6). Sada bismo memoriju objekta
mojKucniCray proirili na sljedei nain:
mojKucniCray.kBMemorije = 64;
refRacunalo.brojDiskova = 5; // odnosi se na isti objekt
struct Vektor {
float ax, ay;
};
int main() {
struct Vektor v;
// ...
MnoziVektorSkalarom(v, 5);
return 0;
};
Iako je to sasvim ispravno rjeenje, ono ima niz nedostataka. Operacija mnoenja je
svojstvena samom vektoru. Naprotiv, gore navedena funkcija za mnoenje razdvaja
operaciju od objekta nad kojim se obavlja zato smo u funkciji za pristup pojedinim
elementima vektora morali koristiti v.ax ili v.ay. C++ prua elegantnije rjeenje ovog
problema:
class Vektor {
public:
float ax, ay;
void MnoziSkalarom(float skalar);
};
Vektor v;
// ...
v.MnoziSkalarom(5.0);
Strukture e biti obraene u sljedeem poglavlju, pa se itatelj koji nije vian jeziku C ne
mora previe zamarati ovim primjerom.
223
normala.MnoziSkalarom(6.7);
a2.MnoziSkalarom(4.0);
Naziv funkcijskog lana mora biti jedinstven u podruju imena unutar klase.
MnoziSkalarom(7.6);
sam za sebe bez operatora . nema smisla, jer nije jasno ijim ax i ay se pristupa u toku
izvoenja. Kako ime funkcije nije vidljivo izvan klase, ovakva naredba e rezultirati
pogrekom u prevoenju Nepoznata funkcija MnoziSkalarom(float).
U naem primjeru unutar definicije klase naveli smo samo prototip funkcije
MnoziSkalarom(), dok smo njenu definiciju naveli izvan klase. Kako imena funkcije
nisu vidljiva izvan klase, prilikom definiranja funkcije koristili smo :: (dvije dvotoke)
operator za razluivanje podruja imena (engl. scope resolution operator). Cijelo ime
funkcijskog lana je zapravo Vektor::MnoziSkalarom(), pa smo takvo ime morali
navesti prilikom definicije funkcije. Valja primijetiti da smo je mogli takoer pozivati
navodei puno ime
v.Vektor::MnoziSkalarom(5.0);
Puno ime lanova deklariranih unutar klase ukljuuje naziv klase koji se od
naziva lana razdvaja operatorom ::.
224
Iako ispravno, ovakvo pisanje je sloenije. Objekt v je klase Vektor, pa prevoditelj sam
zakljuuje da je funkcija MnoziSkalarom() iz klase Vektor.
Vektor::MnoziSkalarom(5, &v);
Velika prednost C++ jezika lei upravo u tome to prevoditelj skriva this od korisnika
i sam obavlja pristupanje preko pokazivaa. U veini primjena koritenje te kljune
rijei nee biti potrebno. Ipak, u nekim specifinim sluajevima bit e potrebno unutar
funkcijskog lana saznati adresu objekta za koji je lan pozvan. To se tada moe uiniti
pomou kljune rijei this. Vano je napomenuti da se this ne smije deklarirati kao
formalni parametar funkcijskog lana. On je automatski dostupan u svakom lanu te ga
nije potrebno posebno navoditi.
Kljuna rije this se esto koristi kada je potrebno vratiti pokaziva ili referencu
na objekt. Da bismo to demonstrirali, proirit emo klasu Vektor funkcijom
ZbrojiSa() koja zbraja vektor s nekim drugim vektorom. Promijenit emo povratni
tip funkcije na Vektor &, ime e funkcijski lan vraati referencu na objekt za koji je
pozvan.
class Vektor {
public:
float ax, ay;
Vektor &MnoziSkalarom(float skalar);
Vektor &ZbrojiSa(float zx, float zy);
};
ax *= skalar;
ay *= skalar;
return *this;
}
Vektor v;
v.ax = 4;
v.ay = 5;
v.MnoziSkalarom(4).ZbrojiSa(5, -7);
class ElementListe {
private:
ElementListe *Sljedeci, *Prethodni;
public:
void Dodaj(ElementListe *Novi);
// ...
};
class Vektor {
public:
float ax, ay;
void MnoziSkalarom(float skalar) {
ax *= skalar;
ay *= skalar;
}
};
Razlozi radi kojih je poeljno funkcijske lanove uiniti umetnutima su isti kao i za
obine funkcije, te su detaljno opisani u odsjeku 5.7, a svode se na bre izvoenje kda
te (u principu) krai prevedeni kd.
Ako je nezgodno smjestiti umetnutu funkciju u samu definiciju klase, isti efekt se moe
postii kljunom rijei inline kao u sljedeem primjeru.
class Vektor {
public:
float ax, ay;
inline void MnoziSkalarom(float skalar);
};
Rezultat e prilikom ovakve deklaracije biti isti. Kljuna rije inline se mogla
ponoviti prije definicije funkcijskog lana. Umetnuti funkcijski lanovi su vrlo vani u
C++ jeziku, jer se esto primjenjuju za postavljanje i itanje podatkovnih lanova koji
nisu javno dostupni (vidi sljedei odsjeak). Prevoditelj koji ne podrava dovoljno
dobro umetnute funkcijske lanove moe vrlo lako generirati kd znaajno degradiranih
performansi.
definirano tom rijei. Zatim se moe navesti neka druga od gore navedenih kljunih
rijei, iza koje slijede elementi koji imaju drukije pravo pristupa. Evo primjera:
class Pristup {
public:
int a, b;
void Funkcija1(int brojac);
private:
int c;
protected:
int d;
int Funkcija2();
};
Prava pristupa odreuju koji e lanovi klase biti dostupni izvan klase, koji iz
naslijeenih klasa, a koji samo unutar klase. Time programer tono odreuje publiku
za pojedini dio objekta.
Javni pristup se dodjeljuje kljunom rijei public. lanovima koji imaju javni
pristup moe se pristupiti izvan klase. Cjelobrojni elementi a i b, te Funkcija1() se
mogu pozvati tako da se definira neki objekt klase Pristup te im se pomou
operatora . pristupi.
lan c ima privatni pristup dodijeljen kljunom rijei private. U vanjskom
programu te u klasama koje su naslijeene od klase Pristup on nije dostupan
njemu se moe pristupiti samo preko funkcijskih lanova klase Pristup.
Zatieni pristup se odreuje kljunom rijei protected te se time ograniava
mogunost pristupa lanu d i funkcijskom lanu Funkcija2(). Njima se ne moe
pristupiti iz vanjskog programa preko objekta. Dostupne su samo u funkcijskim
lanovima klase Pristup i klasama koje su naslijeene od klase Pristup.
Nasljeivanjem se bavi posebno poglavlje 9 ove knjige.
Redoslijed navoenja prava pristupa je proizvoljan, a pojedine vrste pristupa je mogue
ponavljati. Dozvoljeno je primjerice definirati prvo elemente s javnim, zatim elemente s
privatnim te ponovo elemente s javnim pristupom. lanovi navedeni u deklaraciji klase
odmah nakon otvorene vitiaste zagrade do prve kljune rijei za specifikaciju prava
pristupa (ili do kraja klase ako prava pristupa uope nisu navedena) imaju privatni
pristup. Ako se negdje u programu pokua nedozvoljeno pristupiti privatnom ili
zatienom elementu, dobit e se pogreka prilikom prevoenja Za element xx nema
prava pristupa.
Da bismo ilustrirali koritenja prava pristupa, stvorit emo objekt x klase Pristup te
emo pokazati kojim se podatkovnim lanovima iz kojeg dijela programa moe
pristupiti.
228
Pristup x;
int main() {
// ovo je OK: a i b imaju javni pristup
x.a = x.b = 6;
// i ovo je OK: lan Funkcija1(int) takoer
// ima javni pristup
x.Funkcija1(1);
// ovo nije OK: c ima privatni pristup te mu se ne
// moe pristupiti izvana
cout << x.c << endl;
// niti ovo ne valja: Funkcija2() ima zatieni
// pristup
x.Funkcija2()
return 0;
}
class Macka {
int ReciBrojGodina() { return brojGodina; }
private:
int brojGodina;
};
(Odgovor lei u starom pravilu bon-tona, koje kae da se pripadnice ljepeg spola ne
pita za godine!)
Zadatak. Deklarirajte klasu Pravac s privatnim lanovima k i l koji e sadravati
koeficijent smjera i odsjeak pravca na osi y. Dodajte javne funkcijske lanove
UpisiK() i CitajK(), te UpisiL() i CitajL() za pristup podatkovnim lanovima.
jednostavno preraditi, dok ostatak kda ne treba dirati on vidi samo javno suelje
objekta koje ostaje neizmijenjeno.
Da bismo to pojasnili, vratimo se na primjer s vektorima te preradimo klasu tako da
njena implementacija bude neovisna od javnog suelja:
class Vektor {
private:
float ax, ay;
public:
void PostaviXY(float x, float y) { ax = x; ay = y; }
float DajX() { return ax; }
float DajY() { return ay; }
void MnoziSkalarom(float skalar);
};
class Vektor {
friend Vektor ZbrojiVektore(Vektor a, Vektor b);
private:
float ax, ay;
public:
// ...
};
Deklaracija neke funkcije kao prijatelja poinje kljunom rijei friend iza koje se
navede puni prototip funkcije kojoj se dodjeljuju prava pristupa. Nije potrebno
deklarirati funkciju prije nego to se ona uini prijateljem. Tako je u naem primjeru
sama funkcija deklarirana i definirana nakon deklaracije klase. Ako je potrebno vie
funkcija uiniti prijateljem, tada se svaka funkcija navodi zasebno. Nije mogue nakon
jedne kljune rijei friend navesti vie deklaracija. Iako se deklaracije prijatelja mogu
pojaviti bilo gdje u tijelu klase, uvrijeeno je pravilo da ih se navodi odmah iza
deklaracije klase.
Prava pristupa je mogue dodijeliti i pojedinim funkcijskim lanovima drugih klasa.
Tako je mogue definirati klasu Matrica koja e sadravati funkcijski lan koji mnoi
matricu vektorom. Klasa Vektor e dodijeliti prava pristupa funkcijskom lanu
Matrica::MnoziVektorom() na sljedei nain:
class Vektor;
231
class Matrica {
private:
float a11, a12, a21, a22;
public:
// ...
Vektor MnoziVektorom(Vektor &v);
};
class Vektor {
friend Vektor Matrica::MnoziVektorom(Vektor &v);
// ...
};
Prilikom dodjele prava pristupa funkcijskom lanu klase postoji pravilo da se pravo ne
moe dodijeliti ako funkcijski lan nije ve prethodno deklariran. Zato je deklaracija
klase Matrica prethodila deklaraciji klase Vektor, ime je postignuto da u trenutku
deklariranja klase Vektor funkcijski lan kojem se ele dodijeliti prava ve bude
deklariran. Definicija funkcijskog lana Matrica::MnoziVektorom() je stavljena
nakon definicije klase Vektor iz dva razloga: struktura klase Vektor mora biti poznata
da bi se moglo pristupati njegovim lanovima te klasa Vektor mora dodijeliti pravo
pristupa funkcijskom lanu kako bi mogao pristupiti privatnim lanovima.
Ponekad je potrebno dodijeliti pravo pristupa svim funkcijskim lanovima jedne
klase. Tada se cijela klase moe deklarirati prijateljem druge klase. U tom sluaju ta
klasa ima pristup svim privatnim i zatienim tipovima, pobrojenjima i ugnijeenim
klasama. To se ini tako da se iza kljune rijei friend navede kljuna rije class za
kojom slijedi naziv klase kojoj se prava dodjeljuju. Klasa kojoj se dodjeljuju prava
pristupa mora u trenutku dodjele biti deklarirana, makar i nepotpuno (unaprijed), kao u
primjeru:
class Matrica;
class Vektor {
friend class Matrica;
// ...
};
Svi privatni i zatieni tipovi definirani unutar klase Vektor mogu se sada koristiti i u
definiciji funkcijskih lanova klase Matrica.
Unutar deklaracije friend se funkcija moe i definirati tada e ona biti umetnuta.
Time je postignuta slinost s funkcijskim lanovima koji se takoer mogu definirati u
232
class Vektor {
friend Vektor ZbrojiVektore(Vektor a, Vektor b) {
Vektor c;
c.ax = a.ax + b.ax;
c.ay = a.ay + b.ay;
return c;
}
private:
float ax, ay;
public:
// ...
};
Vektor strelicnik;
Vektor normala = strelicnik, polje[10];
Vektor *pokStrelicnik = &strelicnik, &refNormala = normala;
233
8.4.1. Konstruktor
Kada se stvori novi objekt neke klase, njegovi lanovi su neinicijalizirani. Vrijednosti
pojedinih lanova ovise o podacima koji su se sluajno nali na tom memorijskom
prostoru prije nego to je objekt stvoren. Kako je rad s podacima ija je vrijednost
odreena stohastiki potencijalno vrlo opasan, C++ nudi elegantno rjeenje. Svaka klasa
moe imati konstruktorski funkcijski lan koji se automatski poziva prilikom stvaranja
objekata. Konstruktor (engl. constructor) se deklarira kao funkcijski lan koji nema
specificiran povratni tip te je imena identinog nazivu klase.
Konstruktor ima isto ime kao i klasa kojoj pripada, te nema povratni tip.
class Vektor {
private:
float ax, ay;
public:
// definirana su dva konstruktora:
Vektor();
234
Vektor(float x, float y) { ax = x; ay = y; }
void PostaviXY(float x, float y) { ax = x; ay = y; }
float DajX() { return ax; }
float DajY() { return ay; }
void MnoziSkalarom(float skalar);
};
Vektor::Vektor() {
ax = 0;
ay = 0;
}
#include <assert.h>
class Tablica {
private:
int *Elementi;
int BrojElem, Duljina;
public:
Tablica();
Tablica(int BrElem);
void PovecajNa(int NovaDulj);
void DodajElem(int Elt);
void BrisiElem(int Poz);
};
Tablica::Tablica() :
Elementi(new int[10]),
BrojElem(0), Duljina(10) {
assert(Elementi != NULL);
}
Tablica::Tablica(int BrElem) :
Elementi(new int[BrElem]),
BrojElem(0), Duljina(BrElem) {
assert(Elementi != NULL);
}
#define NDEBUG
Tablica::Tablica(int BrElem) :
Elementi(new int[BrElem]),
BrojElem(0), Duljina(BrElem) {
// ...
Ovakav nain inicijalizacije nije obavezan, ali ima prednosti prilikom izvoenja
programa. Postavljanje vrijednosti se moe obaviti i unutar tijela konstruktora. No ako
se izostavi inicijalizacija nekog lana, C++ prevoditelj e sam umetnuti kd za
inicijalizaciju. U tom sluaju se inicijalizacija obavlja dva puta: jednom zato to nije
eksplicitno navedena poetna vrijednost lana i drugi put u samom konstruktoru. Jasno
je da je itav kd zbog toga sporiji.
No postoji jedna zamka koja neupuenom programeru moe zadati glavobolju, a to je
redoslijed inicijalizacije. Naime, pravila C++ jezika kau da se podatkovni lanovi ne
inicijaliziraju redoslijedom kojim su navedeni u inicijalizacijskoj listi, nego
redoslijedom kojim su navedeni u samoj deklaraciji. Tako konstruktor
Tablica::Tablica() : BrojElem(10),
Elementi(new int[BrojElem]),
Duljina(BrElem) { /* ... */ }
class Par {
Vektor prvi, drugi;
public:
Par(float x1, float y1, float x2, float y2) :
drugi(x2, y2), prvi(x1, y1) {}
// ...
};
Podatkovni lanovi ugraenih tipova (int, float, char) ne moraju biti navedeni u
inicijalizacijskoj listi, te e u tom sluaju ostati neinicijalizirani.
Konstruktor se poziva i kod stvaranja dinamikih objekata. Memorija za dinamiki
objekt se dodjeljuje operatorom new kojemu se kao argument navede tip koji se eli
alocirati. Kao rezultat se vraa pokaziva na tip koji je alociran:
jednoznano odrediti koji se konstruktor poziva. Operator new poziva konstruktor nakon
to alocira potrebnu koliinu memorije za smjetaj objekta. Ako alokacija memorije ne
uspije, konstruktor se ne izvodi, a new operator vraa nul-pokaziva.
Zadatak. Klasu Pravac iz zadatka na stranici 228 proirite konstruktorom koji e
pravac inicijalizirati pomou dva objekta klase Tocka (vidi zadatak na stranici 232).
Konstruktor mora imati sljedeu deklaraciju:
class Podrazumijevani {
int broj;
Vektor v;
};
class ImaKonstruktor {
int i;
public:
ImaKonstruktor(int ii) : i(ii) {}
};
class Parametri {
public:
Parametri(int a = 0, float b = 0.0, char c = ' ');
// ...
};
konstruktor klase je ujedno i podrazumijevani konstruktor. Kako sva tri parametra imaju
podrazumijevanu vrijednost te ih se moe izostaviti, prevoditelj ne moe ustanoviti
razliku izmeu tog konstruktora i konstruktora bez parametara. Pokuaj posebne
definicije podrazumijevanog konstruktora zavrio bi pogrekom prilikom prevoenja jer
te dvije funkcije nije mogue razlikovati.
Tablica A Tablica B
ELEMENTI
pokazuju na isti komad memorije. Zbog toga se svaka promjena u tablici A odraava i na
tablicu B. Najopasnija popratna pojava ovakvog kopiranja objekata je u tome to objekt
B moe biti uniten (na primjer zavri li funkcija u kojoj je on bio alociran kao
automatski objekt) ime e se osloboditi memorija na koju pokazuje pokaziva
Elementi. No pokaziva u objektu A e i dalje pokazivati na isto mjesto koje je sada
nevaee. Pokuaj izmjene tablice A e promijeniti sadraj memorije na koju A
pokazuje, no ta memorija je u meuvremenu moda dodijeljena nekom drugom objektu.
Vidimo da ovakav pristup vodi u sveopu zbrku, a programi pisani na ovakav nain ne
mogu funkcionirati ispravno. Ovakvo kopija se esto naziva plitka kopija (engl. shallow
copy), a situacija u kojoj pokazivai dvaju objekata pokazuju na isto memorijsko
podruje na engleskom se naziva pointer aliasing.
Zato emo dodati konstruktor kopije klasi Tablica koji e alocirati zasebno
memorijsko podruje za svaki novi objekt te prepisati sadraj tablice. Time emo dobiti
tzv. duboku kopiju (engl. deep copy) objekta:
class Tablica {
// ...
public:
Tablica(const Tablica &refTabl);
};
Uz najbolju volju, ovaj termin nismo nikako mogli suvislo prevesti. Na engleskom alias znai
lano ime, no termin predstavljanje pokazivaa lanim imenom e prije izazvati provale
smijeha, no opisati problem.
243
Elementi[i] = refTabl.Elementi[i];
}
class Par {
public:
Vektor &refPrvi, &refDrugi;
// nastavak slijedi ...
};
emo pogreku prilikom prevoenja, jer reference ne smiju ostati neinicijalizirane. Evo
kako to izgleda na primjeru klase Par:
class Par {
public:
Vektor &refPrvi, &refDrugi;
Par(Vektor &v1, Vektor &v2) : refPrvi(v1), refDrugi(v2) {}
};
Konstruktor klase Par kao parametar ima reference na objekte klase Vektor te se
reference refPrvi i refDrugi inicijaliziraju tako da pokazuju na parametre.
Obavezno je bilo staviti v1 i v2 kao reference: u suprotnom bi se prilikom poziva
konstruktora stvarale privremene kopije stvarnih parametara, a reference bi se
inicijalizirale adresom tih privremenih kopija. Nakon zavretka konstruktora kopije bi se
unitile, a reference bi pokazivale na memoriju u kojoj vie nema objekta. U nastavku je
dan primjer alociranja objekta klase Par i njegove ispravne inicijalizacije:
Na sline probleme nailazimo ako klasa sadrava konstantne lanove: njih je takoer
potrebno inicijalizirati u konstruktoru, a vrijednost im se kasnije ne moe mijenjati. Na
primjer, neka elimo klasu Tablica promijeniti tako da ona sadri lan maxDuljina
koji oznaava maksimalan broj lanova u tablici. Taj lan je poeljno uiniti
konstantnim, tako da se njegova vrijednost ne moe kasnije mijenjati. Evo deklaracije
klase:
class Tablica {
private:
int *Elementi;
int BrojElem, Duljina;
const int maxDuljina;
public:
Tablica();
Tablica(int BrElem, int duljina);
// ostali lanovi se ne mijenjaju
};
Tablica::Tablica() : maxDuljina(50),
Elementi(new int[10]),
BrojElem(0), Duljina(10) {
Prvi konstruktor inicijalizira lan maxDuljina na vrijednost 50, dok ga drugi postavlja
na proslijeenu vrijednost.
class Vektor {
private:
float ax, ay;
Vektor();
public:
Vektor(float x, float y);
void NekaFunkcija();
// ... ostatak klase
};
void Vektor::NekaFunkcija() {
Vektor priv; // OK
// ...
}
int main() {
Vektor vektor1, vektor2(12, 3);
// vektor1 javlja pogreku
// ...
return 0;
}
246
8.4.7. Destruktor
Kada objekt vie nije potreban, on se uklanja iz memorije. Pri tome se poziva destruktor
(engl. destructor) koji je zaduen za oslobaanje svih resursa dodijeljenih objektu. To je
takoer funkcija koja ima naziv identian nazivu klase ispred kojeg stoji znak ~ (tilda).
Tako destruktor za klasu Tablica ima naziv ~Tablica. Destruktor, kao i konstruktor,
nema povratni tip. Destruktor se automatski poziva u sljedeim situacijama:
za automatske objekte na kraju bloka u kojem je objekt definiran (kraj bloka je
oznaen zatvorenom vitiastom zagradom),
za statike i globalne objekte nakon izlaska iz funkcije main(),
za dinamike objekte prilikom unitenja dinamikog objekta operatorom delete.
Destruktor ne moe biti deklariran s argumentima pa tako ne moe niti biti
preoptereen. Takoer nema specificiran tip koji se vraa iz funkcije. Kao to
konstruktor ne alocira memoriju za objekt nego se poziva nakon to je objekt alociran te
inicijalizira objekt, tako destruktor ne dealocira memoriju nego se poziva prije nego to
se memorija zauzeta objektom oslobodi te obavlja deinicijalizaciju objekta.
Destruktor ima naziv jednak nazivu klase ispred kojeg stoji znak ~ (tilda) te
nema povratnog tipa. Za svaku klasu moe postojati samo jedan destruktor.
class Tablica {
// ...
public:
~Tablica();
// ...
};
247
Tablica::~Tablica() {
delete [] Elementi;
}
Operator delete se moe bez opasnosti primijeniti na pokaziva koji ima vrijednost
nula. U tom sluaju se destruktor ne poziva.
Nema ogranienja na sadraj destruktora. On moe obaviti bilo kakav zadatak
potreban da se objekt u potpunosti ukloni iz memorije. Najee se u destruktoru
oslobaaju svi objekti dinamiki alocirani u konstruktoru. esto je prilikom traenja
pogreaka u programu korisno u destruktor staviti naredbu koja e ispisati poruku o
pozivanju destruktora, na primjer:
Tablica::~Tablica() {
delete [] Elementi;
#ifndef NDEBUG
cout << "~Tablica()" << endl;
#endif
}
248
class Kompleksni {
private:
float r, i;
public:
Kompleksni(float rr, float ii) : r(rr), i(ii) {}
float DajRe() { return r; }
float DajIm() { return i; }
void Postavi(float rr, float ii) { r = rr; i = ii; }
};
int main() {
249
U gornjem programu funkcija sumiraj() zbraja sve argumente koji su joj proslijeeni
prilikom poziva. Suma se pamti u statikom objektu suma, koji je deklariran unutar
funkcije kako bi se onemoguilo vanjskom programu da mijenja sumu direktno.
Takoer, potrebno je objekt deklarirati statikim kako bi se suma pamtila prilikom
svakog poziva funkcije. Takoer, definiran je i globalni objekt a.
Objekti a i suma su definirani klasom, te je prilikom njihovog nastajanja potrebno
provesti postupak konstrukcije. Uostalom, vidi se da oba objekta u svojim deklaracijama
imaju parametre koji slue za poetno postavljanje objekta. To postavljanje se moe
provesti jedino u konstruktoru. Takoer, prilikom zavretka programa, oba objekta e
biti potrebno unititi potrebno je pozvati destruktor.
Dakle, za sve statike i globalne objekte konstruktor e se izvesti prije upotrebe bilo
kojeg objekta ili funkcije u datoteci izvornog kda. U praksi, to znai da e 99,99%
prevoditelja konstruktore pozvati prije ulaska u funkciju main(). Destruktori e se
pozvati nakon to funkcija main() zavri ili ako se program naprasno zavri pozivom
funkcije exit().
Konstruktori e se izvesti u redoslijedu deklaracija unutar datoteke izvornog kda, a
destruktori e se izvesti obrnutim redoslijedom. Standard ne definira redoslijed
pozivanja konstruktora u programima koji imaju vie datoteka izvornog kda, pa je
dobra navika ne oslanjati se u programima na redoslijed inicijalizacije objekata iz
razliitih datoteka.
Vektor polje[3];
delete [] polje;
Ponekad je ba izriito potrebno alocirati polje vektora tako da je svaki vektor zasebno
inicijaliziran. Operator new nudi rjeenje: mogue je smjestiti svaki novostvoreni objekt
na tono odreenu adresu. To se postie tako da se nakon kljune rijei new prije
specifikacije tipa u zagradama navede pokaziva na char koji odreuje mjesto na koje
se objekt smjeta, na primjer:
char *pokMjesto;
// inicijaliziraj pokMjesto
Memorija na koju taj pokaziva pokazuje mora biti alocirana na neki drugi nain. Sam
operator new ne obavlja alokaciju memorije nego samo poziva konstruktor za element i
vraa pokaziva na njega. Da bismo mogli stvarati objekte na odreenom mjestu u
memoriji, potrebno je ukljuiti datoteku new.h, jer ona sadri deklaraciju
preoptereene verzije operatora new koji omoguava alokaciju objekta na eljenom
mjestu u memoriji. Evo kompletnog primjera:
#include <new.h>
Alokacija polja se obavlja tako da se najprije zauzme ukupna memorija potrebna za sve
vektore, a zatim se u nju umeu pojedini vektori koji se inicijaliziraju pomou
konstruktora s dva argumenta. Ovako alocirano polje nije mogue dealocirati
naredbom
delete [] pok;
prevoditelj nee moi pozvati destruktore za lanove polja. Dealokacija polja se mora
obaviti eksplicitnim pozivanjem destruktora za svaki lan polja:
class NekaKlasa {
void FunkcijskiClan(int, float) const;
};
konstantnih funkcijskih lanova. Ukoliko se pokua pozvati obini funkcijski lan, dobit
e se pogreka prilikom prevoenja.
Da bismo objasnili koritenje konstantnih funkcijskih lanova modificirajmo klasu
Vektor tako da funkcijske lanove koji ne mijenjaju vrijednost objekta definiramo
konstantnima:
class Vektor {
private:
float ax, ay;
public:
Vektor() : ax(0), ay(0) {}
Vektor(float x, float y) : ax(x), ay(y) {}
void PostaviXY(float x, float y) { ax = x; ay = y; }
float DajX() const { return ax; } // konstantni
float DajY() const { return ay; } // funkcijski lanovi
};
254
int main() {
const Vektor v(12, 3);
cout << "X: " << v.DajX() << endl; // OK
cout << "Y: " << v.DajY() << endl; // OK
v.PostaviXY(4, 5); // pogreka
return 0;
}
class Vektor {
// ...
void PostaviXY(float x, float y);
void PostaviXY(float, float) const;
};
ogradom da klasa mora imati konstruktor. Evo kako to izgleda u funkcijskim lanovima
DajX() i DajY():
class Vektor {
private:
float ax, ay;
int broji;
public:
Vektor() : ax(0), ay(0), broji(0) {}
Vektor(float x, float y) : ax(x), ay(y), broji (0) {}
void PostaviXY(float x, float y) { ax = x; ay = y; }
float DajX() const;
float DajY() const;
};
Nedavno je u standard C++ jezika ubaena kljuna rije mutable koja omoguava jo
elegantnije rjeenje gornjeg problema. Naime, pojedine nekonstantne nestatike
podatkovne lanove moemo deklarirati tako da ispred deklaracije lana umetnemo tu
kljunu rije. Ti lanovi e tada i u konstantnim objektima biti nekonstantni, te e ih se
moi mijenjati u konstantnim funkcijskim lanovima. Gornji primjer brojanja pristupa
se sada moe ovako rijeiti:
class Vektor {
private:
float ax, ay;
mutable int broji; // lan koji nee biti konstantan
// niti u konstantnom objektu
public:
Vektor() : ax(0), ay(0), broji(0) {}
Vektor(float x, float y) : ax(x), ay(y), broji (0) {}
void PostaviXY(float x, float y) { ax = x; ay = y; }
float DajX() const;
float DajY() const;
};
class NekaKlasa {
public:
int neDiraj;
mutable int radiStoHoces;
};
// ...
const NekaKlasa obj; // objekt obj je konstantan
obj.neDiraj = 100; // pogreka: obj je konstantan
obj.radiStoHoces = 101; // OK: lan je mutable
class PrimjerZaVolatile {
public:
void Funkcija1(); // obina funkcija
void Funkcija2() volatile; // volatile funkcija
};
int main() {
volatile PrimjerZaVolatile a;
a.Funkcija1(); // pogreka: za volatile objekte se moe
// pozvati samo volatile funkcijski lan
a.Funkcija2(); // OK: objekt je volatile i funkcijski
// lan je volatile
return 0;
}
Rjeenje problema daju statiki elementi klase. U tom sluaju varijabla se ne nalazi
u globalnom podruju imena nego u podruju imena unutar klase te joj je pristup
kontroliran, na primjer:
#include <iostream.h>
class Brojeni {
private:
static int Brojac; // statiki podatkovni lan
int MojBroj;
public:
Brojeni();
~Brojeni();
};
Brojeni::Brojeni() : MojBroj(Brojac++) {
cout << "Stvoren element br.: " << MojBroj << endl;
}
Brojeni::~Brojeni() {
cout << "Unisten element br.: " << MojBroj << endl;
}
int Brojeni::Brojac = 0;
Podatkovni lan Brojac nije sadran u svakom objektu klase nego je globalan za cijelu
klasu. Takav lan se deklarira tako da se prije specifikacije tipa ubaci kljuna rije
static. Za statike lanove vrijede standardna pravila pristupa. U gornjem sluaju je
podatkovni lan Brojac deklariran privatnim, te mu se moe pristupiti samo unutar
klase.
Deklaracija statikog lana unutar klase slui samo kao najava ostatku programa da e
negdje u memoriji postojati jedan lan koji e biti zajedniki svim objektima klase. Sam
lan time nije smjeten u memoriju. Inicijalizacija lana se mora eksplicitno obaviti
izvan klase tada se odvaja memorijski prostor i lan se inicijalizira. U gornjem je
primjeru to je uinjeno ovom naredbom:
int Brojeni::Brojac = 0;
Ona odvaja potreban memorijski prostor i inicijalizira lan na nulu. U C++ jeziku se
deklaracija klase esto izdvaja u zasebnu datoteku zaglavlja koja se ukljuuje u sve
segmente programa koji koriste klasu. Tako e deklaracija klase svim dijelovima
programa objaviti da e u memoriji postojati jedan zajedniki lan. U jednoj datoteci se
mora nai i inicijalizacija tog lana ili e se u protivnom dobiti pogreka prilikom
259
povezivanja kako lan nije pronaen u memoriji. Deklaraciju klase Brojeni moemo
smjestiti u datoteku broj.h, a definiciju funkcijskih i statikih lanova u datoteku
broj.cpp. Ako bi se inicijalizacija statikih lanova takoer smjestila u datoteku
zaglavlja, prilikom svakog ukljuenja te datoteke pretprocesorskom direktivom
#include stvorio bi se po jedan lan i na kraju bismo prilikom povezivanja dobili
poruku o pogreki jer je lan Brojeni::Brojac viestruko definiran.
Iz funkcijskih lanova klase statikom se lanu pristupa na isti nain kao i obinom
lanu. Ukoliko statiki lan ima javno pravo pristupa, moe mu se pristupiti i iz ostalih
dijelova programa, i to na dva naina. Prvi je nain pomou objekta: navede se objekt,
zatim operator . pa onda naziv lana. Tada sam objekt slui iskljuivo tome da se
identificira klasa u kojoj je lan definiran. Drugi nain je pristup bez objekta, samo
navodei klasu. Ispred samog lana se mora navesti naziv klase i operator :: da bi se
naznailo u kojoj se klasi nalazi lan kojemu se pristupa. Opi oblik sintakse za pristup
lanovima je naziv_klase :: ime_lana, na primjer:
class Brojeni {
public:
static int Brojac; // javni statiki lan
int MojBroj;
// ...
void IspisiBrojace();
};
void Brojeni::IspisiBrojace() {
cout << "Brojac: " << Brojac << endl;
// ovo je pristup iznutra
cout << "Moj broj: " << MojBroj << endl;
}
int main() {
Brojeni bb;
Brojeni::Brojac = 5; // pristup izvana operatorom ::
bb.Brojac = 5; // isto kao i prethodna naredba
bb.IspisiBrojace();
return 0;
}
Javnim statikim lanovima se moe pristupati direktno (navodei cijelo ime lana), jer
za cijelu klasu postoji samo jedan statiki lan. Naprotiv, obinim podatkovnim
lanovima se ne moe direktno pristupati, tako da pridruivanje
Brojeni::MojBroj = 5;
nije ispravno. Ova naredba izaziva pogreku pri prevoenju jer MojBroj postoji samo u
kontekstu odreenog objekta pa mu se moe pristupiti jedino pomou objekta:
bb.MojBroj = 5;
260
No dozvoljeno je napisati
bb.Brojac = 5;
Objekt bb sada prevoditelju samo kazuje naziv klase u kojoj je statiki lan smjeten.
Gornja naredba nita ne pridruuju objektu bb, ve pristupa statikom lanu.
Statiki podatkovni lanovi se dodatno razlikuju od obinih lanova po tome to
klasa moe sadravati statiki objekt te klase. U sluaju obinih lanova, klasa moe
sadravati samo pokazivae i reference na objekte te klase, na primjer:
class Obj {
// ovo je OK jer je lan statiki:
static Obj statickiClan;
// i ovo je OK jer su lanovi referenca i pokaziva:
Obj &ref, *pok;
// ovo nije OK:
Obj nestatickiClan;
};
int a;
class Param {
int a;
static int b;
public:
// ovo je OK jer je lan statiki:
void func1(int = b);
// i ovo je OK jer se odnosi na globalni a:
void func2(int = ::a);
// ovo je greka jer se odnosi na nestatiki lan a:
void func3(int = a);
};
class Brojeni {
public:
static int Brojac;
int MojBroj;
// ...
static int VrijednostBrojaca() { return Brojac; }
};
// ...
int main() {
Brojeni::Brojac = 5;
cout << Brojeni::VrijednostBrojaca() << endl;
return 0;
}
Iz primjera se vidi da prilikom poziva statike funkcije nije potrebno navoditi objekt
pomou kojeg se funkcija poziva, nego se jednostavno navede cijelo ime funkcije (u
gornjem primjeru niti jedan objekt klase nije bio deklariran). Vano je uoiti da statike
funkcije, slino statikim podatkovnim lanovima, imaju puno ime oblika
naziv_klase :: ime_funkcije. Ime funkcije pripada podruju imena klase, a ne
globalnom podruju imena. Zato je potrebno prilikom referenciranja na funkciju navesti
ime klase. Za statike lanove klasa vrijede pravila pristupa, pa ako funkciju elimo
pozivati izvan objekata klase, ona mora imati javni pristup.
Statiki funkcijski lanovi se mogu pozvati bez objekta, navodei puno ime
funkcije. Kompletno ime obuhvaa naziv klase, odvojen operatorom :: od
naziva lana.
Statiki funkcijski lanovi se, kao i statiki podatkovni lanovi, mogu pozvati i pomou
objekta klase. Sljedei poziv je stoga potpuno ispravan:
int main() {
Brojeni bb;
Brojeni::Brojac = 5;
cout << bb.VrijednostBrojaca() << endl;
return 0;
}
Kako se statike funkcije ne pozivaju pomou objekta, one niti ne sadravaju implicitan
this pokaziva. Svaka upotreba te kljune rijei u statikoj funkciji e rezultirati
pogrekom prilikom prevoenja. Kako nema this pokazivaa, jasno je da i pokuaj
pristupa bilo kojem nestatikom podatkovnom ili funkcijskom lanu klase rezultira
262
pogrekom prilikom prevoenja. Statiki funkcijski lanovi ne mogu biti deklarirani kao
const ili volatile.
Dozvoljeno je koristiti pokazivae na statike podatkovne i funkcijske lanove, kao
u primjeru
// ...
int *pok = &Brojeni::Brojac;
int (*pokNaFunkciju)() = Brojeni::VrijednostBrojaca;
Statikim lanovima klase moe se pristupati i iz drugih modula, odnosno imaju vanjsko
povezivanje. O tome e biti govora u poglavlju o organizaciji kda.
Zadatak. U zadatku na stranici 248 deklarirali smo klasu Muha, te globalnu varijablu
koja prati broj muha u raunalu. Promijenite mehanizam brojenja muha tako da broja
bude privatan te se ita pomou statikog lana. Osim toga, esto je potrebno imati
vezanu listu svih muha u programu (na primjer, za ubijanje svih muha jednim udarcem).
Dodajte statiki lan Muha *zadnja koji e pamtiti adresu zadnje stvorene muhe.
Takoer, kako bi imali vezanu listu muha, dodajte nestatike lanove prethodna i
sljedeca koji e se postavljati u konstruktoru i destruktoru te e pokazivati na
prethodnu i na sljedeu muhu u nizu. Dakle, prilikom stvaranja objekta on e se
automatski ubaciti u listu, a prilikom unitavanja on e se automatski izbrisati iz liste.
class Polje {
public:
Polje() : { duljina = ::duljina;
pokPolje = new int[duljina]; }
private:
int duljina, *pokPolje;
};
oznaava ime iz podruja klase. Sve definicije umetnutih funkcijskih lanova unutar
deklaracije klase se mogu promatrati kao da su navedene izvan deklaracije klase bez
promjene u znaenju, to znai da mogu pristupati lanovima klase deklariranim nakon
umetnute definicije. Ako se pak upotrebi operator za razluivanje podruja bez
navoenja podruja s lijeve strane, identifikator oznaava ime iz globalnog podruja.
Identifikator ::duljina oznaava globalnu cjelobrojnu varijablu. Operator :: se moe
koristiti i za pristup globalnoj varijabli u funkciji u kojoj je deklarirana istoimena
lokalna varijabla.
Varijabla ne postoji u podruju do mjesta do kojeg je navedena njena deklaracija. U
primjeru
void funkcija() {
int dulj = duljina; // referencira se ::duljina
int duljina = 24; // globalna duljina postaje
// skrivena
dulj = duljina; // pridruuje se 24
int d = ::duljina; // pristupa se globalnoj varijabli
// i pridruuje se 12
}
Tako u primjeru
void func() {
int duljina = duljina; // koja duljina?
}
imamo deklaraciju iji je pravi smisao teko odgonetnuti na prvi pogled. U ovom
sluaju ponaanje prevoditelja ovisi o dosta kompleksnim pravilima na koja se nije
dobro oslanjati. Ako i dobro razumijete ta pravila, ne znai da e to razumjeti i ljudi
koji e eventualno biti prisiljeni itati va kd. U gornjem sluaju pravilo kae da se
mjestom deklaracije smatra mjesto na kojem je naveden identifikator, pa se stoga
duljina s desne strane znaka jednakosti odnosi na netom definiranu lokalnu varijablu.
Varijabla duljina u biti ostaje nedefinirane vrijednosti umjesto da joj se dodijeli
vrijednost istoimene globalne varijable.
Svaki funkcijski lan takoer ima svoje podruje u koje smjeta identifikatore
lokalnih varijabli i objekata definiranih u funkciji. Ako se u funkciji deklarira lokalna
264
varijabla istog imena kao podatkovni lan klase tada je lan klase sakriven. No moe mu
se pristupiti pomou operatora za razluivanje podruja:
class Polje {
public:
Polje() { duljina = 10; pokPolje = new int[duljina]; }
int Zbroji();
private:
int duljina, *pokPolje;
};
int Polje::Zbroji() {
int duljina = Polje::duljina; // dohvaa se statiki
// lan klase
int s = 0;
while (duljina) s += pokPolje[--duljina];
return s;
}
Puni naziv pojedinog lana klase ukljuuje naziv klase koji se operatorom
:: odvaja od naziva lana. Kompletan naziv se mora navesti ako elimo
dohvatiti lan u nekom drugom podruju imena.
Ime u nekom podruju sakrit e isto ime u nadreenom podruju bez obzira
to ta dva imena oznaavaju razliite entitete.
Tako e primjerice lokalna deklaracija objekta sakriti globalnu funkciju istog imena u
nadreenom podruju. Funkcijski lan istog imena kao i globalna funkcija sakrit e
identifikator globalne funkcije bez obzira na razliku u parametrima, na primjer:
class IgraSkrivaca {
public:
void func1(int); // skriva ::func1()
int func2; // skriva ::func2(int)
void duljina(int, float); // skriva ::duljina
};
memorije pozvat emo funkcijski lan objekta i registrirati alokaciju. Kada memorija
vie ne bude potrebna, pozvat emo funkcijski lan koji e izbrisati alokaciju iz liste
alokacija. Koristi od takvog sustava mogu biti viestruke prilikom traenja pogreaka u
programu budui da je jedna od najeih pogreaka neoslobaanje zauzetih segmenata
memorije. Program normalno funkcionira sve do trenutka kada se koliina slobodne
memorije ne smanji tako da program ne moe alocirati dodatnu memoriju. Pomou
sistema praenja alokacije memorije mogli bismo na kraju programa provjeriti jesu li svi
zauzeti segmenti vraeni sistemu.
Bilo bi vrlo korisno takoer imati nekakav pokazatelj koji bi ukazivao na mjesto na
kojem je alokacija bila uinjena. ANSI/ISO C++ specifikacija jezika nudi predefinirane
globalne simbole _ _LINE_ _ i _ _FILE_ _ (dvostruki znak podcrtavanja se nalazi ispred
i iza rijei). Simbol _ _LINE_ _ ima numeriku vrijednost i pokazuje broj linije u kojoj
je simbol naveden, dok simbol _ _FILE_ _ ima vrijednost znakovnog niza i daje naziv
datoteke u kojoj je linija navedena. Na kraju programa bismo mogli ispisati brojeve
linija i nazive datoteka u kojima je obavljena alokacija.
Takav napredan sistem manipulacije memorijom realizirat emo klasom koja e
objedinjavati mehanizme za dodavanje i brisanje alokacije te za ispisivanje liste
alokacija. Svaka alokacija e biti predstavljena jednim objektom koji e se smjetati u
listu prilikom svakog alociranja. Klasa tog objekta ne mora biti dostupna ostatku
programa jedino klasa memorijskog alokatora moe raditi s njom. Zbog toga je
najbolje takvu klasu deklarirati u podruju klase alokatora te se time njeno ime uklanja
iz globalnog podruja. Deklaracija se smjeta u javni, privatni ili zatieni dio
deklaracije klase. Evo primjera kda za memorijski alokator:
#include <alloc.h>
#include <string.h>
#include <iostream.h>
class MemorijskiAlokator {
private:
// ugnijeena klasa
class Alokacija {
public:
Alokacija *slijed, *pret;
size_t velicina;
void *pocetak;
int linija;
char *datoteka;
public:
MemorijskiAlokator() : prva(NULL), zadnja(NULL) {}
~MemorijskiAlokator();
void Dodaj(size_t vel, void *pok, int lin,
char *dat = NULL);
int Brisi(void *pok);
void Ispisi();
};
MemorijskiAlokator::Alokacija::Alokacija(size_t vel,
void *pok, int lin, char *dat) :
velicina(vel), pocetak(pok), linija(lin),
datoteka(new char [strlen(dat) + 1]),
slijed(NULL), pret(NULL) {
if (dat)
strcpy(datoteka, dat);
else
datoteka[0] = 0;
}
MemorijskiAlokator::Alokacija::~Alokacija() {
delete [] datoteka;
}
void MemorijskiAlokator::Alokacija::Ispisi() {
cout << datoteka << ":" << linija << " " << velicina;
cout << " " << pocetak << endl;
}
Klasa Alokacija sadri podatke o svakoj alokaciji: broj linije i naziv datoteke, adresu
poetka bloka i veliinu. Posjeduje takoer pokazivae na prethodni i na sljedei
element. Vrlo je vano primijetiti da je naziv klase sada dio podruja klase
MemorijskiAlokator. Prilikom definicije lanova klase izvan deklaracije potrebno je
navesti punu stazu do imena klase, a to je MemorijskiAlokator::Alokacija. Klasa
na uobiajeni nain pristupa svojim podatkovnim lanovima. Moe se rei da je klasa
Alokacija ugnijeena, dok je klasa MemorijskiAlokator okolna klasa (engl.
enclosing class). Okolna klasa nema nikakvih posebnih privilegija prilikom pristupa
lanovima ugnijeene klase i obrnuto. Slijedi definicija klase MemorijskiAlokator:
MemorijskiAlokator::~MemorijskiAlokator() {
Alokacija *priv = prva, *priv1;
while (priv) {
priv1 = priv;
priv = priv->slijed;
delete priv1;
}
}
void MemorijskiAlokator::Ispisi() {
Alokacija *priv = prva;
while (priv) {
priv->Ispisi();
priv = priv->slijed;
}
}
int prva;
class MemorijskiAlokator {
class Alokacija {
// ...
void Funkcija();
};
Alokacija *prva;
// ...
};
void MemorijskiAlokator::Alokacija::Funkcija() {
prva = 0; // pogreka prilikom prevoenja
}
void MemorijskiAlokator::Alokacija::Funkcija() {
::prva = 0; // sada je OK
}
Upravo zbog takvog algoritma prilikom razluivanja podruja nije potrebno navoditi
podruje prilikom referenciranja statikog lana. Statiki lan ima smisla i bez objekta
klase. Ugnijeena klasa takoer moe referencirati pobrojenja, tipove i druge klase
definirane u okolnoj klasi pod pretpostavkom da ugnijeena klasa ima pravo pristupa.
Ukoliko se ugnijeena klasa nalazi unutar javnog dijela tijela okolne klase, mogu
se objekti te klase koristiti i u glavnom programu. Prilikom definiranja takvih objekata
potrebno je navesti potpuno ime klase pomou operatora za razluivanje podruja.
Pretpostavimo da je deklaracija klase MemorijskiAlokator ovako napisana:
class MemorijskiAlokator {
private:
// ... privatni clanovi
public:
class Alokacija {
public:
Alokacija *slijed, *pret;
// ...
};
};
270
Sada je mogue u glavnom programu deklarirati objekt klase Alokacija, pri emu je
potrebno navesti puno ime ugnijeene klase:
int main() {
// deklaracija objekta ugnijeene klase
MemorijskiAlokator::Alokacija aloc, aloc1;
aloc.slijed = NULL;
aloc.pret = &aloc1;
// ...
return 0;
}
void Clan() {
broj = sirina; // OK: irina je statika
// varijabla
broj += duljina; // pogreka: duljina je
// automatska varijabla
}
};
Lokalna lokVar;
// ... tijelo funkcije
}
X2,Y2
odrezujue podruje
X1,Y1
Slika 8.4. Dijelovi linije koji padaju izvan podruja odrezivanja se uklanjaju
272
class Linija {
public:
int X1, Y1, X2, Y2;
// ... Ovdje emo kasnije umetnuti
// potrebne funkcijske lanove
};
Podatkovnim lanovima koji pamte koordinate dali smo javni pristup kako bismo
demonstrirali upotrebu pokazivaa na njih. Pokaziva na cjelobrojni lan klase Linija
ima sljedei tip:
int Linija::*
int Linija::*pokKoord;
Linija objLinija;
int *pokClanObjekta = &objLinija.X1; // ispravno
U ovom sluaju uzima se adresa lana koji je dio objekta. Kako je objekt stvoren na
tono odreenom mjestu u memoriji, adresa lana je u potpunosti poznata te je njeno
uzimanje sasvim legalno.
Adresu lana klase moemo dohvatiti pomou operatora & te pridruiti pokazivau
na lan klase:
Sada smo varijabli pokKoord pridruili podatak koji identificira pojedini lan klase
Linija. Valja primijetiti da se adresa realnog lana klase ne moe pridruiti
pokazivau na cjelobrojni lan. Takoer, nije mogue pridruiti adresu lana jedne
klase pokazivau na lan neke druge klase:
class Elipsa {
public:
int cx, cy, velikaPoluos;
float ekscentricitet;
};
// ...
Pokazivai na lanove se uvijek koriste u kontekstu nekog objekta. Pri tome postoje dva
specifina C++ operatora koji omoguavaju pristup lanovima preko pokazivaa, a to
su .* (toka i zvjezdica) te ->* (minus, vee od i zvjezdica). Operator .* se koristi
tako da se s lijeve strane nalazi objekt kojemu se eli pristupiti, a s desne strane
pokaziva na lan. Operatoru ->* se s lijeve strane navede pokaziva na objekt kojemu
se eli pristupiti dok mu se s desne strane navede pokaziva na lan. Demonstrirajmo
upotrebu operatora na sljedeem primjeru:
int main() {
int Linija::*pokCjelobrojni = &Linija::X1;
pokCjelobrojni = &Linija::Y1;
7
9
u tip void *. Takoer nema implicitne konverzije u aritmetiki tip te nije podrana
pokazivaka aritmetika. To znai da naredbom
class Linija {
private:
int X1, Y1, X2, Y2;
void OdreziTocku(int Linija::*pok1,
int Linija::*pok2,
int v1, int v2, int p, int tip);
public:
void Odrezi(int px1, int py1, int px2, int py2);
};
class GrafObjekt {
public:
void Crtaj();
int Gore();
int Dolje();
int Lijevo();
int Desno();
void PostaviVelicinu(int sirina, int visina);
};
278
int (GrafObjekt::*pokNaFClan)();
pokNaFClan = GrafObjekt::Gore;
Kao i kod pokazivaa na funkcije, nije potrebno navoditi operator za uzimanje adrese
prije navoenja funkcijskog lana. Tako je zapis pokazivaa &GrafObjekt::Gore i
GrafObjekt::Gore ekvivalentan.
Prilikom pozivanja funkcijskog lana pomou pokazivaa na njega koriste se
operatori .* i ->* pri emu se s lijeve strane operatora navodi objekt za operator .*
odnosno pokaziva na objekt za operator ->*, a s desne strane pokaziva na funkcijski
lan. Iza njega se u zagradama navode mogui parametri funkcijskog lana. Evo
primjera:
Upotreba zagrada oko naziva funkcije i objekta je obavezna zato jer operator () za
poziv funkcije ima vei prioritet izvoenja. U gore navedenom pozivu funkcijskog
lana na koji pokazuje bezparam redoslijed izvoenja operatora je sljedei: najprije se
pomou
grObj.*bezparam
grObj.*bezparam();
bi se interpretirala kao
grObj.*(bezparam());
bezparam = GrafObjekt::PostaviVelicinu;
// pogreno: PostaviVelicinu ima dva parametra, a
// bezparam je deklariran kao pokaziva na lan
// koji nema parametara
bezparam = GrafObjekt::Crtaj;
// pogreno: Crtaj ne vraa vrijednost, a
// bezparam je deklariran kao pokaziva na lan
// koji vraa cjelobrojnu vrijednost
bezparam = GrafObjekt::Gore; // OK
bezparam = GrafObjekt::Lijevo; // OK
class DrugaKlasa {
public:
int FunkClan();
};
bezparam = DrugaKlasa::FunkClan;
// pogreno: bezparam je definiran kao pokaziva na
// funkcijski lan klase GrafObjekt, a ne klase DrugaKlasa
Pokazivai na funkcijske lanove imaju isti skup dozvoljenih operacija kao i pokazivai
na podatkovne lanove.
Promotrimo rjeenje naeg problema pomicanja objekta. Dan je funkcijski lan
PonoviPomak() koji ponavlja pomak u eljenom smjeru puta puta:
GrafObjekt nekiObjekt;
nekiObjekt.PonoviPomak(GrafObjekt::Dolje, 4);
Zadatak. Evo jednog primjera iz ivota (!). Nainite klasu Tijelo koje e opisivati
ljudsko tijelo. Pri tome sami izaberite skup organa koje elite ukljuiti u klasu. Svaki
organ neka je predstavljen jednim podatkovnim lanom tipa bool koji e pokazivati da
li dotini organ radi ispravno ili ne. Evo primjera:
class Tijelo {
private:
bool srce;
bool lijeviBubreg;
bool desniBubreg;
bool jetra;
bool slezena;
};
C++ jezika i u velikoj mjeri ovise o implementaciji prevoditelja. Svi primjeri navedeni u
ovom poglavlju su prevedeni pomou Borland C++ 4.5 prevoditelja.
class Vektor {
friend void ZbrojiVektore(Vektor &a, Vektor &b,
Vektor &rez);
private:
float ax, ay;
public:
Vektor(float x = 0, float y = 0) : ax(x), ay(y) {}
float DajX() { return ax; }
float DajY() { return ay; }
void PostaviXY(float x, float y) { ax = x; ay = y; }
};
Zbrajanje dvaju vektora moemo obaviti tako da deklariramo tri objekta klase Vektor
te pozovemo funkciju ZbrojiVektore():
ZbrojiVektore(a, b, c);
Ovakav nain rada je sasvim ispravan i regularan. Objekt c e na kraju sadravati zbroj
vektora a i b. Jedina je mana to vektor a, iako nikada kasnije u programu ne koristimo,
moramo deklarirati, i to je jo ozbiljnije, za njega odvojiti memorijski prostor koji
ostaje zauzet do kraja bloka unutar kojeg je deklariran. Mnogo praktinije je provesti
282
raunsku operaciju tako da stvorimo privremeni objekt klase Vektor koji e ivjeti
samo za vrijeme raunanja zbroja, a nakon toga e biti uniten. Trebamo, dakle, nain
da u kd upiemo vektorsku konstantu, ekvivalentnu cjelobrojnim konstantama koje
piemo u klasinim izrazima.
Privremeni objekt moemo stvoriti tako da navedemo naziv klase i u okruglim
zagradama stavimo parametre konstruktoru (ili samo otvorenu i zatvorenu zagradu ako
parametre izostavljamo) . Prema tome bismo isti rezultat kao i u gornjem primjeru mogli
postii sljedeim pozivom:
Vektor c;
Iako dovitljivi itatelj moe primijetiti da bi bilo daleko jednostavnije i efikasnije odmah
deklarirati vektor c i dodijeliti mu vrijednost izraunatu pomou depnog raunala, ovaj
primjer pokazuje kako se mogu stvoriti privremeni objekti. Prije ulaska u funkciju
ZbrojiVektore() prevoditelj e stvoriti dva privremena objekta, a zatim pozvati
funkciju adrese objekata e biti proslijeene preko referenci. Kada funkcija zavri,
prevoditelj e privremene objekte unititi.
ivot privremenih objekata je ogranien na izraz u kojem se pojavljuju. Na kraju
naredbe (kod toke-zarez) svi objekti stvoreni u izrazu se unitavaju. Da bi se mogao
pratiti tok stvaranja i unitavanja objekata, moemo proiriti konstruktore i destruktore
naredbama koje ispisuju poruke o tome da je objekt stvoren, odnosno uniten. Klasi
Vektor emo takoer dodati statiki lan brojac koji e brojati stvorene objekte.
Svaki objekt e zapamtiti broj pod kojim je stvoren u lanu redbr. Vrijednost tog lana
e se ispisati prilikom stvaranja i unitavanja objekta, te emo na taj nain moi
identificirati objekte.
#include <iostream.h>
class Vektor {
friend void ZbrojiVektore(Vektor &a, Vektor &b,
Vektor &rez);
private:
static int brojac;
int redbr;
float ax, ay;
public:
Vektor(float x = 0, float y = 0);
~Vektor();
float DajX() { return ax; }
float DajY() { return ay; }
void PostaviXY(float x, float y) { ax = x; ay = y; }
};
int Vektor::brojac = 0;
283
Vektor::Vektor(float x, float y) :
ax(x), ay(y), redbr(++brojac) {
cout << "Stvoren vektor pod brojem " << redbr << endl;
cout << "X: " << ax << " Y: " << ay << endl;
}
Vektor::~Vektor() {
cout << "Uniten vektor pod brojem " << redbr << endl;
cout << "X: " << ax << " Y: " << ay << endl;
}
int main() {
cout << "Ulazak u main" << endl;
Vektor c;
cout << "Pozivam ZbrojiVektore" << endl;
ZbrojiVektore(Vektor(10.0, 2.8), Vektor(-2.0, 5.0), c);
cout << "Zavravam" << endl;
return 0;
}
Ulazak u main
Stvoren vektor pod brojem 1
X: 0 Y: 0
Pozivam ZbrojiVektore
Stvoren vektor pod brojem 2
X: -2 Y: 5
Stvoren vektor pod brojem 3
X: 10 Y: 2.8
Zbrajam
284
Zbrojio sam
Uniten vektor pod brojem 3
X: 10 Y: 2.8
Uniten vektor pod brojem 2
X: -2 Y: 5
Zavravam
Uniten vektor pod brojem 1
X: 8 Y: 7.8
Vektor pod brojem 1 je vektor c deklariran kao lokalni objekt. Prije ulaska u funkciju se
stvaraju privremeni vektori koji se unitavaju odmah nakon povratka iz nje. Program
zatim zavrava te se na samom kraju unitava i objekt 1.
Mogue je pozvati funkcijski lan privremenog objekta ili pristupati njegovim
podatkovnim lanovima. Dodajmo funkcijski lan IspisiVektor() za ispis vektora te
promotrimo rezultat sljedeeg kda:
class Vektor {
// ... ovdje ide deklaracija klase
public:
void IspisiVektor();
};
void Vektor::IspisiVektor() {
cout << "Ispis {" << ax << "," << ay << "}" << endl;
}
int main() {
cout << "Prije izraza" << endl;
Vektor(12.0, 3.0).IspisiVektor();
cout << "Nakon izraza" << endl;
return 0;
}
Prije izraza
Stvoren vektor pod brojem 1
X: 12 Y: 3
Ispis {12,3}
Uniten vektor pod brojem 1
X: 12 Y: 3
Nakon izraza
class Vektor {
// ... ovdje idu deklaracije
public:
Vektor(const Vektor &ref);
};
void NaNulu(Vektor v) {
cout << "Uao u NaNulu" << endl;
v.PostaviXY(0, 0);
cout << "Postavio sam" << endl;
}
int main() {
cout << "Uao sam u main" << endl;
Vektor c(12.0, 3.0);
cout << "Pozivam NaNulu" << endl;
NaNulu(c);
cout << "Zavravam" << endl;
return 0;
}
Ispis je sljedei:
X: 0 Y: 0
Zavravam
Uniten vektor pod brojem 1
X: 12 Y: 3
Vektor *pok;
void funkcija(Vektor a) {
pok = &a;
// ... radi neto s a
}
int main() {
Vektor a;
funkcija(a);
// pok visi - pokazuje na memoriju koja vie
// nije objekt
return 0;
}
memoriju gdje je bio privremeni objekt. I dok pristup podatkovnom lanu pomou tog
pokazivaa vjerojatno nee sruiti sistem te e samo vratiti neku nedefiniranu
vrijednost, pristup funkcijskom lanu e vrlo vjerojatno uzrokovati ispad programa.
Takoer e pokuaj upisivanja vrijednosti u objekt vrlo vjerojatno rezultirati
prepisivanjem preko drugih podataka, to za aplikaciju moe biti pogubno. Slini efekti
se mogu postii i tako da se reference inicijaliziraju objektima koji se zatim unite.
Pouka ovog primjera je da su visei pokazivai i reference vrlo opasni po integritet
programa. Po mnogim studijama oni su jedni od najeih uzroka nepredvienog kraha
mnogih komercijalnih programa te valja biti vrlo oprezan da se ovakve situacije
izbjegnu.
Ponekad optimizacije izvedbenog koda koje prevoditelj provodi mogu dovesti do
toga da postupak prenoenja objekata odstupa od gore izloenog. Promotrimo sljedei
primjer:
int main() {
cout << "Uao sam u main" << endl;
cout << "Pozivam NaNulu" << endl;
NaNulu(Vektor(12, 3));
cout << "Zavravam" << endl;
return 0;
}
Vektor a, b = a;
288
interpretira kao Stvori vektor a i zatim stvori vektor b tako da konstruktorom kopije
kopira a u b. Inicijalizacija
int main() {
cout << "Uao sam u main" << endl;
Vektor a = Vektor(12.0, 3.0);
// ...
return 0;
}
int main() {
cout << "Uao u main" << endl;
Vektor a(12.0, 3.0), b(-2.0, -6.0), c;
cout << "Ulazim u ZbrojiVektore" << endl;
c = ZbrojiVektore(a, b);
cout << "Zavravam" << endl;
289
return 0;
}
Uao u main
Stvoren vektor pod brojem 1
X: 12 Y: 3
Stvoren vektor pod brojem 2
X: -2 Y: -6
Stvoren vektor pod brojem 3
X: 0 Y: 0
Ulazim u ZbrojiVektore
Uao u ZbrojiVektore
Stvoren vektor pod brojem 4
X: 10 Y: -3
Stvoren vektor pomou konstruktora kopije pod brojem 5
X: 10 Y: -3
Uniten vektor pod brojem 4
X: 10 Y: -3
Uniten vektor pod brojem 5
X: 10 Y: -3
Zavravam
Uniten vektor pod brojem 5
X: 10 Y: -3
Uniten vektor pod brojem 2
X: -2 Y: -6
Uniten vektor pod brojem 1
X: 12 Y: 3
Sada se ne stvara lokalni objekt koji se zatim kopira u rezultat, nego se stvara
privremeni objekt koji se nakon zavretka proglaava rezultatom funkcije. Nakon
izvoenja program daje sljedee rezultate:
Uao u main
Stvoren vektor pod brojem 1
X: 12 Y: 3
Stvoren vektor pod brojem 2
X: -2 Y: -6
Stvoren vektor pod brojem 3
X: 0 Y: 0
Ulazim u ZbrojiVektore
Uao u ZbrojiVektore
Stvoren vektor pod brojem 4
X: 10 Y: -3
Uniten vektor pod brojem 4
X: 10 Y: -3
Zavravam
Uniten vektor pod brojem 4
X: 10 Y: -3
Uniten vektor pod brojem 2
X: -2 Y: -6
Uniten vektor pod brojem 1
X: 12 Y: 3
Funkcija takoer moe vratiti referencu na objekt te se tada objekt ne kopira. Tipian
primjer takve funkcije je funkcijski lan koji vraa referencu na neki od lanova
objekta. Klasa Tablica koju smo uveli u poglavlju 8.4.1 ima lanove za ubacivanje
elemenata u tablicu te njeno poveanje i smanjenje. No nigdje nismo naveli funkcijski
lan kojim se moe pristupiti pojedinom elementu tablice. Stoga emo dodati lan
Element koji e kao parametar imati redni broj elementa tablice koji elimo dohvatiti
te e vratiti referencu na eljeni lan. Evo kako to izgleda:
class Tablica {
private:
int *Elementi;
int BrojElem, Duljina;
public:
Tablica();
Tablica(int BrElem);
void PovecajNa(int NovaDulj);
void DodajElem(int Elt);
void BrisiElem(int Poz);
int &Element(int indeks);
};
291
Paljiv itatelj sigurno se pita zato se vraa referenca na element umjesto da se vraa
sam element. Razlog je u tome to se prilikom vraanja elementa stvara privremeni
objekt koji ivi do kraja izraza u kojem je stvoren te nema nikakve veze s elementom
tablice. U sluaju vraanja reference nema stvaranja privremenog objekta. Referenca
nije nita drugo nego adresa koja pokazuje gdje se u memoriji nalazi objekt i moe se
nai s lijeve strane operatora pridruivanja. Mogue je napisati sljedee:
int main() {
Tablica t;
// inicijaliziraj tablicu na 5 elemenata
for (int i = 0; i < 5; i++) t.DodajElem(i * 8);
cout << t.Element(3); // pristup vrijednosti treeg
// elementa
t.Element(2) = 45; // izmjena vrijednosti
// drugog elementa
return 0;
}
Nije ispravno vratiti referencu na lokalni objekt zato jer se lokalni objekt unitava nakon
to funkcija zavri. Vraena referenca e uvijek pokazivati na podruje u memoriji koje
ne sadrava objekt, na to e mnogi prevoditelji upozoriti prilikom prevoenja.
292
9. Strukture i unije
struct Zaposleni {
char *ime;
int brojGodina;
char spol;
char *odjel;
int brojUzdrzavaneNejaci;
};
Jezik C++ uvodi novinu u odnosu na C prilikom deklaracije objekata iji je tip definiran
strukturom. U C jeziku prilikom deklaracije strukturnih varijabli potrebno je bilo navesti
293
kljunu rije struct iza koje se navodio naziv strukture te naziv varijable, kao na
primjer:
struct Par {
float a, b;
};
U C++ jeziku prilikom deklaracije varijabli nije potrebno navoditi poetnu rije
struct, nego se jednostavno samo navede naziv strukture iza kojeg se navedu nazivi
objekata koje se eli deklarirati, ba kao da se radi o klasama:
Razlika izmeu struktura i klasa je vie filozofska, ali je mnogi programeri potuju.
Naime, klasa definira objekt te oznaava da novi tip potuje koncepte objektnog
programiranja. To znai da e klasa osim podatkovnih, definirati i funkcijske lanove
koji opisuju operacije na objektu. Struktura, pak, predstavlja jednostavno sloeni tip
poznat jo iz C jezika. To je samo nakupina podataka umotanih u zajedniku ovojnicu.
Ova razlika nije definirana jezikom te je na vama da odluite elite li se toga pridravati.
Strukture su u C++ jeziku ostavljene primarno radi kompatibilnosti s jezikom C.
Kako pojam klase sam po sebi oznaava podatkovni skup o kojem je potrebno
razmiljati na drukiji nain nego o strukturama, uvedena je nova kljuna rije class.
Njome se istie da navedeni tip ima dodatna svojstva u odnosu na obine strukture.
Kljuna rije struct je ostavljena zato da se olaka prijelaz na C++ dotadanjim C
korisnicima, te da se omogui prevoenje postojeih programa bez veih izmjena.
9.2. Unije
Zamislimo da elimo napraviti program koji simulira kalkulator. Radi jednostavnosti
pretpostavimo da korisnik najprije unosi prvi operand koji je broj, zatim operator, zatim
drugi operand te na kraju znak jednakosti. Pri tome elimo unos korisnika prikazati
jednim objektom koji e se dalje obraivati u programu za izraunavanje rezultata.
Vidimo da nam se unos sastoji od dva sutinski razliita tipa podataka: brojeva, u
sluaju da je korisnik unio broj, te niza znakova, u sluaju da je korisnik unio operator
294
(uzimamo niz znakova jer je korisnik mogao unijeti primjerice sqrt kao operator za
kvadratni korijen). Unos korisnika bismo mogli opisati sljedeom klasom:
Klasa sadri podatkovni lan tip koji svojom vrijednou odreuje da li objekt
predstavlja broj ili operator. lanovi broj i nizOperator sadre podatke o broju
odnosno operatoru koji je unesen.
Vano je primijetiti da nikada nije potrebno pamtiti podatke i o broju i o operatoru:
prisutan je samo jedan ili drugi podatak. Upravo zbog toga gornje rjeenje nije efikasno
sa stajalita zauzea memorijskog prostora. Svaki objekt klase sadri oba lana, bez
obzira na to to je u jednom trenutku potreban samo jedan od njih. Bilo bi vrlo pogodno
kada bi lanovi broj i nizOperator mogli dijeliti memorijski prostor pa bi u svakom
objektu postojao samo jedan ili samo drugi lan, ovisno o potrebi.
Upravo to je mogue postii upotrebom posebnih tipova podataka nazvanih
unijama (engl. union). Unije se deklariraju slino klasama i strukturama s tom razlikom
to se koristi kljuna rije union. One mogu sadravati podatkovne i funkcijske
lanove, ugnijeene klase i rijei za dodjelu prava pristupa. Bitna razlika u odnosu na
klase je u tome to svi podatkovni lanovi unije dijele isti memorijski prostor. To znai
da se prilikom pridruivanja vrijednosti jednom podatkovnom lanu prepisuje vrijednost
podatkovnog lana koji je bio ranije upisan. Objekt Unos napisan pomou unija
izgledao bi ovako:
union Unos {
int broj;
char *nizOperator;
};
Unos moe sadravati ili broj ili nizOperator, ali nikako oba odjednom. Mogue je
definirati objekte tipa Unos. To se radi na isti nain kao i prilikom definiranja objekata
klase, tako da se iza naziva unije navedu identifikatori objekata:
Unos saTipkovnice;
Unije su postojale i u C jeziku gdje se prije definiranja unije ispred naziva morala
stavljati kljuna rije union:
U C++ jeziku to vie nije potrebno (no ako se koristi nije pogreno).
295
Kako svi lanovi dijele isti memorijski prostor, veliina unije je jednaka veliini
njenog najveeg lana. Budui da se razliiti tipovi podataka u memoriju pohranjuju
prema razliitim bitovnim predlocima, vrlo je vano biti oprezan prilikom koritenja
unija kako bi se uvijek pristupalo podatkovnom lanu iju vrijednost unija u tom
trenutku sadrava. Ako pridruimo neku vrijednost lanu broj unije Unos, nije
sintaktiki pogreno itati vrijednost lana nizOperator. No program koji to radi
zasigurno nee funkcionirati ispravno. Vrijednost koja e se u takvom sluaju proitati
iz lana nizOperator je u principu sluajna i ovisi o nainu na koji raunalo
pohranjuje cjelobrojni i pokazivaki tip. Na primjer, kd
#include <iostream.h>
int main() {
union Unos saTipkovnice;
saTipkovnice.nizOperator = "+";
cout << saTipkovnice.broj << endl;
return 0;
}
e ispisati broj ija e vrijednost ovisiti o mjestu na kojem se nalazi znakovni niz "+".
Kako se i cijeli broj i pokaziva na znak pohranjuju u isti memorijski prostor, prilikom
itanja lana broj raunalo e jednostavno pristupiti memoriji i proitati vrijednost
koja je tamo zapisana. Ta vrijednost ovisi o adresi znakovnog niza te o nainu njene
pohrane.
Unija moe sadravati objekte klasa pod uvjetom da oni nemaju definiran niti
konstruktor niti destruktor. Sama unija, naprotiv, moe sadravati i vie konstruktora i
destruktor. Statiki lanovi unija nisu dozvoljeni. Pojedinim lanovima se moe
dodijeliti proizvoljno pravo pristupa. Ako se pravo pristupa ne navede, podrazumijeva
se javni pristup. Unije mogu sadravati funkcijske lanove. Prilikom pisanja funkcijskih
lanova takoer valja imati na umu da unija odjednom sadri samo jedan objekt.
Kako bi se vodila evidencija o tome koji je podatak upisan u uniju, esto se unija
navodi kao podatkovni lan neke klase. Jedan lan klase obino prati koji je podatkovni
lan unije trenutno aktivan. Taj lan se zove diskriminanta unije (engl. union
discriminant). Ako se prilikom deklaracije unije odmah deklarira varijabla ili
podatkovni lan klase tipa te unije, a unija se vie nigdje ne koristi, njeno se ime moe
izostaviti. Demonstrirajmo to primjerom:
class Unos {
enum { unosOperator, unosBroj} tip;
union { // unija nema ime
int broj;
char *nizOperator;
} vrijednost;
};
296
Klasa Unos se sada sastoji iz podatkovnog lana tip koji odreuje vrstu unosa te iz
lana vrijednost koji je definiran kao bezimena unija (engl. nameless union) i koji
odreuje vrijednost unosa. Sada je mogue napisati funkcijski lan za ispis vrijednosti s
tipkovnice na sljedei nain:
class Unos {
// Ovdje treba umetnuti gornju definiciju
void Ispis();
};
void Unos::Ispis() {
switch (tip) {
case unosOperator:
cout << vrijednost.nizOperator << endl;
break;
case unosBroj:
cout << vrijednost.broj << endl;
break;
}
}
U gornjoj definiciji uveden je podatkovni lan vrijednost koji sadri vrijednost unosa
te ga je potrebno navoditi svaki put prilikom pristupa lanovima broj i
nizOpertator. To je dosta nepraktino i moe se izbjei koritenjem anonimnih unija
(engl. anonymous unions). Anonimna unija nema niti naziv unije niti naziv varijable
koja se deklarira. Elementi unije tada pripadaju podruju u kojem je unija definirana te
im se pristupa direktno:
class Unos {
enum { unosOperator, unosBroj} tip;
union { // anonimna unija
int broj;
char *nizOperator;
};
void Ispis();
};
void Unos::Ispis() {
switch (tip) {
case unosOperator:
cout << nizOperator << endl;
break;
case unosBroj:
cout << broj << endl;
break;
}
}
297
U ovoj zavrnoj verziji klase Unos lanovi broj i nizOperator pripadaju podruju
klase Unos te njihov naziv mora biti jedinstven unutar klase.
Zadatak. Napiite univerzalni tip koji e moi sadravati cijeli broj, realni broj ili
pokaziva na znakovni niz. Opi oblik je ovakav:
class MultiPraktik {
char tip;
union {
int cj_vrij;
double d_vrij;
char *zn_vrij;
};
};
class Prekid {
public:
unsigned short dozvoljen : 1;
unsigned short prioritet : 3;
unsigned short maska : 2;
};
Klasa Prekid ima tri polja bitova te e biti upakirana u memorijski prostor koji
zauzima tip unsigned short. Svakom od lanova mogue je nezavisno pristupati, a
prevoditelju se preputa da se brine o tome da modificira samo odreene bitove nekog
podatka. Pristup poljima bitova obavlja se standardnom C++ sintaksom:
298
Prekid int1;
// ...
int1.dozvoljen = 0;
if (int1.prioritet == 2) int1.maska = 1;
Prilikom pridruivanja je potrebno voditi rauna o opsegu vrijednosti koje pojedini lan
moe pohraniti. Na primjer, lan dozvoljen je duljine samo jednog bita, tako da moe
poprimiti samo vrijednosti 1 ili 0. lan prioritet je duljine tri bita, pa moe poprimiti
vrijednosti od 0 do 7 ukljuivo.
Operator za uzimanje adrese & se ne moe primijeniti na polje bitova pa tako niti
pokaziva na lan na polje bitova nema smisla. Takoer, polje bitova ne moe
sadravati statiki lan.
Zadatak. Promijenite program iz odsjeka 2.4.11 za definiranje parametara serijske
komunikacije tako da se umjesto direktnog pristupa pojedinim bitovima cjelobrojne
varijable koriste polja bitova.
299
Zbog toga emo uvesti tip ZnakovniNiz koji e sadravati pokaziva na niz
znakova te niz funkcijskih lanova, operatora i operatora konverzije potrebnih za
manipulaciju nizom. Evo deklaracije klase:
#include <iostream.h>
#include <string.h>
class ZnakovniNiz {
private:
char *pokNiz;
public:
// implementaciju klase emo dodavati postupno
};
class ZnakovniNiz {
private:
char *pokNiz;
public:
// konstruktor koji e obaviti i konvenrziju
// u tip ZnakovniNiz
ZnakovniNiz(char *niz = "");
ZnakovniNiz(const ZnakovniNiz &ref);
~ZnakovniNiz();
char *DajPokazivac() { return pokNiz; }
};
301
ZnakovniNiz::ZnakovniNiz(char *niz) :
pokNiz(new char[strlen(niz) + 1]) {
strcpy(pokNiz,niz);
cout << "Stvoren niz: " << niz << endl;
}
ZnakovniNiz::~ZnakovniNiz() {
cout << "Uniten niz: " << pokNiz << endl;
delete [] pokNiz;
}
int main() {
ZnakovniNiz a = "Niz a"; // konverzija prilikom
// inicijalizacije
Funkcija("parametar"); // konverzija prilikom
// prenoenja parametara
return 0;
}
class Razlomak {
private:
float brojnik, nazivnik;
public:
// eksplicitni konstruktor
explicit Razlomak(float raz) : brojnik(raz),
nazivnik(1.0) {}
};
Razlomak r1(10.5); // OK
Razlomak r2 = r1; // OK
void AplusB(Razlomak raz1, Razlomak raz2);
AplusB(r1, 5.0); // pogreka: 5.0 se prevodi u
// razlomak implicitnom
// konstrukcijom
AplusB(r1, Razlomak(5.0)); // OK: konstruktor je pozvan
// ekplicitno
operator tip();
303
Pri tome je potrebno tip zamijeniti nazivom tipa u koji se pretvorba obavlja. Operator
za konverziju ne smije imati naveden rezultirajui tip niti parametre.
Dodajmo klasi ZnakovniNiz operator koji konvertira objekt u pokaziva na prvi
lan:
class ZnakovniNiz {
// ...
public:
operator char*() { return pokNiz; }
};
Sada je mogue koristiti objekt klase ZnakovniNiz svugdje gdje se moe pojaviti
pokaziva na znak. Na primjer:
#include <stdio.h>
int main() {
ZnakovniNiz a = "abrakadabra";
puts(a);// poziva konverziju objekta a u char *
return 0;
}
Jedna klasa moe definirati vie operatora konverzije u razliite tipove. Moemo dodati
operator koji konvertira ZnakovniNiz u cijeli broj jednak duljini niza:
class ZnakovniNiz {
// ...
public:
operator int() { return strlen(pokNiz); }
};
Kada klasa ima vie operatora konverzije, vrlo esto dolazi do situacije da prevoditelj ne
moe razluiti koju konverziju treba primijeniti:
int main() {
ZnakovniNiz a = "U to e me pretvoriti?";
Ispisi(a); // pogreka: koja konverzija?
return 0;
}
304
U ovom sluaju nije jasno treba li objekt a pretvoriti u cijeli broj ili u pokaziva na znak
te se zbog toga dobiva pogreka prilikom prevoenja. Programer mora eksplicitno
naznaiti koju konverziju eli pomou klasine sintakse za dodjelu tipa. U okruglim
zagradama se navede eljeni tip, a iza zatvorene zagrade se navede objekt. Eksplicitna
pretvorba u pokaziva na znak izgleda ovako:
Ispisi((char *)a);
Ako se ciljni tip ne moe direktno dostii jednom korisniki definiranom pretvorbom,
prevoditelj e pokuati pronai korisniki definiranu konverziju koja e podatak
pretvoriti u meutip, koji se zatim svodi na traeni tip standardnom konverzijom.
Korisniki definirana konverzija se ne obavlja ako je na rezultat konverzije potrebno
primijeniti jo jednu korisniki definiranu konverziju. Na primjer:
int main() {
ZnakovniNiz a = "Dugi niz";
PisiLong(a);// poziva se konverzija u int, a int
// se zatim standardnom konverzijom
// pretvara u long
return 0;
}
Radi daljnje demonstracije svojstava operatora konverzije, dodat emo jo jednu klasu
Identifikator koja moe biti od koristi prilikom izgradnje prevoditeljskog
programa. Klasa modelira svojstva identifikatora te sadri objekt naziv klase
ZnakovniNiz koji pamti naziv identifikatora, te cjelobrojni lan indeks koji pokazuje
indeks identifikatora u tablici identifikatora koju prevoditelj generira prilikom
prevoenja. Klasu emo opremiti s dva operatora konverzije, i to konverzijom u tip
ZnakovniNiz koji e vraati vrijednost lana naziv, te u int koji e vraati
vrijednost lana indeks.
class Identifikator {
private:
int indeks;
ZnakovniNiz naziv;
public:
Identifikator(int ind, const ZnakovniNiz &ref) :
indeks(ind), naziv(ref) { }
305
int main() {
Identifikator a(5, ZnakovniNiz("Nema konverzije"));
PisiZnakovniNiz(a); // pogreka
return 0;
}
Ako bismo eljeli da nam ovaj kd proradi, morali bismo umetnuti u klasu
Identifikator operator za konverziju u char *:
class Identifikator {
public:
// ...
operator char*() { return naziv; }
// implicitno se poziva konverzija
// klase ZnakovniNiz u char*
};
Sada je gornji poziv ispravan. Pri tome se u naredbi return provodi implicitna
konverzija iz tipa ZnakovniNiz u char * pomou korisniki definirane konverzije iz
klase ZnakovniNiz. Opisana konverzija u char * moe imati neugodne popratne
pojave:
int main() {
ZnakovniNiz pobjednikNaLutriji = "Janko";
char *pokNiz = pobjednikNaLutriji;
// gornja naredba provodi implicitnu konverziju tako da
// vraa pokaziva na implementaciju
*pokNiz = 'R'; // pobjednik postaje Ranko
return 0;
}
Problem dvosmislenosti se esto moe pojaviti u sluaju kada dvije klase definiraju
operatore konverzije izmeu sebe. Proirimo klasu ZnakovniNiz konstruktorom koji
konvertira objekt klase Identifikator u ZnakovniNiz:
class ZnakovniNiz {
// ...
public:
ZnakovniNiz(const Identifikator &ref);
};
void NekaFunkcija(ZnakovniNiz);
int main() {
Identifikator a(5, ZnakovniNiz("znn"));
NekaFunkcija(a);
return 0;
}
NekaFunkcija(a.operator ZnakovniNiz());
Zadatak. Dodajte klasi ZnakonvniNiz konstruktor koji e kao parametar uzimati cijeli
broj. Pri tome e se znakovni niz inicijalizirati tako da sadri zadani broj. Na primjer,
deklaracija
ZnakovniNiz godina(1997);
307
class Tocka {
public:
int x, y;
};
class Pravokutnik {
private:
Tocka gornjaDesna, donjaLijeva;
};
class Poligon {
private:
int brojVrhova;
Tocka *vrhovi;
};
. .* :: ?: sizeof
Takoer, nije mogue uvesti novi operator. Na primjer, jezik FORTRAN posjeduje
operator ** za potenciranje koji bi vrlo esto bio koristan i u C++ programima. No
308
naalost, nije mogue dodati novi operatorski simbol, ve samo proiriti znaenje ve
postojeih simbola.
Svaki operator zadrava broj argumenata koji ima po standardnoj C++ sintaksi. To
znai da zato to je operator << definiran kao binarni, mora biti i preoptereen kao
binarni. Redoslijed obavljanja raunskih operacija takoer ostaje nepromijenjen.
+ - * / % ^ & |
~ ! , = < > <= >=
++ -- << >> == != && ||
+= -= /= %= ^= &= |= *=
<<= >>= [] () -> ->* new delet
e
Operator se definira kao funkcija ili funkcijski lan koji za naziv ima kljunu rije
operator iza koje se stavlja simbol operatora. Operator se moe, a i ne mora odvojiti
razmakom od kljune rijei operator i liste parametara. Operatori
+ - & *
postoje u unarnoj i binarnoj varijanti (dakle, mogu uzimati jedan ili dva parametra), pa
se mogu i preopteretiti kao unarni ili binarni.
Kako ima smisla zbrajati vektore, moemo u klasu Vektor ubaciti operator za
zbrajanje:
class Vektor {
friend Vektor operator+(Vektor &a, Vektor &b);
private:
float ax, ay;
public:
Vektor(float x = 0, float y = 0) { ax = x; ay = y; }
void PostaviXY(float x, float y) { ax = x; ay = y; }
float DajX() const { return ax; }
float DajY() const { return ay; }
void MnoziSkalarom(float skalar);
};
inline
void Vektor::MnoziSkalarom(float skalar) {
ax *= skalar;
ay *= skalar;
}
#include <iostream.h>
int main() {
Vektor a(12.0, 3.0), b(-3.0, -6.0), c;
c = a + b; // poziva se operator za zbrajanje vektora
cout << c.DajX() << " " << c.DajY() << endl;
return 0;
}
c = a + b;
se interpretira kao
c = operator+(a, b);
= [] () ->
mogu iskljuivo biti definirane kao funkcijski lanovi. Ako se operatorska funkcija
definira kao funkcijski lan, broj parametara je za jedan manji nego u sluaju kada je
funkcija definirana izvan klase.
Definirajmo klasu X te operatore + i -. Binarni operatori ije funkcije su definirane
kao lanovi imaju jedan parametar, dok unarni nemaju parametara. Takoer emo
definirati objekte a i b klase X:
class X {
public:
X operator-(const X &desno);
};
X a, b;
Poziv
a + b;
interpretira se kao
operator+(a, b);
dok se poziv
a - b;
interpretira kao
a.operator-(b);
Dakle, ako je operator definiran izvan klase, lijevi i desni objekt se prosljeuju
operatorskoj funkciji kroz prvi odnosno drugi parametar. Kada je operatorska funkcija
definirana kao funkcijski lan, poziva se funkcijski lan za objekt lijevo od operatora,
dok se objekt desno od operatora prosljeuje kroz parametar.
Kako operatori +, -, * i & imaju svoju unarnu i binarnu varijantu, obje varijante mogu
biti preoptereene neovisno, kao u sljedeem primjeru:
311
class Y {
public:
// definicija unutar klase
void operator*(); // unarni *
Y operator*(const Y &ref); // binarni *
};
Y a, b;
int main() {
*a; // a.operator*();
a * b; // a.operator*(b);
&a; // operator&(a);
a & b; // operator&(a, b);
return 0;
}
Iako se ini da je svejedno je li operatorska funkcija definirana unutar ili izvan klase,
postoji sutinska razlika u jednom i drugom pristupu.
Na primjer:
class Kompleksni {
friend Kompleksni operator-(const Kompleksni &l,
const Kompleksni &d);
private:
float real, imag;
public:
Kompleksni(float r = 0, float i = 0) :
real(r), imag(i) {}
Kompleksni operator+(const Kompleksni &d);
};
Kako je bilo koji realni broj ujedno i kompleksni (njegov imaginarni dio je nula), klasa
Kompleksni ima konstruktor koji moe svaki realni broj konvertirati u kompleksni.
Operator + je definiran kao funkcijski lan, dok je operator - definiran izvan klase.
Matematiki je sasvim ispravno zbrajati ili oduzimati realan i kompleksan broj, ali e
pokuaj zbrajanja u sljedeem primjeru izazvati nevolje:
int main() {
Kompleksni a, b;
b = 5 + a; // pogreka: 5 se ne konvertira u Kompleksni
b = 5 - a; // OK: 5 se konvertira u Kompleksni
return 0;
}
5 + a
interpretira kao
5.operator+(a) // pogreka
to je oigledna glupost, jer je objekt s lijeve strane ugraeni tip za koji se ne mogu
uope pozivati funkcijski lanovi. Naprotiv, poziv
5 - a;
se interpretira kao
operator-(5, a); // OK
Tako nije mogue, na primjer, definirati novi operator za zbrajanje realnih brojeva, jer
se time zapravo ne preoptereuje operator za novi tip, nego se mijenja znaenje
ugraenog operatora za ve postojee tipove:
a = b += c;
10.3.1. Operator =
Iako je mogue operator pridruivanja redefinirati za sasvim egzotine primjene koje
nemaju veze s dodjelom (na primjer redefinirati ga tako da ima znaenje provjere
jednakosti), to se ne preporua. Naime, u tom sluaju nee biti mogue dodjeljivati
vrijednosti jednog objekta drugom. Pakostan programer koji eli zatititi svoj izvorni
kd moe i tome doskoiti, tako da pridruivanje smjesti, primjerice, u operator % i time
si definitivno osigurati prvu nagradu na natjeaju za najlueg hackera u Jankomiru.
Operator pridruivanja mora biti definiran kao funkcijski lan. Ako klasa ne
definira operator pridruivanja, prevoditelj e sam generirati
podrazumijevani operator koji e primijeniti operator pridruivanja na svaki
lan klase.
Razlozi zbog kojih to ponekad nije prihvatljivo su isti kao i razlozi zbog kojih
podrazumijevani konstruktor kopije nije ispravan. Ako klasa koristi dinamiku alokaciju
memorije, potrebno je prilikom pridruivanja osloboditi staru memoriju i alocirati novu.
To emo pokazati na primjeru klase ZnakovniNiz. Naime, ako napiemo
class ZnakovniNiz {
// ...
public:
ZnakovniNiz& operator=(const ZnakovniNiz &desno);
};
U gornjem kdu se prije dodjele oslobaa memorija na koju pokazuje pokNiz prije
pridruivanja te se alocira novi memorijski prostor za smjetaj niza s desne strane.
Vano je primijetiti da je potpuno ispravno pridodijeliti objekt samome sebi. Zato se
prvo ispituje je li adresa desnog operanda razliita od adrese objekta kojemu se
vrijednost pridruuje. Kada tog uvjeta ne bi bilo, naredbom
a = a;
ZnakovniNiz a, b;
b = a = "copycat";
se interpretira kao
b.operator=(a.operator=(ZnakovniNiz("copycat")));
10.3.2. Operator []
Operator [] prvenstveno se koristi za dohvaanje lanova polja te se zove operator za
indeksiranje (engl. indexing operator). On mora uvijek biti definiran kao funkcijski lan
klase. Izraz
x[y];
se interpretira kao
x.operator[](y);
class ZnakovniNiz {
// ...
public:
char& operator[](int n) { return pokNiz[n]; }
// ...
};
Ovim je nainom omoguen pristup pojedinim znakovima znakovnog niza na isti nain
kao i u sluaju obinih znakovnih nizova (char *), na primjer
Zbog toga se kao rjeenje namee razviti klasu Matrica kojom bi se taj nedostatak
izbjegao. Takva klasa mora biti indeksirana po dvije dimenzije, jer bi u suprotnom
dohvaanje pojedinih lanova bilo vrlo neitko. Zato emo se morati posluiti raznim
trikovima. Osnovna ideja je da se izraz
obj[m][n]
interpretira kao
(obj[m])[n]
Prvo se operator [] primjeni na objekt obj. Operator vraa privremeni objekt u kojem
je pohranjen podatak o prvoj dimenziji. Na taj se objekt zatim primijeni operator [] koji
sada odreuje drugu dimenziju. Privremeni objekt zna sada i prvu i drugu dimenziju te
moe obaviti indeksiranje matrice. Evo rjeenja problema:
class Matrica {
private:
float *mat;
int redaka, stupaca;
class Pristupnik {
public:
int prvadim, stupaca;
float *mat;
Pristupnik(int pd, int st, float *mt) :
prvadim(pd), stupaca(st), mat(mt) {}
float& operator[](int drugadim) {
return mat[prvadim * stupaca + drugadim];
}
};
public:
Matrica(int red, int stu);
~Matrica() { delete [] mat; }
Pristupnik operator[](int prvadim) {
return Pristupnik(prvadim, stupaca, mat);
}
};
U klasi Matrica lanovi redaka i stupaca pamte broj redaka i stupaca matrice te se
oni moraju navesti u konstruktoru, ime se matrica postavlja na neku poetnu dimenziju.
Pokaziva mat pokazuje na podruje memorije veliine redaka * stupaca, te se u
njega smjetaju lanovi matrice. Podruje je alocirano kao jednodimenzionalan niz zato
jer prilikom koritenja operatora new [] za alokaciju niza sve dimenzije osim prve
317
moraju biti poznate prilikom prevoenja. U naem sluaju su obje dimenzije nepoznate
prilikom prevoenja, te se pozicija elementa unutar podruja izraunava prilikom
pristupa elementu.
Klasa ima preoptereen operator [] koji kao rezultat vraa privremeni objekt klase
Pristupnik. Konstruktorom se objekt inicijalizira tako da se zapamti prva dimenzija,
broj stupaca i pokaziva na poetak matrice. Klasa Pristupnik ima takoer operator
[] koji vraa referencu na element matrice.
Nedostatak ovakve simulacije dvodimenzionalnog indeksiranja jest taj da se svaki
put prilikom pristupa matrici stvara privremeni objekt koji se unitava na kraju izraza,
to moe bitno usporiti rad programa. Ponekad to moe biti jedino rjeenje, pogotovo
ako je potrebno provjeravanje je li indeks unutar dozvoljenog raspona, no na problem
moemo pokuati rijeiti i drukije. Ideja je da operator [] kao svoj rezultat vrati
pokaziva na podruje unutar matrice gdje poinje traeni redak. Na to se primjeni
ugraeni operator [] koji oznaava pomak od pokazivaa te se njime pristupi traenom
elementu matrice. Sada vie nije mogue provjeriti da li je drugi indeks unutar
dozvoljenog podruja, no indeksiranje ide bre. Klasa Pristupnik vie nije potrebna:
class Matrica {
private:
float *mat;
int redaka, stupaca;
public:
Matrica(int red, int stu);
~Matrica() { delete [] mat; }
float *operator[](int prvadim) {
return mat + prvadim * stupaca;
}
};
Ovakvo rjeenje radi bre, no mogue ga je primijeniti samo zato jer smo na prikladan
nain realizirali implementaciju objekta (matricu smo pamtili po recima). Veliki je
nedostatak u tome to operator vraa pokaziva u implementaciju, ime se omoguava
programeru da mijenja unutranjost objekta na nain zavisan od implementacije. To
ugroava integritet objekta jer ako se pokae da je matricu potrebno pamtiti na drukiji
nain, program koji koristi pokazivae unutar objekta pada u vodu. Kao sve stvari u
ivotu, C++ programi su kompromisi izmeu elja i mogunosti.
10.3.3. Operator ()
Operator za poziv funkcije () mora biti definiran kao funkcijski lan klase te moe
imati niti jedan ili proizvoljan broj parametara, a takoer moe pomou ... (tri toke)
imati neodreeni broj parametara. Izraz
x( parametri );
se interpretira kao
318
x.operator()( parametri );
class ZnakovniNiz {
// ...
public:
ZnakovniNiz operator()(int start, int duljina);
};
int main() {
ZnakovniNiz a = "Kramer protiv Kramera";
cout << (const char *)a(2, 17) << endl;
return 0;
}
x->m
319
(x.operator->())->m
#include <iostream.h>
class Z {
public:
int clan;
Z(int mm) : clan(mm) {}
};
class Y {
public:
Z &referencaNaZ;
int clan;
Y(Z &rz, int mm) : referencaNaZ(rz), clan(mm) {}
Z *operator ->() {
cout << "Pozvan oeprator -> od Y" << endl;
return &referencaNaZ;
}
};
class X {
public:
Y &referencaNaY;
int clan;
X(Y &ry, int mm) : referencaNaY(ry), clan(mm) {}
Y& operator ->() {
cout << "Pozvan operator -> od X" << endl;
return referencaNaY;
}
};
int main() {
Z objZ(1);
Y objY(objZ, 2);
X objX(objY, 3);
cout << "lan: " << objX->clan;
320
return 0;
}
class XX {
// klasa XX je sam objekt koji ima svoje lanove
// ovisno o samom problemu koji se rjeava
};
class PokXX {
private:
int uMemoriji; // postavljen na 1 pokazuje da
// je objekt trenutno u memoriji
XX *pokXX; // pokaziva na objekt u memoriji
// preostali lanovi ovise o nainu
// na koji se objekti smjetaju na disk
public:
XX* operator->();
};
XX* PokXX::operator->() {
if (!uMemoriji) {
// pozovi objekt s vanjske jedinice
uMemoriji = 1;
}
return pokXX;
Dakle, izraz
a->*b
se interpretira kao
a.operator->*(b)
operator->*(a, b)
class Primjer {
// ...
void operator++();
};
Operatorska funkcija moe biti lan klase, a moe biti i definirana izvan klase te tada
uzima jedan parametar. Postfiks operator ++ ima isti potpis te se prilikom prevoenja ne
moe razaznati da li definicija operatorske funkcije pripada prefiks ili postfiks verziji.
Kako bi se ipak omoguilo preoptereenje i jedne i druge varijante operatora, uvedena
je konvencija po kojoj postfiks verzija ima jo jedan dodatni parametar tipa int. Evo
primjera definicija prefiks i postfiks verzija operatora:
class Primjer {
public:
// deklaracije unutar klase
void operator++(); // prefiks verzija
void operator++(int); // postfiks verzija
};
int main() {
Primjer obj;
obj--; // prevoditelj daje svoju
// vrijednost parametra
obj.operator++(76); // eksplicitni poziv postfiksne
// funkcije s navedenim parametrom
operator--(obj); // prefiks operator
operator--(obj, 32); // postfiks operator
return 0;
}
323
// primjer poziva
Kompleksni *pok1 = new (67, "Naziv") Kompleksni(6.0, 7.0);
Prvi parametar daje veliinu bloka kojeg je potrebno alocirati i on je obavezan. Njega
izraunava sam prevoditelj prilikom poziva operatora te ga nije mogue navesti
324
prilikom poziva. Preostali parametri se navode u okruglim zagradama prije naziva tipa
kojeg je potrebno alocirati. Prilikom traenja prikladne verzije operatora potuju se
pravila preoptereenja, to znai da se parametri moraju poklapati po tipovima i
pozicijama. Ako operator new nema parametara, zagrade se ne navode. Operator mora
vratiti pokaziva na alociranu memoriju. Isto tako izgleda i deklaracija operatora
new []:
// primjer poziva
Vektor *pok2 = new (49) Vektor[4];
// primjeri poziva
delete pok1;
delete [] pok2;
Iako klasa moe imati preoptereen operator new, ponekad je potrebno pozvati
globalnu verziju operatora new. To se moe uiniti pomou operatora za odreivanje
podruja tako da se navede ::new. U suprotnom, ako se operator :: izostavi, pozvat e
se operator new iz klase. Na primjer:
class Primjer {
public:
void *operator new(size_t vel);
};
int main() {
Primjer *pok1 = new Primjer;// poziva se new iz
// klase Primjer
Primjer *pok2 = ::new Primjer; // poziva se globalna
// verzija
return 0;
}
Reeno vrijedi i za operator new[]: ako elimo pozvati globalni operator, napisat emo
::new[].
325
#include <stddef.h>
#include <malloc.h>
#include <string.h>
#include <iostream.h>
struct Alokacija {
Alokacija *sljeceda;
void *mjesto;
int linija;
size_t velicina;
char datoteka[80];
};
Prva = pom->sljeceda;
if (!pom->sljeceda) Zadnja = pom1;
free(pom);
break;
}
pom1 = pom;
pom = pom->sljeceda;
}
}
void IspisiListu() {
Alokacija *pom = Prva;
while (pom) {
cout << pom->linija << ":";
cout << pom->datoteka << ":";
cout << pom->velicina << endl;
pom = pom->sljeceda;
}
}
Svaka alokacija memorije se biljei u listi. Prilikom izlaska iz programa lista se moe
ispisati te se moe ustanoviti je li sva memorija ispravno vraena. Operatori new i
new [] imaju dva parametra pomou kojih se moe locirati mjesto u programu na
kojem je alokacija obavljena. Prvi parametar je broj linije kda, a drugi je naziv
datoteke u kojoj je naredba smjetena. Pretprocesorski simbol _ _LINE_ _ se prilikom
prevoenja zamjenjuje brojem linije, dok se simbol _ _FILE_ _ zamjenjuje nazivom
datoteke. Evo primjera poziva preoptereenog operatora new:
int main() {
char *p = new (_ _LINE_ _, _ _FILE_ _) char[20];
327
class Polinom {
private:
int stupanj;
float *koef;
public:
Polinom(int pocetniStupanj);
};
rn = an
rn 1 = rn x + an 1
...
r0 = r1 x + a0
Raunaju se redom brojevi ri po gornjim formulama. Pri tome x predstavlja toku u
kojoj se izraunava vrijednost polinoma, a ai predstavlja i-ti koeficijent polinoma.
Svaki ozbiljan objektno orijentirani jezik koji danas postoji ima implementiran barem
nekakav rudimentaran mehanizam nasljeivanja, koji znaajno moe skratiti vrijeme
potrebno za razvoj sloenih programa. Nasljeivanje u jeziku C++ je vrlo razraeno, to
omoguava brzo i efikasno stvaranje novih klasa iz ve postojeih.
Osnovna ideja nasljeivanja jest da se prilikom razvoja identificiraju klase koje
imaju slinu funkcionalnost, te da se u izvedenim klasama samo redefiniraju specifina
svojstva, dok se preostala svojstva nasljeuju u nepromijenjenom obliku. Razumijevanje
mehanizama nasljeivanja i graenja hijerarhije klasa je od kljune vanosti za ispravnu
primjenu koncepta objektno orijentiranog programiranja.
class GrafObjekt {
private:
int Boja;
public:
void PostaviBoju(int nova) { Boja = nova; }
int CitajBoju() { return Boja; }
void Crtaj() {}
void Translatiraj(int, int) {}
void Rotiraj(int, int, int) {}
};
class Linija {
private:
int x1, y1, x2, y2;
public:
GrafObjekt osnovna;
void Crtaj();
void Translatiraj(int vx, int vy);
void Rotiraj(int cx, int cy, int kut);
};
Kada bi se sada htjelo postaviti boju nekoj liniji, to bi se moralo uiniti ovako:
#define CRVENA 14
Linija l;
l.osnovna.PostaviBoju(CRVENA);
l.Crtaj();
l.osnovna.Crtaj();
class Linija {
private:
int x1, y1, x2, y2;
public:
GrafObjekt osnovna;
void PostaviBoju(int nb) { osnovna.PostaviBoju(nb); }
int CitajBoju() { return osnovna.CitajBoju(); }
void Crtaj();
void Translatiraj(int vx, int vy);
void Rotiraj(int cx, int cy, int kut);
};
Iako je ovakvo rjeenje znatno prikladnije, pisanje kda je izuzetno zamorno, a sam
program je optereen funkcijama koje slue samo pozivanju funkcija iz osnovne klase.
Jezik C++ na elegantan nain rjeava sve te probleme pomou nasljeivanja (engl.
inheritance). Pri tome se definira nova izvedena klasa (engl. derived class) na osnovu
ve postojee osnovne klase (engl. base class). Objekti izvedene klase sadre sve
funkcijske i podatkovne lanove osnovne klase, te mogu dodati nova svojstva.
333
Nasljeivanje se specificira tako da se u deklaraciji klase iza naziva klase navede lista
osnovnih klasa, kao u sljedeem primjeru:
Lista osnovnih klasa navodi se tako da se iza naziva klase stavi dvotoka te se navedu
nazivi osnovnih klasa razdvojeni zarezom. Ispred svakog od naziva mogue je staviti
jednu od kljunih rijei public, private ili protected ime se navodi tip
nasljeivanja. Ovime se jednim potezom specificira da e objekt klase Linija
sadravati sve funkcijske i podatkovne lanove klase GrafObjekt. Slian bi se uinak
postigao da se u klasu Linija ukljuio potpun sadraj osnovne klase. Gornjom
deklaracijom je klasa Linija izvedena te nasljeuje svojstva osnovne klase
GrafObjekt.
Podatkovni i funkcijski lanovi osnovne klase u sluaju javnog nasljeivanja (kada
se koristi kljuna rije public u listi nasljeivanja) zadravaju svoje pravo pristupa. To
znai da podatkovni lan Boja ostaje privatan, te mu se ne moe pristupiti niti unutar
naslijeene klase Linija, dok lanovi PostaviBoju() i CitajBoju() imaju javni
pristup te im se moe pristupiti i izvan klase.
Klasa Linija dodaje nove podatkovne lanove x1, y1, x2 i y2 koji e pamtiti
koordinate poetne i krajnje toke linije. U izvedenoj klasi je mogue zamijeniti eljene
funkcijske lanove osnovne klase novim lanovima. Taj postupak se zove zaobilaenje
(engl. overriding). Tako klasa Linija definira svoj postupak za crtanje koje se
razlikovati od postupka za crtanje navedenog u osnovnoj klasi.
Razmotrimo kako se dalje moe graditi hijerarhijsko stablo grafikih objekata.
Neka klasa ElipsinIsj definira elipsin isjeak. Ta klasa sadri podatkovne lanove
centarX i centarY koji pamte koordinate centra elipse, zatim poluosA i poluosB
koji pamte koordinate velike i male poluosi elipse te pocetniKut i zavrsniKut koji
specificiraju poetni i krajnji kut crtanja isjeka. Kako bi se omoguilo postavljanje
parametara objekta, potrebno je dodati funkcijske lanove za postavljanje i itanje svih
podatkovnih lanova. Na prikladan nain se definiraju i funkcijski lanovi za crtanje,
rotaciju i translaciju.
Krug emo opisati klasom Krug. Krug je grafiki objekt koji je zapravo jedna
podvrsta elipsinog isjeka, te je najbolji nain za implementaciju kruga naslijediti klasu
ElipsinIsj. Pri tome je potrebno dodati funkcijske lanove koji e omoguiti
postavljanje i itanje radijusa. Vano je primijetiti da u ovom sluaju nije potrebno
ponovo definirati funkcijski lan za crtanje. Kako je krug elipsin isjeak koji ima obje
poluosi jednake te poetni kut 0, a zavrni 360 stupnjeva, operacija crtanja elipsinog
isjeka e u ovom sluaju nacrtati krug.
Slina je situacija i s poligonom i pravokutnikom. Klasa Poligon opisuje openiti
objekt koji moe imati proizvoljno mnogo vrhova. Klasa Pravokutnik nasljeuje od
klase Poligon te jedino na prikladan nain postavlja podatkovne lanove objekta.
Operacije za translaciju, rotaciju i crtanje nije potrebno ponavljati jer one definirane u
334
GrafObjekt
Krug Pravokutnik
Klasa moe naslijediti i nekoliko drugih klasa. U tom sluaju e svi podatkovni i
funkcijski lanovi svih osnovnih klasa biti ukljueni u novu klasu. Zamislimo da elimo
stvoriti nove grafike objekte koji e sadravati neki tekst. Sve postojee elemente
emo naslijediti kako bi se dodalo novo funkcijsko svojstvo.
Iako je mogue jo jednom proi cijelo hijerarhijsko stablo, te kod svake klase gdje
je to potrebno dodavati elemente potrebne za ispis teksta, jednostavnije je definirati
zasebnu klasu TekstObj koja e definirati tekstovni objekt. Objekti te klase moraju
pamtiti znakovni niz koji se ispisuje te poziciju niza na ekranu. Dodavanje teksta na bilo
koji postojei grafiki element sada se jednostavno moe napraviti nasljeivanjem klase
TekstObj i eljene klase grafikog objekta. Na primjer, klasa TPravokutnik koja
opisuje pravokutnik u kojem je ispisan tekst, nasljeuje klasu TekstObj i klasu
Pravokutnik, dok se klasa TKrug koja opisuje krug s upisanim tekstom dobiva tako
da se naslijedi klasa TekstObj i klasa Krug. Takvo hijerarhijsko stablo prikazano je na
slici 11.2.
GrafObjekt
TKrug TPravokutnik
// osnovne klase
class GrafObjekt {};
class TekstObj {};
// izvedene klase
class Poligon : public GrafObjekt {};
class Pravokutnik : private Poligon {};
class TPravokutnik : Pravokutnik, public TekstObj {};
Usprkos tome, dobra je navika i u sluaju privatnog nasljeivanja navesti kljunu rije
private kako bi se izbjegle mogue zabune. Takav kd je daleko pregledniji i manje
podloan pogrekama.
Prilikom nasljeivanja, jedna klasa se ne moe navesti vie nego jednom u listi
nasljeivanja. To je i logino, jer bi se u suprotnom unutar objekta izvedene klase
nalazila primjerice dva objekta osnovne klase, to bi rezultiralo nedoumicom kojem se
objektu od njih pristupa.
Svi lanovi, funkcijski i podatkovni, osnovnih klasa automatski se prenose u
izvedenu klasu. Tim lanovima je mogue pristupati kao da su navedeni u izvedenoj
klasi (s time da vrijede prava pristupa). Zapravo, objekt koji osnovna klasa definira je u
cijelosti ukljuen kao podobjekt u izvedenu klasu. Time je izbjegnuta potreba za
prepisivanjem sadraja klase prilikom nasljeivanja. Na primjer, neka su klase
definirane ovako:
336
Linija ln;
ln.Rotiraj(10, 10, 10); // pristup lanu klase Linija
ln.PostaviBoju(CRNA); // pristup osnovnoj klasi
Kako bismo smisao nasljeivanja pojasnili u praksi, opisat emo jedan od moguih
naina za realizaciju dvostruko vezane liste objekata. Vezana lista se esto koristi u
sluajevima kada nije poznat ukupan broj objekata koji se ele zapamtiti, ili kad se taj
broj mijenja tijekom izvoenja programa. Naime, ako objekte elimo pamtiti u polju,
potrebno je prilikom alokacije niza navesti broj objekata. Takoer, ako elimo ubaciti
novi objekt u polje na odreeno mjesto, ili ako elimo izbrisati neki objekt iz polja, a da
pri tome ne ostavimo rupu, morat emo premjetati lanove polja uzdu i poprijeko.
Vezana lista (engl. linked list) je u tom sluaju znatno efikasnije rjeenje.
Prije nego to zaponemo objanjavati princip rada liste te damo programski kd
koji ju realizira, evo vane napomene: lista je kontejnerska klasa, a takve klase se u C++
jeziku daleko kvalitetnije rjeavaju predlocima. Tome je vie razloga, a osnovni je taj
to predloci znaju toan tip objekta koji se smjeta u kontejner. Naprotiv, rjeenje koje
je ovdje prikazano ne zna toan tip objekta, pa tako kontejner niti ne moe pristupiti
specifinostima objekta. Ovakav pristup rezultira intenzivnim koritenjem operatora za
dodjelu tipa kako bi se izigrao sistem statikih tipova koji je duboko usaen u C++.
Zbog toga e lista biti ponovo obraena u poglavlju 11. No i ovakva realizacija liste ima
svoje prednosti u sluaju kada u listi elimo uvati objekte razliitih klasa. Tada nam
predloci ne pomau znatno, a koritenje dodjela tipova je neumitno.
337
Vezana lista se ostvaruje tako da svaki lan liste osim vrijednosti sadri i pokaziva
na sljedei element liste. U jaoj varijanti vezanih lista dvostruko povezanoj listi
svaki lan liste sadrava i pokaziva na prethodni lan. U naem primjeru to e biti
pokazivai pokPrethodni i pokSljedeci. Struktura svakog lana liste prikazana je
na slici 11.3a. Kako nema lana ispred prvoga, njegov pokPrethodni bit e jednak
nul-pokazivau, ime se signalizira poetak liste. Ista je stvar i s posljednjim lanom
liste: njegov pokSljedeci e biti jednak nul-pokazivau ime se oznaava kraj liste.
Samoj listi se pristupa preko dva pokazivaa: glava i rep. Pokaziva glava
pokazuje na prvi lan liste, dok pokaziva rep pokazuje na posljednji lan. Shematski je
to prikazano na slici 11.3b. Pojedini lan se dohvaa tako da se lista slijedno pretrauje,
tako da se krene od glave ili od repa, te se uzastopno iitavaju pokazivai na sljedei
odnosno prethodni lan. Umetanje i brisanje lana iz liste je trivijalno: umjesto da se
lanovi premjetaju po memoriji i time troi vrijeme, potrebno je samo promijeniti
pokazivae prethodnog i sljedeeg lana. Ono to listama gubimo jest mogunost
izravnog dohvaanja lana pomou indeksa da bismo dohvatili pojedini lan uvijek je
potrebno krenuti od glave ili od repa i postupno doi do eljenog lana.
Vrijednost
pokSljedeci
a)
pokprethodni
glava rep
b)
0
0
Zamislimo sada da elimo u listu sa slike 11.3b umetnuti novi lan izmeu prvog i
drugog lana. Pri tome je samo potrebno preusmjeriti pokaziva pokSljedeci prvog
lana i pokaziva pokPrethodni drugog lana na novi lan. Pokazivae
pokPrethodni i pokSljedeci novoga lana potrebno je usmjeriti na prvi odnosno
drugi lan liste, kao na slici 11.4.
Definirat emo klasu Atom koja e opisivati objekt koji se dri u listi. Ta klasa e
sadravati pokaziva na prethodni i sljedei lan u listi, koji e biti privatni kako bi im
se sprijeio pristup izvan klase. Takoer, klasa Atom e sadravati funkcijske lanove za
338
glava rep
0
0
class Atom {
private:
Atom *pokSljedeci, *pokPrethodni;
public:
Atom *Sljedeci() { return pokSljedeci; }
Atom *Prethodni() { return pokPrethodni; }
void StaviSljedeci(Atom *pok) { pokSljedeci = pok; }
void StaviPrethodni(Atom *pok) { pokPrethodni = pok; }
};
Evo i klase Lista koja e sadravati funkcijske lanove za umetanje i brisanje lanova.
Pri tome klasa Lista nije odgovorna za alokaciju memorije za pojedini lan liste. To
mora uiniti vanjski program pomou operatora new. Funkcijskim lanovima klase
Lista se tada samo prosljeuje pokaziva na taj lan.
class Lista {
private:
Atom *glava, *rep;
public:
Lista() : glava(NULL), rep(NULL) {}
Atom *AmoGlavu() { return glava; }
Atom *AmoRep() { return rep; }
void UgurajClan(Atom *pok, Atom *izaKojeg);
void GoniClan(Atom *pok);
};
// da li se dodaje na kraj?
if (izaKojeg->Sljedeci() != NULL)
// ne dodaje se na kraj
izaKojeg->Sljedeci()->StaviPrethodni(pok);
else
// dodaje se na kraj
rep = pok;
pok->StaviSljedeci(izaKojeg->Sljedeci());
izaKojeg->StaviSljedeci(pok);
}
else {
// dodaje se na poetak
pok->StaviSljedeci(glava);
if (glava != NULL)
// da li ve ima lanova u listi?
glava->StaviPrethodni(pok);
glava = pok;
}
pok->StaviPrethodni(izaKojeg);
}
Kao posljedica nasljeivanja, u svakom LVektor objektu postoji podobjekt Atom koji
sadrava sve potrebno da bi se objekt mogao pohraniti u listu. Prisutni su svi podatkovni
i funkcijski lanovi. Javnim lanovima je mogue pristupiti i na klasian nain:
LVektor lv;
lv.StaviSljedeci(NULL);
if (lv.Prethodni()) {
// ...
}
Sada uope nije potrebno ponavljati funkcijske lanove za postavljanje i itanje realnog
i imaginarnog dijela; oni su jednostavno naslijeeni iz klase Vektor. Grafiki se objekt
klase LVektor moe prikazati kao na slici 11.5.
LVektor
Vektor Atom
Lista lst;
// punjenje liste
lst.UgurajClan(new LVektor(10.0, 20.0));
lst.UgurajClan(new LVektor(-5.0, -6.0));
341
// ...
// itanje liste
LVektor *pok = (LVektor *)lst.AmoGlavu();
while (pok) {
// obrada vektora, na primjer ispis:
cout << "(" << pok->DajX() << ","
<< pok->DajY() << ")" << endl;
pok = (LVektor *)pok->Sljedeci();
}
LVektor lv;
lv.Atom::StaviSljedeci(NULL);
lv.Vektor::PostaviXY(5, 8);
#include <iostream.h>
class Osnovna {
public:
int i;
void Var() { cout << "Osnovna::i " << i << endl; }
};
int main() {
Izvedena izv;
izv.i = 9; // pristupa se Izvedena::i
izv.Osnovna::i = 20; // pristupa se Osnovna::i
izv.Var(); // ispisuje 'Osnovna::i 20'
izv.Int(); // ispisuje 'Izvedena::i 9'
return 0;
}
Situacija je neto sloenija ako u osnovnim klasama postoje dva ili vie ista
identifikatora.
Ako dvije osnovne klase sadre isti identifikator, njegovo navoenje bez
specificiranja pripadnog podruja je neprecizno te rezultira pogrekom
prilikom prevoenja.
class A {
public:
void Opis() { cout << "Ovo je klasa A" << endl; }
};
class B {
public:
void Opis() { cout << "Ovo je klasa B" << endl; }
};
D obj;
obj.Opis(); // pogreka: dvosmislenost
Objekt D u biti posjeduje dva funkcijska lana Opis, jedan u A dijelu i jedan u B dijelu.
Prilikom poziva neophodno je eksplicitno odrediti kojemu se pristupa:
D obj;
obj.A::Opis(); // ispisuje 'Ovo je klasa A'
obj.B::Opis(); // ispisuje 'Ovo je klasa B'
D obj;
obj.Opis(); // ispisuje 'Ovo je klasa D'
prava pristupa pojedinim lanovima klase definira se javno suelje pomou kojeg
objekt komunicira s okolinom.
Uvoenjem nasljeivanja, osim javnog suelja namijenjenog korisnicima klase,
potrebno je uvesti i suelje vidljivo programerima koji e izvoditi nove klase iz
postojee. Time je mogue sakriti osjetljive informacije u izvedenoj klasi to osigurava
konzistentnost objekta.
Pravo pristupa lanu u izvedenoj klasi odreeno je pravom pristupa u osnovnoj
klasi te vrstom nasljeivanja. Kako postoje tri prava pristupa te tri mogua tipa
nasljeivanja, imamo ukupno devet moguih kombinacija.
class GrafObjekt {
protected: // zatieni lanovi
int Boja;
public:
void PostaviBoju(int nova) { Boja = nova; }
int CitajBoju() { return Boja; }
void Crtaj() {}
void Translatiraj(int, int) {}
void Rotiraj(int, int, int) {}
void PostaviBrojac(int br) { Brojac = br; }
};
U gornjem primjeru klasa GrafObjekt definira lan Boja zatienim. Njemu se i dalje
ne moe pristupiti izvan klase, na primjer, pomou objekta te klase, no mogue ga je
koristiti u funkcijskim lanovima izvedenih klasa. Na primjer:
void Linija::Crtaj() {
// crni objekti se ne crtaju
if (Boja == CRNA) return;
// ...
}
345
Klasa Linija moe potpuno regularno pristupati lanu Boja. To nipoto ne treba
shvatiti da je mogue tom lanu pristupiti pomou objekta klase Linija:
Linija ln;
ln.Boja = CRNA; // pogreka
lan Boja je jednostavno dio suelja za nasljeivanje: time se odreuje koji lanovi su
namijenjeni koritenju u izvedenim klasama. Privatni lanovi nisu dostupni u izvedenim
klasama, a javni lanovi su dostupni i korisnicima objekta.
class GrafObjekt {
private:
int Brojac;
protected:
int Boja;
public:
void PostaviBoju(int nova) { Boja = nova; }
int CitajBoju() { return Boja; }
void Crtaj() {}
void Translatiraj(int, int) {}
void Rotiraj(int, int, int) {}
void PostaviBrojac(int br) { Brojac = br; }
};
Klasi GrafObjekt je dodan cjelobrojni lan Brojac koji pamti redni broj objekta i
inicijalizira se pomou javnog funkcijskog lana PostaviBrojac(). Kako je lan
privatan, to znai da mu se ne moe pristupiti iz klase Linija. Svaki objekt klase e
sadravati taj lan, no nee mu se moi pristupiti, na primjer:
void Linija::Crtaj() {
if (Brojac) { // pogreka: lan nije dostupan
// ...
}
}
#define PLAVA 35
#define BIJELA 22
void Linija::Crtaj() {
if (Boja == PLAVA) { // OK: Boja je zatieni lan
// ...
}
}
int main() {
Linija l;
l.Boja = BIJELA;// pogreka: lan Boja nije
// dostupan izvan klase
return 0;
}
class Vektor {
private:
float ax, ay;
public:
Vektor(float a = 0, float b = 0) : ax(a), ay(b) {}
void PostaviXY(float x, float y) { ax = x; ay = y; }
float DajX() { return ax; }
float DajY() { return ay; }
float MnoziSkalarnoSa(Vektor &vekt);
};
348
Sada nije vie mogue pozvati primjerice lan MnoziVektorskiSa() na objektu klase
Kompleksni.
Nije mogua implicitna konverzija iz izvedene klase u privatnu osnovnu klasu. Ako bi
to bilo dozvoljeno, tada bi sljedei programski odsjeak omoguio pristup klasi na
neprikladan nain preko javnog suelja osnovne klase:
class Osnovna {
public:
int i;
protected:
int j;
private:
int k;
};
void Izvedena::Pristupi() {
i = 8; // OK: lan je preuzet kao protected
j = 9; // OK: lan je preuzet kao protected
k = 10; // pogreka: lan je bio privatan
}
int main() {
Izvedena obj;
obj.i = 7; // pogreka: lan je zatien
obj.j = 6; // pogreka: lan je takoer zatien
obj.k = 5; // pogreka: lan je privatan za osnovnu
350
// klasu
return 0;
}
int main() {
// pogreka: nema ugraene konverzije izvan klase
Osnovna *pok = new Izvedena();
return 0;
}
void Izvedena::Pristupi() {
// OK: ugraena konverzija je ispravna unutar klase
Osnovna *pok = new Izvedena();
}
pogled moe se pomisliti da time nasljeivanje definira odnos slian prijateljstvu klasa,
no to nije tako. Vrlo je vano uoiti bitnu razliku izmeu nasljeivanja i prijateljstva.
Prijateljska funkcija ili klasa ima potpuna prava pristupa svim lanovima objekata
neke klase. Uz modifikacije u deklaraciji klase GrafObjekt, funkcija
AnimirajBoju() moe pristupati svim lanovima objekta:
class GrafObjekt {
friend void AnimirajBoju(); // prijateljska funkcija
private:
int Brojac;
protected:
int Boja;
public:
void PostaviBoju(int nova) { Boja = nova; }
int CitajBoju() { return Boja; }
void Crtaj() {}
void Translatiraj(int, int) {}
void Rotiraj(int, int, int) {}
void PostaviBrojac(int br) { Brojac = br; }
};
void AnimirajBoju() {
GrafObjekt obj;
obj.Brojac++; // dozvoljeno
obj.Boja = (obj.Boja + 1) % 16; // takoer dozvoljeno
}
void AnimirajBoju() {
Linija l;
l.Boja = BIJELA;// dozvoljeno
l.x1 = 8; // nije dozvoljeno
}
352
Ako bismo htjeli da funkcija ima pristup lanu x1, potrebno je funkciju uiniti
prijateljem klase Linija.
Funkcijski lanovi izvedenih klasa imaju mogunost pristupa javim i zatienim
naslijeenim lanovima osnovnih klasa, no nemaju nikakvo posebno pravo pristupa
lanovima objekata osnovnih klasa. Na primjer, funkcijski lan Crtaj() klase Linija
moe pristupiti naslijeenom lanu Boja, no ne moe mu pristupiti u nekom drugom
objektu:
void Linija::Crtaj() {
GrafObjekt go;
Linija ln;
Boja = CRVENA; // OK: pristupa se naslijeenom lanu
go.Boja = CRNA; // pogreka: nema prava pristupa
// objektima osnovnih klasa
ln.Boja = CRNA; // OK: postoje sva prava pristupa za
// objekt iste klase
}
Funkcije prijatelji klasa imaju ista prava pristupa kao da su lanovi te klase. To znai da
u sluaju da je prijateljstvo funkcije AnimirajBoju() pomaknuto u klasu Linija,
sljedei pristupi bi bili mogui:
class GrafObjekt {
private:
int Brojac;
protected:
int Boja;
public:
void AnimirajBoju() {
Linija ln;
GrafObjekt go;
ln.Boja = BIJELA; // OK
go.Boja = BIJELA; // pogreka
ln.Brojac = CRNA; // pogreka
}
353
Pristup ln.Boja je ispravan jer svaki funkcijski lan klase Linija (a time i svaki
prijatelj klase) ima pristup naslijeenim lanovima osnovne klase. Pristup go.Boja nije
ispravan jer funkcijski lan klase Linija (a time i svaki prijatelj klase) nema posebna
prava pristupa lanovima objekata osnovnih klasa. Pristup ln.Brojac nije ispravan jer
je Brojac lan osnovne klase s privatnim pristupom.
class Vektor {
private:
float ax, ay;
public:
Vektor(float a = 0, float b = 0) : ax(a), ay(b) {}
void PostaviXY(float x, float y) { ax = x; ay = y; }
float DajX() { return ax; }
float DajY() { return ay; }
float MnoziSkalarnoSa(Vektor &vekt);
};
Relacije tipa sadri se opisuju pripadnou: kako se poligon sastoji od etiri linije,
ispravna implementacija klase Pravokutnik je ona koja nasljeuje klasu GrafObjekt
i sadri etiri objekta klase Linija:
class GrafObjekt {
private:
int Brojac;
protected:
int Boja;
public:
};
};
class Prva {
public:
Prva(int);
};
class Druga {
357
public:
Druga(Prva &);
};
Zadatak. Dodajte klasi Stog iz zadatka na stranici 354 konstruktor koji e kao
parametar imati cijeli broj koji e definirati maksimalan broj elemenata u stogu.
class Atom {
private:
Atom *pokSljedeci, *pokPrethodni;
public:
// funkcijski lanovi nisu bitni
};
class Vektor {
private:
float ax, ay;
public:
// funkcijski lanovi nisu bitni
};
int redBroj;
// funkcijski lanovi nisu bitni
};
void Linija::Crtaj() {
int Linija::*pokClan;
void Linija::Crtaj() {
int GrafObjekt::*pokClan;
class Atom {
protected:
Atom *pokSljedeci;
// ...
};
class Vektor {
// ...
};
pretrauje se postoji li takav naslijeeni lan u bilo kojoj osnovnoj klasi. Ako takav lan
postoji u vie osnovnih klasa, onda nije jednoznano jasno kojem se lanu pristupa te
prevoditelj javlja pogreku. Zatim se po istom principu pretrauje podruje osnovne
klase sve dok se ne pronae traeni identifikator. Ako se identifikator ne pronae, javlja
se pogreka. Tako je u gornjem primjeru lan pokSljedeci u funkcijskom lanu
DajSljedeci() identificiran kao naslijeeni lan Atom::pokSljedeci.
class Osnovna {
public:
void funkcija();
};
int main() {
Izvedena obj;
// pogreka: nasuprot oekivanju da e donji poziv
// pozvati Osnovna::funkcija() to ne funkcionira jer
// lan Izvedena::funkcija(int) skriva istoimeni lan
// osnovne klase
obj.funkcija();
return 0;
}
class SkupNizova {
// ...
};
class Elem {
friend class SkupNizova;
protected:
Elem(char *niz);
};
Ovakav pristup ima vie nedostataka. Kao prvo, naziv Elem se pojavljuje u globalnom
podruju imena iako se ne moe koristiti iz tog podruja. Time se onemoguava
njegova upotreba za klasu koja e imati smisla u globalnom podruju. Nadalje, ako se
kasnije eli proiriti skup novim operacijama, to se moe izvesti tako da se naslijedi
klasa SkupNizova. Problem je to je novu klasu potrebno navesti prijateljem klase
Elem kako bi dotina klasa imala pristup konstruktoru te mogla stvarati objekte klase.
Konano, mogue bi bilo naslijediti klasu Elem kako bi se primjerice osigurao rad
dvostruke povezane liste. U tom sluaju, naslijeena klasa bi morala ponoviti sve klase
kao prijatelje da bi im omoguila stvaranje objekata klase. Takvo pisanje sloenih
programa je vrlo nepraktino.
Ti problemi se mogu rijeiti tako da se klasa Elem ugnijezdi u podruje klase
SkupNizova. Ako se postavi u privatni ili zatieni dio, glavni program nee imati
pristup identifikatoru te e time automatski biti onemogueno stvaranje objekata klase.
Nadalje, naziv Elem bit e automatski uklonjen iz globalnog podruja.
Ako se klasa Elem ugnijezdi u zatieni dio klase SkupNizova, sve klase koje
nasljeuju klasu SkupNizova automatski e moi koristiti klasu Elem. Kako nema vie
opasnosti od pristupa izvan klase SkupNizova, klasa Elem sada moe imati i javni
konstruktor, te vie nema potrebe za davanjem posebnih prava pristupa klasama koje
koriste klasu Elem. Opisane deklaracije klasa sada bi izgledale ovako:
363
class SkupNizova {
protected:
class Elem {
Elem(char *niz);
// ...
};
// ...
};
Pristup ugnijeenoj klasi iz izvedene klase slijedi prava pristupa za obine lanove:
mogunost pristupa ovisi o pravu pristupa u osnovnoj klasi i o tipu nasljeivanja, a
pravila koja to reguliraju opisana su u prethodnim odjeljcima.
lanovi ugnijeene klase se mogu definirati i izvan okolne klase. Tada se podruja
jednostavno nadovezuju operatorom :: po sljedeem principu:
SkupNizova::Elem::Elem(char *niz) {
// ...
}
Takoer, ugnijeena klasa moe posluiti kao objekt nasljeivanja, s time da u tom
sluaju ona mora biti javno dostupna:
class SkupNizova {
public:
class Elem {
Elem(char *niz);
};
};
listi parametara navodi neku drugu klasu. Jezik C++ definira niz pravila kojima se tada
navedeni tip moe pokuati svesti na tip naveden u parametrima preoptereene funkcije
i tako odrediti poziv. Ta pravila ukljuuju tono podudaranje tipova, primjenu
standardne konverzije i primjenu korisniki definiranih konverzija.
Linija lin;
U gornjem primjeru se poziv funkcije preusmjerava na funkciju koja ima toan tip kao
parametar. Prilikom preoptereenja funkcija vano je imati na umu da algoritam za
odreivanje argumenata ne moe razlikovati objekt i referencu na taj objekt, te e takva
preoptereenja prouzroiti pogreku prilikom preoptereenja:
Linija lin;
// OK: lin se konvertira u GrafObjekt&
Deformiraj(lin);
GrafObjekt grobj;
// pogreka: nema konverzije u izvedenu klasu
ProduljiLiniju(grobj);
Ako postoje dvije funkcije od kojih u parametrima svaka ima po jednu neposrednu
javnu osnovnu klasu, poziv funkcije se smatra automatski nejasnim te se javlja pogreka
prilikom prevoenja. Programer tada mora pomou eksplicitne dodjele tipa odrediti
koja se varijanta funkcije poziva:
LVektor lv;
//pogreka: postoje funkcije s obje javne osnovne klase
Obradi(lv);
Ako postoji vie osnovnih klasa, pri emu je samo jedna blia klasi navedenog objekta,
provodi se pretvorba u taj tip te se poziva odgovarajua funkcija:
class A { };
class B : public A { };
class C : public B { };
int main() {
C objc;
// OK: poziva se funkcija(B &) jer je klasa B blia
// klasi C nego klasa A
funkcija(objc);
return 0;
}
366
Tip void * je najudaljeniji od bilo koje klase, odnosno neki pokaziva e biti pretvoren
u taj tip samo ako niti jedna druga konverzija nije mogua.
class Kompleksni {
private:
double cx, cy;
public:
Kompleksni(double a = 0, double b = 0)
: cx(a), cy(b) {}
// ...
};
Ako bismo pozvali primjerice funkciju Korijen1() tako da joj kao parametar
navedemo realan broj, prvo bi se pozvao konstruktor klase Kompleksni koji bi
konvertirao realan broj u kompleksni tako to bi stvorio privremeni objekt. Taj objekt bi
se zatim proslijedio pozvanoj funkciji. Na primjer:
Ovako korisnik klase tretira realne i kompleksne brojeve na isti nain. No i cijeli brojevi
su kompleksni. Na svu sreu, nije potrebno posebno navoditi konstruktor koji e
konvertirati cijele brojeve u kompleksne zato jer postoji ugraena konverzija izmeu
cijelih i realnih brojeva. Poziv
367
class Vektor {
// ...
operator Linija();
};
Ako postoji funkcija koja kao parametar oekuje objekt klase GrafObjekt, mogue joj
je kao parametar navesti objekt klase Vektor:
Vektor v;
368
NestoRadi((Linija)v);
// rezultira konverzijom objekta klase Vektor u objekt
// klase Linija koja se ugraenom konverzijom moe
// svesti na klasu GrafObjekt
class A {
public:
// ...
operator double();
operator Kompleksni();
};
A obj_a;
Primjerice, ako u osnovnoj klasi imamo operator koji usporeuje objekt klase s cijelim
brojem te ga u izvedenoj klasi redefiniramo kao operator za usporedbu klase sa
znakovnom nizom, sljedei programski odsjeak e prouzroiti pogreku prilikom
prevoenja:
369
class Osnovna {
public:
// ...
int operator==(int i);
};
Izvedena obj;
Prva usporedba se moe prevesti jer u klasi Izvedena postoji operator koji moe
usporediti objekt sa znakovnim nizom. Druga usporedba, naprotiv, nije ispravna jer je
operator usporedbe definiran u osnovnoj klasi, te ga operator u izvedenoj klasi skriva
nasljeivanje nije isto to i preoptereenje. Ako elimo u klasi Izvedena ostaviti
mogunost usporedbe sa cijelim brojevima te dodati jo usporedbu sa znakovnim
nizovima, potrebno je ponoviti definiciju operatora operator==(int) i u izvedenoj
klasi:
Svaka izvedena klasa mora definirati svoj operator pridruivanja. Jasna je i svrha tog
pravila: operator pridruivanja mora inicijalizirati cijeli objekt, a ne samo njegov dio.
Operator pridruivanja osnovne klase moe inicijalizirati samo dio objekta koji je
naslijeen, a dio objekta koji je dodan ostat e neinicijaliziran. No pogledajmo to e se
dogoditi u sluaju da pokuamo prevesti sljedei dio koda:
class Osnovna {
public:
// ...
370
Izvedena izv;
izv = 2; // pogreka prilikom prevoenja
Osnovna klasa definira operator kojim je mogue nekom objektu pridruiti cijeli broj.
Izvedena klasa ne definira operator pridruivanja, no definira konstruktor. Kako se
operator pridruivanja ne nasljeuje, klasa Izvedena nee imati operator pridruivanja
te e se primjenjivati podrazumijevani operator koji e provesti kopiranje objekata
jedan u drugi, bit po bit. No da bi se to moglo provesti, objekti moraju biti istog tipa.
Prevoditelj e zbog toga pokuati konvertirati cijeli broj 2 u objekt klase Izvedena te
e pronai konstruktor s jednim parametrom koji e obaviti konverziju. Takav
konstruktor ne postoji te e se prijaviti pogreka.
Operacija crtanja primijenjena na taj objekt bi uvijek trebala rezultirati crtanjem linije.
No klasa GrafObjekt sadri funkcijski lan Crtaj() koji nita ne radi, zato jer ona
definira samo opa svojstva svih grafikih objekata. Izneseni problem se moe ilustrirati
sljedeim programskim odsjekom:
class GrafObjekt {
public:
// ...
void Crtaj() {}
};
e pozvati funkcijski lan Crtaj() iz klase GrafObjekt, zato jer pokaziva pokGO
pokazuje na objekt klase GrafObjekt. Ovime operacija crtanja, umjesto da je vezana
za objekt, postaje ovisna o nainu poziva. Takvo ponaanje znatno naruava objektno
orijentirani pristup programiranju.
Na prvi pogled se gornje ponaanje moe initi loginim: korisnik mora naznaiti
kako eli gledati na objekt. U prvom sluaju korisnik gleda na objekt kao na liniju, pa se
stoga i poziva operacija iz klase Linija. U drugom sluaju korisnik eli dotini objekt
promatrati kao podobjekt poetnog objekta pa se zbog toga poziva pripadna funkcija
crtanja. Ovakvo ponaanje je u suprotnosti s konceptima objektno orijentiranog
programiranja. Svaki objekt ima operacije koje se uvijek izvode na isti nain, to
odgovara stvarnom ponaanju objekata u prirodi.
Na primjer, mogli bismo sve ljude klasificirati u hijerarhiju klasa koja poinje s
podjelom ljudi po boji koe. Svaki (razuman) ovjek e odgovoriti na pitanje Kako se
373
// ...
nizObj[0] = new Linija;
nizObj[1] = new Poligon;
nizObj[2] = new ElipsinIsj;
// na isti nain se mogu popuniti i preostali lanovi polja
// ...
Svaki objekt tono zna kako treba obaviti pojedinu radnju te nain njenog izvoenja
sada ne ovisi o nainu gledanja na objekt. No ovaj primjer, kako je napisan, sam za sebe
ne radi. Kako je svaki element polja nizObj pokaziva na GrafObjekt, operacija
crtanja pozivat e GrafObjekt::Crtaj(). Ovakvo odreivanje funkcijskog lana
pomou tipova dostupnih prilikom prevoenja zove se statiko povezivanje (engl. static
binding).
Promotrimo kako bi se izneseni problem mogao rijeiti. Osnovna potekoa je u
tome to prevoditelj prilikom prevoenja ima dostupnu samo informaciju o tipu
pokazivaa, odnosno o nainu gledanja. Taj pokaziva moe pokazivati na razne
objekte, te bismo htjeli u pojedinim situacijama pozivati odgovarajue funkcijske
lanove razliitih klasa. Oito se prilikom prevoenja ne moe odrediti koja se funkcija
poziva, nego se odluka o samom pozivu mora odgoditi za trenutak izvoenja programa.
Zajedno s objektom, u memoriju mora biti pohranjen podatak o stvarnom tipu objekta.
Prilikom prevoenja poziva funkcije prevoditelj mora stvoriti kd koji e proitati tu
informaciju te na osnovu nje odrediti koja se funkcija u poziva. Takvo odreivanje
pozvanih lanova naziva se u objektno orijentiranim jezicima kasno povezivanje (engl.
late binding), a u C++ terminologiji uvrijeen je naziv dinamiko povezivanje (engl.
dynamic binding).
Slino ponaanje moe se simulirati dodavanjem klasi GrafObjekt cjelobrojnog
lana koji e oznaavati tip objekta. Prilikom poziva funkcijskog lana potrebno je,
ovisno o vrijednosti tog lana, pretvoriti pokaziva u pokaziva na neki drugi tip:
class GrafObjekt {
// ...
public:
TipObj tip;
void Crtaj() {}
};
Klasu GrafObjekt emo modificirati tako da funkcijske lanove koji moraju biti
povezani uz sam objekt oznaimo virtualnima:
class GrafObjekt {
private:
int Boja;
public:
void PostaviBoju(int nova) { Boja = nova; }
int CitajBoju() { return Boja; }
virtual void Crtaj() {}
virtual void Translatiraj(int, int) {}
virtual void Rotiraj(int, int, int) {}
};
void Linija::Crtaj() {
// virtual se ne ponavlja u definiciji
// ...
}
vptr
Boja
x1
y1
x2
y2 vtable
Crtaj()
vptr Translatiraj()
Boja Rotiraj()
x1
y1
x2
y2
Moe se postaviti pitanje zato nisu svi lanovi podrazumijevano virtualni tako da
programer ne mora voditi rauna o virtualnosti lanova. Razlog tome lei u efikasnosti
prilikom izvoenja. Poziv virtualnog funkcijskog lana troi neto vie procesorskog
vremena jer je prije poziva potrebno pogledati vtable. Takoer, sama tablica virtualnih
funkcija zauzima izvjestan memorijski prostor.
lanove PostaviBoju() i CitajBoju() emo ostaviti nevirtualnima, te e se
pozvani funkcijski lan odrediti prilikom prevoenja. Postavljanje i itanje boje su
operacije za koje se oekuje da se nee mijenjati prilikom nasljeivanja, odnosno da e
svi objekti na isti nain postavljati i itati svoju boju. Ti su lanovi ostavljeni
nevirtualnima kako bi se utedjelo na veliini virtualne tablice i dobilo na brzini
izvoenja programa.
Za nevirtualne lanove se kae da se pozivaju statiki (engl. static call). To nipoto
ne treba mijeati sa statikim lanovima klasa deklariranih kljunom rijei static.
Ovdje se primarno misli na to da se odreivanje lana u pozivu provodi statiki, na
osnovu podataka dostupnih prilikom prevoenja, za razliku od virtualnih poziva koji se
odreuju dinamiki, na osnovu podataka dostupnih prilikom izvoenja. Pravi statiki
377
lanovi nisu vezani za objekt pa niti ne mogu biti virtualni. Da bi se izbjegle zabune i
ruan termin nevirtualni, takve lanove emo zvati lanovi sa statikim pozivom.
lanovi sa statikim pozivom osnovne klase mogu biti redeklarirani kao virtualni u
izvedenoj klasi. Odreivanje da li se lan poziva virtualno ili statiki u tom se sluaju
odnosi na klasu iz koje se poziva lan, na primjer:
#include <iostream.h>
class A {
public:
void func() { cout << "A::func()" << endl; }
};
class B : public A {
public:
virtual void func() { cout << "B::func()" << endl; }
};
class C : public B {
public:
virtual void func() { cout << "C::func()" << endl; }
};
int main() {
C objc;
A *pokA = &objc;
B *pokB = &objc;
pokA->func(); // ispisuje A::func()
pokB->func(); // ispisuje C::func()
return 0;
}
Prvi poziv rezultira pozivom funkcije A::func() zato jer se funkcija poziva preko
pokazivaa na klasu A. U toj klasi je func() definiran bez kljune rijei virtual pa se
on poziva statiki, a to rezultira pozivom iz klase A. U drugom sluaju lan se poziva
preko pokazivaa na klasu B. U toj klasi lan je redeklariran kao virtualni, pa se on
class GrafObjekt {
private:
int Boja;
public:
void PostaviBoju(int nova) { Boja = nova; }
int CitajBoju() { return Boja; }
virtual void Crtaj() {}
virtual void Brisi() {}
virtual void Translatiraj(int, int) {}
void Pomakni(int px, int py);
};
Linija duzina;
GrafObjekt *go1 = &duzina, &go2 = duzina;
Postoje tri sluaja kada se poziv lana obavlja statiki iako je lan deklariran kao
virtualan. Prvi takav sluaj je kada se funkcijski lan poziva preko objekta neke klase.
To je i logino, jer ako se prilikom prevoenja tono zna klasa objekta za koji se lan
poziva, mogue je odmah tono odrediti i odgovarajui funkcijski lan. To ne vrijedi
za dereferencirane pokazivae; za njih se pozivi takoer provode virtualno. Na primjer:
Prvi poziv moe biti statiki jer se tono zna o kojoj se klasi radi; identifikator duzina
jednoznano oznaava objekt klase Linija. Drugi poziv je virtualan. Vidi se da je
virtualan mehanizam u ovom sluaju potreban, jer *go1 oznaava objekt klase
GrafObjekt, dok je u memoriji stvarno objekt klase Linija.
Mehanizam virtualnih poziva moe se zaobii i tako da se operatorom za
odreivanje podruja eksplicitno navede klasa iz koje se eli pozvati funkcijski lan. Na
primjer:
go1->Linija::Crtaj(); // pogreka
Gornji primjer je neispravan zato to prilikom prevoenja nije poznato na koji objekt
pokaziva go1 pokazuje. Ako bi go1 pokazivao na objekt klase GrafObjekt, lan
Linija::Crtaj() bi mogao pristupiti lanovima x1 ili y1 koji ne postoje u objektu
GrafObjekt, to bi rezultiralo neispravnim radom programa.
Statiki poziv pomou operatora za odreivanje podruja odnosi se samo na onaj
lan koji je eksplicitno naveden u pozivu. Eventualni virtualni pozivi iz tog lana ostaju
nepromijenjeni. Na primjer:
go1->GrafObjekt::Pomakni(5, 7);
class GrafObjekt {
private:
int Boja;
public:
void PostaviBoju(int nova) { Boja = nova; }
int CitajBoju() { return Boja; }
virtual void Crtaj() = 0;
virtual void Brisi() = 0;
virtual void Translatiraj(int, int) = 0;
void Pomakni(int px, int py);
};
Klasa koja nasljeuje apstraktnu klasu nasljeuje i sve iste virtualne funkcije. Ona ih
moe definirati, ime se zapravo daje smisao javnom suelju danom u osnovnoj klasi.
Ako se iste virtualne funkcije ne redefiniraju u izvedenoj klasi, one automatski postaju
dio izvedene klase pa i izvedena klasa postaje apstraktna.
nizObj[1]->Crtaj();
class GrafObjekt {
// ...
public:
virtual ~GrafObjekt();
};
Dakle, nakon gornje promjene u osnovnoj klasi sve izvedene klase e automatski imati
virtualne destruktore. No kako bi se programski kd uinio itljivijim, dobra je praksa i
382
u izvedenim klasama dodati kljuni rije virtual ispred deklaracije destruktora. Tada
je svakom odmah jasno kako poziva li se destruktor virtualno ili ne, bez nepotrebnog
gledanja u osnovnu klasu.
Destruktori apstraktnih klasa se u pravilu navode kao virtualni. Razlog tome je to
objekt virtualne klase ne moe biti deklariran, pa niti destruktor sa statikim pozivom za
takav objekt nema smisla.
Iz izloenoga se vidi da je pozivanje ispravnog destruktora eljeno ponaanje za
svaki objekt. Teko je zamisliti situaciju u kojoj bi pozivanje neispravnog destruktora
moglo biti eljeno ponaanje. Postavlja se logino pitanje zato onda uope postoje
destruktori sa statikim pozivom. Odgovor je praktine naravi: mehanizam virtualnih
funkcija proiruje svaki objekt s vptr pokazivaem na tablicu virtualnih funkcija.
Takoer, za svaku klasu je potrebno zauzeti u memoriji tablicu virtualnih funkcija. U
mnogim sluajevima, naprotiv, klasa nee uope biti naslijeena te nee niti doi do
opasnosti od poziva neodgovarajueg destruktora. Zbog toga bi resursi za smjetaj
objekata u memoriji bili uzalud potroeni. C++ jezik prua programeru na volju da sam
optimizira svoj program kako smatra najprikladnijim. Kako programer sije, tako e i
eti.
Zadatak. Listi opisanoj u odsjeku 11.2 dodajte lan UPotrazi() koji e kao
parametar imati referencu na neki objekt naslijeen od klase Atom. On e pretraiti
cijelu listu dok se ne nae lan koji odgovara zadanom. Usporedba e se obavljati
pomou preoptereenog operatora ==. Zbog toga je klasu Atom potrebno proiriti
istim virtualnim operatorom == te definirati taj operator u klasi koja nasljeuje klasu
Atom.
Zadatak. Umjesto smjetaja grafikih objekata u polje, prepravite klase grafikih
objekata tako da se objekti mogu smjetati u vezanu listu definiranu klasom Lista. (To
znai da GrafObjekt mora naslijediti klasu Atom.) Napiite program koji e crtati
grafike objekte iz liste, umjesto iz polja.
Zadatak. Napiite funkciju Animacija() koja kao parametar ima pokaziva na
grafiki objekt te pomie taj objekt po ekranu. Funkcija mora raditi ispravno sa svakim
objektom, to zapravo znai da e koristiti virtualne pozive funkcija za crtanje i
brisanje.
public:
virtual void PostaviTekst(char *niz);
virtual void Crtaj();
virtual void Brisi();
virtual void Translatiraj(int px, int py);
virtual ~Tekst();
};
Ponekad je potrebno tekst uokviriti pravokutnikom. Bilo bi vrlo praktino stvoriti novu
klasu UokvireniTekst koja bi definirala upravo takav objekt. Takav objekt je neka
vrsta krianca izmeu objekta Tekst i objekta Pravokutnik; on jest u isto vrijeme i
tekst i pravokutnik. Na njega ima smisla primijeniti i operacije koje se primjenjuju na
tekst, ali i operacije koje se primjenjuju na pravokutnike. Jedno od moguih rjeenja je
iskoristiti mehanizme viestrukog nasljeivanja:
UokvireniTekst ut;
ut.PostaviBoju(CRVENA); // pogreka: nejasan pristup
384
Pravokutnik
Poligon
GrafObjekt
UokvireniTekst
UokvireniTekst
nije nimalo jasniji: klasa GrafObjekt postoji u klasi UokvireniTekst dva puta pa je
navedeni pristup podjednako nejasan. Za pristup tom lanu je potrebno tono navesti
kojem se od tih dvaju lanova pristupa: onom naslijeenom od klase Pravokutnik ili
onom naslijeenom od klase Tekst:
ut.Pravokutnik::PostaviBoju(BIJELA);
ut.Tekst::PostaviBoju(ZELENA);
U nekim primjenama pojavljivanje osnovne klase vie puta u toku nasljeivanja moe
biti poeljno. U ovom sluaju to je oito pogreno. Klasa GrafObjekt predstavlja
osnovnu klasu za sve grafike objekte u hijerarhiji. Kako je UokvireniTekst u cjelini
samo grafiki objekt, bilo bi logino da ta klasa ima klasu GrafObjekt proslijeenu
samo jednom. Tako oblikovano hijerarhijsko stablo izgledalo bi kao na slici 11.10.
Mehanizam virtualnih osnovnih klasa (engl. virtual base classes) omoguava
definiranje osnovnih klasa koje se dijele izmeu vie izvedenih klase te se prilikom
nasljeivanja izvedenoj klasi prosljeuju samo jednom.
GrafObjekt
Poligon
Tekst
Pravokutnik
UokvireniTekst
To znai da se deklaracija klase GrafObjekt ne mijenja. tovie, ona moe jednom biti
upotrijebljena kao virtualna, a drugi put kao nevirtualna:
Kako su klase Poligon (a preko nje i klasa Pravokutnik) i Tekst definirale klasu
GrafObjekt kao virtualnu osnovnu klasu, klasa UokvireniTekst e sada zaista
imati samo jedan podobjekt klase GrafObjekt.
Mehanizam virtualnih klasa omoguava nam da rijeimo na problem na
zadovoljavajui nain, a to je da definiramo jednu klasu zajedniku svim preostalim
klasama. No takvo nasljeivanje uvodi novi niz pravila koja se moraju potivati da bi se
mehanizam mogao uspjeno primjenjivati.
klase koji potiu od osnovnih virtualnih klasa nisu sadrani u objektu izvedene klase.
Oni se uvaju u posebnom bloku memorije, a skriveni pokaziva izvedene klase
pokazuje na podobjekt osnovne virtualne klase, kako je prikazano na slici 11.11.
Prilikom izvoenja oba izvedena objekta e jednostavno pokazivati na isti podobjekt.
Na elemente virtualnih osnovnih klasa primjenjuju se pravila pristupa i tipova
izvoenja kao i za obine nevirtualne klase: lanovi naslijeeni po javnom nainu
nasljeivanja zadravaju svoje originalno pravo pristupa, dok lanovi naslijeeni po
privatnom i zatienom nainu dobivaju privatno ili zatieno pravo pristupa.
No to se dogaa u sluajevima kada je osnovna virtualna klasa naslijeena jednom
kao javna, a jednom kao privatna? Na primjer:
Pravokutnik
GrafObjekt
Tekst
GrafObjekt
Pravokutnik
GrafObjekt
Tekst
UokvireniTekst
};
UokvireniTekst ut;
ut.PostaviBoju(CRNA); // OK
Sada u klasi UokvireniTekst postoje dva funkcijska lana za postavljanje boje: jedan
iz klase GrafObjekt dobiven preko klase Pravokutnik i drugi definiran u klasi
Tekst. Ako se prilikom izvoenja klase Tekst ne bi koristilo virtualno nasljeivanje,
prevoditelj ne bi mogao razlikovati ta dva lana, odnosno prilikom prevoenja bi
dojavio pogreku. No ako lan koji se poziva pripada virtualnoj osnovnoj klasi i nekoj
klasi koja se nalazi blie u hijerarhijskom lancu, prevoditelj e izabrati ovaj potonji.
Kae se da lanovi iz bliih klasa dominiraju nad lanovima osnovnih virtualnih klasa.
Na primjer:
UokvireniTekst ut;
ut.PostaviBoju(BIJELA); // poziva Tekst::PostaviBoju(int)
class GrafObjekt {
public:
GrafObjekt(int b);
// ...
};
Vrlo korisna mogunost jezika C++ jesu predloci. Oni omoguavaju pisanje
openitog kda koji vrijedi jednako za razliite tipove. Time se vrijeme razvoja
sloenih programa moe znaajno skratiti, a olakava se i odravanje programa te
ispravljanje eventualnih pogreaka.
Predloci se dijele u dvije osnovne skupine: predloci funkcija i predloci klasa.
Prvi omoguavaju pisanje opih funkcija koje se zatim primjenjuju za razliite tipove
parametara. Potonji omoguavaju definiranje opih klasa koje se zatim aktualiziraju
tipovima prilikom poziva. U ovom poglavlju bit e iscrpno objanjene obje vrste
predloaka.
manji(10, 20)
manji('a', 'z')
No dobiveno rjeenje je zapravo dvolini Janus koji svoje pravo lice otkriva tek ako mu
kao parametar proslijedimo izraz. Naime, makro funkcije se prevode tako da se poziv
jednostavno zamijeni tekstom definicije makro funkcije, pri emu se formalni parametri
zamijene stvarnima. Izraz se tako izvodi dva puta, to moe bitno utjecati na rezultat:
int i = 4, j;
j = manji(++i, 10); // suprotno oekivanju j e sadravati
// broj 6
Poveavanje varijable i se obavlja dva puta: jednom prije i jednom poslije usporedbe,
pa se i cjelokupni rezultat dosta razlikuje od oekivanog.
C++ jezik nudi rjeenje na zadane probleme u obliku predloaka (engl. templates).
Oni omoguavaju upravo eljeno svojstvo da se samo jednom navede algoritam koji se
zatim poziva za razliite tipove podataka. Postoje dvije osnovne vrste predloaka:
predloci funkcija i predloci klasa. Prvi omoguavaju definiciju opih funkcija, dok
drugi omoguavaju definiciju opih klasa.
Gornja definicija predloka specificira NekiTip kao identifikator tipa koji e se kasnije
navesti prilikom poziva funkcije. Iako je zapis funkcije slian navedenoj makro-naredbi,
ovakva definicija oznaava funkciju, iji e se parametri izraunati prije poziva. Zbog
toga e sada poziv
dati oekivani rezultat. Predloak nije makro funkcija prilikom prevoenja predloka
prevoditelj nee parametre funkcije samo ubaciti u definiciju predloka. Ovdje se radi o
definiciji (hipotetski) beskonanog skupa funkcija koje su parametrizirane tipom.
Prilikom prevoenja, predloak e za svaki pojedini tip parametra rezultirati jednom
sasvim konkretnom funkcijom, koja e se pozivati na uobiajeni nain.
Identifikator tipa se u listi formalnih parametara predloka smije pojaviti samo
jednom:
Svaki formalni parametar mora biti oznaen kljunom rijei class. Sljedea definicija
je neispravna:
Formalne parametre NekiTip, Tip i Elem iz gornjih primjera treba shvatiti kao
simbolike oznake za tipove koji e se navesti prilikom poziva funkcije. Na primjer,
ako emo funkciju manji() pozvati za usporeivanje cijelih brojeva, tada e
parametar NekiTip poprimiti vrijednost int, a ako emo usporeivati cipele po
veliini (opisane klasom Cipela) parametar NekiTip e poprimiti vrijednost Cipela.
Iza kljune rijei template i liste formalnih parametara navodi se ili deklaracija ili
definicija funkcije. Prilikom pisanja definicije funkcije vrijede sva dosad navedena
pravila C++ jezika, s tom razlikom to se unutar deklaracije i definicije funkcije za
navoenje tipova koji se obrauju koriste formalni parametri predloka. Na primjer,
umjesto da se navede stvarni tip podataka iji se minimum trai u funkciji manji(),
navest e se tip NekiTip. Formalni parametri se mogu pojaviti na svim mjestima gdje
se moe pojaviti bilo koja regularna oznaka tipa: u listi parametara, za specificiranje
povratne vrijednosti, za deklaraciju varijabli i u izrazima za dodjelu tipa.
Da bismo demonstrirali upotrebu predloaka, realizirat emo funkciju koja odreuje
minimalni lan nekog niza. Funkcija e biti realizirana pomou predloka
parametriziranim tipom elemenata koji ine niz. Funkciji se preko parametara
prosljeuje pokaziva na poetak niza i duljina niza, a ona vraa referencu na objekt
koji je pronaen kao minimalni. Evo realizacije funkcije:
Formalni parametar predloka je Elem. Pomou njega je parametar niz oznaen kao
pokaziva na tip Elem, kao i lokalna varijabla pokNajmanji. Osim toga, povratna
vrijednost funkcije je Elem &, to zapravo znai da se vraa referenca na tip Elem.
Danas to vie nije sluaj: dozvoljeno je prilikom deklaracije predloka navesti parametre
koji se koriste samo u povratnoj vrijednosti funkcije. tovie, mogue je odreeni
parametar uope ne koristiti niti u argumentima niti u povratnoj vrijednosti. Prethodne
su dvije deklaracije stoga danas sasvim ispravne. Tipovi koji nedostaju specificiraju se
prilikom instantacije predloka tako da se navedu u paru znakova < i >. Tako e se
verzija predloka prosjek koja uzima polje float brojeva, a vraa double,
specificirati ovako:
float polje[100];
double pros = prosjek<double, float>(polje, 100);
Parametar predloka funkcije koji nije iskoriten u listi argumenata moe se koristiti za,
primjerice, podeavanje preciznosti raunanja:
Prilikom uvoenja predloaka u C++ jezik izabrana je kljuna rije class kao rije
koja specificira da odreeni parametar predloka predstavlja neki tip. Podrazumijevalo
se da taj tip ne mora biti klasa, ve moe biti i ugraeni tip ili pobrojenje. Kako bi se
istakla uloga pojedinog parametra u deklaraciji predloka, uvedena je nova kljuna rije
typename koja se moe koristiti umjesto kljune rijei class. Time se eksplicitnije
naznaava da se odreeni argument predloka zapravo ime tipa, a ne klasa. Kljuna rije
typename ima dodatno znaenje za predloke njome se unutar predloka moe
odrediti da odreeni izraz predstavlja tip, a ne neto drugo. Na primjer:
U gornjem primjeru naoko bezopasna deklaracija T::A ima razliito znaenje ovisno o
tome stavi li se ispred nje typename ili ne. U naem sluaju T je neki tip, unutar kojeg
je mogue definirati druge tipove. Pretpostavimo da, zbog potreba problema koji
rjeavamo, svaki tip kojim emo instancirati predloak mora imati u sebi definiran tip A
(na primjer, ugnijeenu klasu koja se zove A). Programer bi zasigurno elio pristupiti
tom tipu unutar funkcije, no pitanje je kako. Prva deklaracija ne ini ono to elimo
396
Kljuna rije typename, ba kao i eksplicitno navoenje tipa u pozivu predloka nisu
podrani od mnogih C++ prevoditelja pa ih koristite s oprezom.
Parametar predloka funkcije moe biti i neka konstanta kojom se dodatno upravlja
radom funkcije. Na primjer, mogue je predloak funkcije za uitavanje polja podataka
proizvoljnog tipa s analogno/digitalnog pretvaraa parametrizirati cjelobrojnom
konstantom:
koristimo predloak kojemu poseban tip specificira preciznost rauna, i taj tip u veini
sluajeva iznosi double, predloak moemo definirati ovako:
int main() {
cout << "Od 5 i 6: " << manji(5, 6) << endl;
cout << "Od 'a' i 'b' je " << manji('a', 'b') << endl;
return 0;
}
U gornjem programu funkcija manji() se poziva dva puta. Prvi put se kao parametri
navode cjelobrojne konstante, dok se drugi put navode znakovne konstante. Za svaki od
ova dva poziva stvara se po jedna verzija funkcije. Prevoditelj koristi tipove argumenata
da bi odredio koji se predloak funkcije koristi. Zbog toga se i svi formalni argumenti
predloka obavezno moraju pojaviti u listi parametara funkcije.
U prvom sluaju se openiti tip Tip zamjenjuje tipom int, a u drugom sluaju s
char. Nakon supstitucije tipova, stvaraju se dvije preoptereene verzije funkcije
manji(), iji bi ekvivalent u izvornom kdu bio:
398
Stvaraju se dvije verzije funkcije, ba kao da smo i sami napisali obje verzije, no posao
generiranja razliitih verzija za iste parametre je prebaen na prevoditelja. Time je,
takoer, uklonjena mogunost pogreke prilikom runog kopiranja kda u razliite
varijante funkcije.
U posljednjoj verziji C++ standarda je parametre predloka mogue eksplicitno
navesti unutar para znakova < i >. Unutar tih znakova mogue je navesti eventualne
konstantne parametre koje predloak moe imati. To svojstvo se najvie koristi prilikom
specificiranja parametara koji odreuju povratni tipova ili koji se uope ne spominju u
deklaraciji funkcije. Na primjer:
float polje[100];
double pros = prosjek<double>(polje, 100);
399
Pri tome, ako se navode parametri predloka, dozvoljeno je izostaviti samo parametre s
kraja liste nije mogue, primjerice, izostaviti prvi parametar i oekivati da e on biti
zakljuen iz poziva, a drugi specificirati eksplicitno, na primjer:
float polje[100];
double pros = prosjek<double>(polje, 100); // pogreno
Prilikom navoenja liste parametara potrebno je znak < koji zapoinje listu
uvijek nalijepiti uz naziv funkcije: time se prevoditelju daje na znanje da
slijedi lista parametara. U suprotnom e prevoditelj taj znak protumaiti kao
znak za manje-od.
int **i;
manji_u_nizu(i, 1);
Na primjer:
Na primjer:
Gornji poziv nee biti uspjean jer je potrebno postii potpuno slaganje tipova, a u
konkretnom pozivu je za drugi parametar umjesto podatka tipa int naveden podatak
tipa long. Da bi poziv uspio, potrebno je primijeniti eksplicitnu dodjelu tipa:
m = manji_u_nizu(niz, (int)dulj);
Vektor *niz, m;
// inicijaliziraj niz
m = manji_u_nizu(niz, 5);
Ovakav poziv rezultira pogrekom zato jer prevoditelj prilikom instanciranja funkcije ne
zna kako treba usporediti dva vektora. Ako izmislimo nain usporeivanja vektora i
ugradimo ga u klasu Vektor tako da preopteretimo operator <, gornji poziv postaje
ispravan i on stvarno nalazi najmanji vektor s obzirom na zadani kriterij usporedbe.
Zadatak. Napiite predloak funkcije srednja_vrijednost(). Kao parametar ona e
uzimati polje i cijeli broj koji e pokazivati duljinu polja. Rezultat e biti istog tipa kojeg
su i tipovi elemenata polja. Pri tome se za zbrajanje lanova polja i za dijeljenje lana
polja cijelim brojem koriste operatori + i / definirani u klasi koja opisuje tip elemenata
polja. Pretpostavite da lan polja ne mora imati podrazumijevani konstruktor, ali
sigurno ima ispravan konstruktor kopije.
Zadatak. Napiite predloak funkcije binarno_pretrazi() koja e kao parametre
imati polje, referencu na objekt istog tipa kojeg su i elementi polja te cijeli broj koji
definira duljinu polja. Funkcija pretpostavlja da je ulazni niz sortiran po veliini te e
provesti binarno pretraivanje proslijeenog polja. Kao rezultat e biti cijeli broj koji
e pokazivati indeks proslijeenog objekta u polju ili -1 ako objekt nije pronaen.
Uputa: binarno pretraivanje radi tako da se prvo pogleda element na sredini polja.
Ako je zadani element manji od traenog, pretrauje se preostala gornja polovica polja
(jer je polje sortirano), a u suprotnom donja polovica. Postupak se ponavlja po istom
principu na sve manjem i manjem dijelu polja, dok se traeni objekt ne pronae ili se
interval ne svede na jedan lan. Tada je ili taj lan jednak traenom ili se traeni objekt
ne nalazi u polju. Za usporedbu koristite operatore <, >, <=, >=, == te != definirane u
klasi koja opisuje pojedini lan polja.
402
// eksplicitne instantacije
template double Prosjek<double, float>(float *, int);
template long Prosjek<long, int>(int *, int);
Kao i prilikom implicitne instantacije, pojedini tip se moe izostaviti ako se njegova
vrijednost moe odrediti pomou liste argumenata:
int i = 9, k;
unsigned int j = 6;
k = zbroji(i, j); // krivo: i je int, a j je unsigned int
U gornjem primjeru prvi parametar je tipa int, a drugi je unsigned int. Kako se
konverzije tipova ne provode prilikom poziva predloaka funkcija, gornji poziv ne
uspijeva. Ako bismo eljeli zbrajati razliite tipove preoptereenom verzijom funkcije
zbroji(), morali bismo dodati novu verziju predloka:
int i = 5, j = 7, k;
k = zbroji(i, j); // koji se poziva: zbroji(Tip, Tip) ili
// zbroji(Tip1, Tip2) ?
float a;
double b;
double rez = zbroji<double>(a, b);
float a;
double b, samoZaPovratniTip;
double rez = zbroji(a, b, samoZaPovratniTip);
405
#include <string.h>
// poziva specijalizaciju
char *rez = manji("manji", "vei");
U posljednjoj verziji C++ jezika mogue je prilikom specijalizacije unutar znakova < i
> navesti parametre za koje se definira specijalizacija. Na primjer:
Lista parametara predloka moe biti i prazna, ako se oni mogu zakljuiti iz liste
argumenta funkcije:
U gornjem primjeru prva deklaracija definira opi predloak parametriziran s dva tipa.
Druga deklaracija je djelomina specijalizacija: ona je parametrizirana jednim tipom, no
drugi argument je char. Ta specijalizacija e se koristiti za sve oblike funkcije kod
kojih navedemo char kao drugi argument: f<long, char>(),
f<Kompleksni, char>(), f(int (*)(int, Kompleksni &), char>() itd.
Postupak odreivanja odgovarajue funkcije prilikom poziva izvodi se po
sljedeem algoritmu:
1. Prvo se pretrauju sve specijalizacije predloka te se trai tono podudaranje
argumenata. Ako se nae vie funkcija koje odgovaraju, dojavljuje se pogreka
prilikom prevoenja. Ako se nae tono jedna odgovarajua funkcija, postupak
zavrava. U protivnom se prelazi na sljedei korak.
2. Pretrauju se svi predloci funkcije te se trai tono podudaranje parametara. Ako se
pronae vie od jednog odgovarajueg predloka, javlja se pogreka prilikom
prevoenja. Ako se pronae tono jedan predloak, funkcija je odreena te se
provjerava je li ve generirana varijanta funkcije za zadane parametre. Ako nije,
prevoditelj prevodi predloak te generira traenu funkciju. Ako nije pronaen
odgovarajui predloak, prelazi se na sljedei korak.
3. Sve specijalizacije se promatraju kao skup preoptereenih funkcija te se pokuava
odrediti odgovarajua funkcija pravilima za poziv preoptereene funkcije, to
ukljuuje i mogue konverzije tipa.
2 4 3 1 2 3 1 4 2 1 3 4
2 3 4 1 2 1 3 4 1 2 3 4
2 3 1 4
Nakon sortiranja rezultat se moe ispisati funkcijom pisi_niz() koja se takoer moe
realizirati predlokom. Ona moe ispisati bilo koji niz pod uvjetom da je za zadani tip
definiran operator << za ispis na izlazni tok.
#include <iostream.h>
Evo i primjera poziva tih funkcija, jednom za niz cijelih brojeva, a jednom za niz realnih
brojeva:
int main() {
int niz1[] = {4, 5, 2, 7, 1, 0, 9};
double niz2[] = {2.6, 7.9, 1.4, 8.9, 9.9, 3.3};
bubble_sort(niz1, 7);
cout << "Niz cijelih brojeva:" << endl;
pisi_niz(niz1, 7);
bubble_sort(niz2, 6);
cout << "Niz realnih brojeva:" << endl;
pisi_niz(niz2, 6);
return 0;
}
radi li se o listi cijelih ili kompleksnih brojeva. Ako bismo primijenili metodu runog
kopiranja i promjene kda klase, tada bi svaka dobivena klasa morala imati svoje
posebno ime: CjelobrLista, KomplLista, ListaNizaZnakova i sl.
Argumenti predloka klase se navode slino kao i argumenti predloka funkcije: svaki
argument sastoji se od kljune rijei class i imena parametra. Kljuna rije template
se mora staviti ispred svake deklaracije unaprijed (kao u gornjem primjeru) ili ispred
definicije klase. Lista parametara ne smije biti prazna. Vie parametara se navodi
razdvojeno zarezima:
Slino predlocima funkcije, parametar predloka klase moe biti i izraz. Taj parametar
se u deklaraciji navodi slino obinim parametrima funkcija: umjesto kljune rijei
class stavlja se naziv tipa iza kojeg se stavlja identifikator. Na primjer, ako bismo
htjeli ograniiti broj elemenata u listi te time sprijeiti preveliko zauzee memorije,
mogli bismo dodati predloku klase cjelobrojni parametar maxElem tipa int:
Klase definirane kao predloci koriste se u programu na isti nain kao i obine klase, s
tom razlikom to se iza naziva klase u kutnim zagradama (< >) obavezno moraju navesti
parametri. Na primjer, lista cijelih brojeva se oznaava kao
Lista<int>
Lista<Kompleksni>
OgranicenaLista<Kompleksni, 20>
Tip vrij;
Tip &vrij;
dio memorije koji uope ne sadrava eljeni objekt. Pokuaj pristupa objektu vjerojatno
e rezultirati pogrekom u radu programa, a takoer moe znatno poremetiti rad
operacijskog sustava.
Kao to se vidi, nema ope preporuke koju je mogue bezuvjetno slijediti. Programer
mora prilikom razvoja sustava odvagnuti razloge za i protiv te se odluiti za pristup koji
najbolje odgovara danom podruju primjene.
I na kraju, promotrit emo jo nain na koji je izveden konstruktor klase. Kao prvo,
konstruktoru se kao parametar prosljeuje referenca na objekt. U suprotnom bi se
prilikom poziva konstruktora objekt nepotrebno kopirao u lokalni objekt ime bi se
usporilo izvoenje kda. Takoer, lokalni objekt vrij se inicijalizira u inicijalizacijskoj
listi konstruktora. Time se izbjegava nepotrebna inicijalizacija tog objekta
podrazumijevanim konstruktorom i naknadno kopiranje parametra u taj objekt.
U posljednjoj verziji C++ jezika mogue je koristiti podrazumijevane vrijednosti
parametara. Na primjer, zamislimo predloak klase NizZnakova koji moe biti
parametriziran tipom znaka koji se koristi (char ili wcahr_t, ovisno o situaciji).
Gotovo sigurno emo u 99 % sluajeva koristiti char, pa je praktino char postaviti
kao podrazumijevani parametar:
NizZnakova<> niz; // OK
ElementListe<int> eli(5);
ElementListe<Kompleksni> elk(Kompleksni());
ElementListe<char *> elz("Listam");
413
Gornje definicije treba promatrati na sljedei nain: ime ElementListe oznaava sve
mogue elemente liste te ne opisuje eljeni objekt dovoljno precizno. Zbog toga nije
mogue napraviti objekt klase navodei samo naziv klase, ve je potrebno stvoriti
konkretnu realizaciju predloka. Tako je ElementListe<int> zapravo potpuni naziv
jedne konkretne klase koja opisuje element liste cijelih brojeva. Ako se prihvati da je to
puno ime jedne klase (koja je dobivena preko predloaka, no sa stanovita njenog
koritenja nema nikakve razlike s ostalim klasama koje nisu definirane preko
predloaka), onda je odmah jasno kako se takve klase mogu koristiti. Objekti klasa
predloaka deklariraju se isto kao i objekti obinih klasa, s time da se mora navesti puni
naziv klase.
Nakon prethodnih deklaracija prevoditelj e zapravo stvoriti tri klase. To e biti tri
nezavisne klase iji e puni nazivi glasiti ElementListe<int>,
ElementListe<Kompleksni> i ElementListe<char *>. Moemo si zamisliti kao
da e prevoditelj u sutini provesti pretrai-i-zamijeni operaciju tri puta. Svaki put e
formalni argument Tip zamijeniti stvarnim tipom i dobiveni kd ponovo prevesti. Tek
tada e se provjeriti sintaktika ispravnost dobivenog kda. Tako se esto moe
dogoditi da predloak ispravno radi s jednim tipom kao parametrom, a ne radi s nekim
drugim. U naem sluaju, za ispravan rad predloka, klasa koja se prosljeuje kao
parametar mora imati definiran konstruktor kopije (ili podrazumijevani konstruktor
kopije mora odgovarati). U suprotnom moe doi do problema prilikom kopiranja
proslijeenog objekta u lokalni objekt vrij. Ovo je primjer kada se predloak moe
prevesti, ali ne funkcionira ispravno. No postoje i sluajevi kada instanciranje jednim
tipom prolazi bez problema, dok instanciranje drugim tipom prouzrokuje pogreke
prilikom prevoenja. To se deava kada primjerice neki od funkcijskih lanova klase
koristi operator usporedbe za tipove kojima je predloak parametriziran. Da bi se takva
klasa uspjeno instancirala, tip koji joj se prosljeuje kao parametar mora imati
definirane operatore usporedbe.
Sve uobiajene konvencije koje vrijede za obine klase vrijede i za predloke klasa.
Tako je mogue neke objekte definirati vanjskima pomou kljune rijei extern ili
statikima pomou kljune rijei static. Mogue je definirati polja objekata,
pokazivae i reference na njih, te dodavati kvalifikatore volatile i const:
Iako bi to iz dosadanjeg izlaganja trebalo biti samo po sebi jasno, klase istog predloka
razliitih parametara predstavljaju potpuno razliite tipove i nije ih mogue mijeati. Na
primjer, nije ih mogue usporeivati, pridruivati ili prosljeivati kroz parametre (osim
414
nije sintaktiki ispravna i nema smisla. Ona pokuava meusobno pridruiti dva
potpuno razliita tipa. Uostalom, ako se malo bolje pogleda, gornje pridruivanje niti
nema nekog logikog smisla.
Izmeu gornja dva rjeenja postoji sutinska razlika. Da bi se ona razumjela, potrebno je
sjetiti se da se predloci instanciraju tako da se identifikatori parametara zamijene
stvarnim parametrima po pretrai-i-zamijeni principu te svaki put dobivamo zasebnu
klasu. To znai da e instantacije
definirati dvije odvojene klase. Svaka od tih klasa imat e svoj skup funkcijskih
lanova. Dobiveni izvrni program bit e stoga dulji jer e sadravati verziju
funkcijskih lanova za 5 i za 6 elemenata. Ako bi se u programu koristile jo liste drugih
duljina, dobiveni izvedbeni kd bi znatno rastao, pogotovo ako je klasa opirna i s
mnogo funkcijskih lanova. Takoer, parametar maxElem mora biti konstantan izraz,
odnosno mora biti poznat prilikom prevoenja.
Drugo rjeenje nema taj nedostatak: postoji jedna klasa koja se jednom instancira za
odreeni tip. Duljina liste se odreuje prilikom poziva konstruktora. Tim vie, duljina ne
mora biti konstantan izraz, nego se moe izraunati prije poziva konstruktora.
Postavlja se pitanje zato bi onda netko uope htio koristiti izraze kao parametre
predloku. Razlog lei u brzini izvoenja. Naime, kod prvog rjeenja se alokacija
memorije potrebne za spremanje liste obavlja vrlo brzo jer se alocira jedno polje
objekata poznate duljine. U drugom sluaju koristi se dinamika dodjela memorije, koja
radi dosta sporije. Alokacija preko polja je mogua zato jer je parametar maxElem
poznat prilikom prevoenja. U drugom sluaju parametar konstruktoru se odreuje
prilikom izvoenja, pa nije mogue alokaciju obaviti na tako elegantan nain.
Ako se neki funkcijski lan klase ne koristi u programu za odreeni skup
argumenata, prevoditelj ga nee generirati (osim u sluaju eksplicitne instantacije
pogledajte sljedei odsjeak). To omoguava da neke operacije budu definirane samo za
neke tipove. Primjerice, klasa Lista moe sadravati funkcijski lan za sortiranje
lanova u rastui poredak. Pri tome e se za usporedbu koristiti operator < koji mora biti
preoptereen za zadani tip. No mnogi tipovi, primjerice Kompleksni, nemaju
definirane operatore usporedbe, jer se za njih usporedba ne moe definirati na neki
razuman nain. Ako bi prevoditelj prilikom instantacije generirao sve lanove predloka
klase, tada ne bismo mogli koristiti Lista<Kompleksni> jer lan za sortiranje ne bi
proao prevoenje.
416
#include <string.h>
U gornjem primjeru parametar konstruktoru je char * const &. Nije sasvim oito, no
radi se o referenci na konstantan pokaziva na znak. Nije bilo dovoljno navesti char *,
const char * niti char const *. Evo i zato: konstruktor deklariran u klasi kao
parametar ima referencu na konstantan tip. Tip specijaliziranog konstruktora mora tome
odgovarati zato nije dovoljno samo prenijeti pokaziva na znak, nego referencu na
pokaziva. Uz ovakvu dopunu, sada e se za sve elemente liste koji sadravaju
pokazivae na znak koristiti dani konstruktor.
Ponekad implementacija definirana predlokom nije dovoljno efikasna za neke
tipove. Takoer, mogue je da za neki tip klasa treba dodatak javnom suelju kako bi
funkcionirala ispravno. Tada moemo definirati specijalizaciju cijele klase za pojedini
tip. Specijalizacija se definira tako da se nakon naziva klase unutar znakova <> navede
tip za koji se specijalizacija definira. Ona se ne mora poklapati u sadranim funkcijskim
lanovima s predlokom klase. Specijalizacija predstavlja jednu zasebnu klasu koja
moe biti potpuno drukija od preostalih klasa dobivenih iz predloka. No sa
specijalizacijama ne treba pretjerivati.
U naem sluaju, ako klasa ElementListe sadrava znakovne nizove, ona iziskuje
dodatni funkcijski lan destruktor. Predloak klase ne definira destruktor jer e svi
podatkovni lanovi objekta biti automatski uniteni. Objekt koji se uva u listi bit e
automatski uniten pomou podrazumijevanog destruktora klase. No u sluaju
znakovnih nizova podrazumijevani destruktor nee osloboditi zauzetu memoriju. Zbog
toga emo definirati specijalizaciju klase za tip char *:
ElementListe<char *>::~ElementListe() {
delete [] vrij;
}
Dodani destruktor e sada ispravno unititi objekt jer e i osloboditi zauzetu memoriju.
Po istom principu smo dodali funkcijski lan Kopiraj(). On kopira znakovni niz u
memorijski spremnik te je svojstven samo implementaciji za znakovne nizove. Takoer,
kako specijalizirana klasa moe definirati sasvim drukije lanove, vie nije potrebno
patiti se s udnim konstruktorima konstruktor je sada deklariran prirodno (lat.
declaratio naturalis).
Lista();
~Lista();
void Dodaj(T *elt);
void Brisi(T *elt);
ElementListe<T> *Pocetak() { return glava; }
ElementListe<T> *Kraj() { return rep; }
};
sve e se liste poeti brojati od nule. Ako bismo htjeli poeti brojati liste realnih brojeva
od broja 5 (ne ulazei u smisao takvog brojanja), to bismo mogli uiniti na sljedei
nain:
int Lista<float>::brojLista = 5;
int main() {
Lista<int> lista1, lista2;
Lista<char *> lista3;
cout << "Broj cjelobrojnih lista: " <<
Lista<int>::brojLista << endl;
cout << "Broj lista znakovnih nizova: " <<
Lista<char *>::brojLista << endl;
return 0;
}
Prilikom izvoenja, u prvom retku e se ispisati broj 2 jer u memoriji postoje dvije
cjelobrojne liste, dok e se u drugom retku ispisati broj 1 jer postoji samo jedna lista
nizova znakova. Pristup
Parametar duljina mora biti poznat prilikom prevoenja. To znai da on mora biti
sastavljen od samih konstanti. Prilikom prevoenja sve pojave identifikatora duljina
se zamjenjuju vrijednou navedenog izraza. Prilikom instantacije klase nije dozvoljeno
parametre definirati varijablama, jer je njihova vrijednost poznata tek prilikom
izvoenja. Na primjer:
Spremnik<20> s1;
Spremnik<10 + 10> s2;
Spremnik<10 * 2> s3;
Spremnik<30> s4;
int n = 5;
Spremnik<n * 20> uzas; // pogreka
Tip izraza koji je stavljen na mjesto parametra mora biti potpuno jednak tipu
navedenom u deklaraciji predloka nema konverzije:
class ElementListe {
public:
Tip vrij;
ElementListe *prethodni, *sljedeci;
};
Ako su ugnijeeni tipovi javni, moe im se pristupati i iz okolnog programa. Pri tome
je potrebno potpuno navesti put do klase kojoj se eli pristupiti. Svaka varijanta klase
Lista definira jednu klasu ElementListe. Tako je mogue koristiti tipove
Lista<int>::ElementListe
Lista<char *>::ElementListe
Predloak klase takoer moe imati ugnijeena pobrojenja. Njima se isto tako moe
pristupiti iz okolnog podruja pod uvjetom da se navede puna staza do tipa. Na primjer,
klasa Spremnik moe definirati pobrojenja koja opisuju redni broj zadnjeg elementa u
spremniku te kritinu veliinu spremnika koja je jednaka 75% ukupne veliine:
Punu stazu je potrebno navesti i prilikom pristupa identifikatoru maksimum, bez obzira
na to to se vrijednost tog identifikatora ne mijenja u razliitim instantacijama klase:
};
};
X<char>::Y<Kompleksni> obj;
Neka sada imamo funkciju sqrt() koja rauna korijen iz kompleksnog broja. Ona sada
mora biti parametrizirana odreenom instancom predloka uzet emo da kao
parametar uzima Kompleksni<double> zato jer se time postie najvea preciznost:
Kompleksni<double> sqrt(Kompleksni<double>);
class X {
public:
template <class T>
virtual void f(T&) = 0; // neispravno
};
class Y : public X {
public:
template <class T>
virtual void f(T&); // neispravno
};
Nije dozvoljeno stvarati predloke lokalnih klasa (klasa deklariranih unutar funkcija ili
funkcijskih lanova).
426
class A {
void FClan();
};
class B {
public:
void FClan();
};
void Funkcija();
koja nam daje mehanizam za odravanje skupa. Klasa SkupRijeci e samo dodati
nove funkcijske lanove za pretraivanje, sortiranje i slino. Takvo nasljeivanje e se
postii na sljedei nain:
Druga mogunost jest da se predloak klase izvodi iz neke obine klase. Na primjer,
mogue je definirati apstraktnu baznu klasu Kontejner koja e definirati opa
svojstva objekta koji sadrava druge objekte, na primjer sposobnost odreivanja broja
elemenata u kontejneru, pranjenje kontejnera i slino. Klasa Lista e zatim naslijediti
tu klasu te e definirati navedene operacije u skladu sa svojom implementacijom. No
kako klasa Lista moe sadravati objekte razliitog tipa, ona mora biti definirana
predlokom. Takvo izvoenje se moe provesti ovako:
class Kontejner {
public:
virtual int BrojElemenata()=0;
virtual void Prazni()=0;
};
Nakon ovakvog nasljeivanja sve varijante klase Lista e imati po jedan podobjekt
klase Kontejner koji nije parametriziran predlokom.
Trea mogunost nasljeivanja je nasljeivanje u kojem se predloak klase izvodi
iz nekog drugog predloka. Na primjer, klasa Lista definira opa svojstva objekta koji
uva druge objekte. No moe nam zatrebati i klasa Stog koja e biti slina klasi Lista,
s tom razlikom to e se dodavanje i uklanjanje elemenata uvijek obavljati na poetku
liste. Ta klasa takoer mora biti realizirana kao predloak, jer e taj stog uvati elemente
razliitih tipova. U tom sluaju predloak klase Lista moe posluiti za izvoenje
predloka klase Stog, to e se obaviti ovako:
Iz svake varijante klase Lista izvodi se po jedna varijanta klase Stog. Prilikom
koritenja osnovnih klasa definiranih predlokom valja se voditi sljedeim pravilom:
ako se kao osnovna klasa koristi predloak klase, on mora imati listu parametara.
Naprotiv, izvedena klasa nikada nema listu parametara; ona se specificira koritenjem
429
Ovaj sluaj je identian prvom, s tom razlikom to se kao osnovna klasa koristi jedna
varijanta predloka.
Zadatak. Realizirajte klasu Stog pomou klase Tablica. Stog je podatkovna struktura
kod koje je lanove mogue dodavati i itati samo na vrhu stoga. Uputa: klasa Stog e
biti predloak te e nasljeivati predloak klase Tablica.
Iako razlog za to moda nije odmah oit, funkcija je realizirana predlokom argument
predloka odreuje tip za koji se eksponencijalna funkcija rauna. Tako se jednim
potezom moe dobiti vie funkcija za raunanje eksponencijalne funkcije realnog broja,
ali i kompleksnog broja, pa ak i matrice (u naprednoj analizi mogue je definirati
eksponencijalnu funkciju za matrice, i to upravo preko gore napisanog beskonanog
reda). Kako ipak nemamo cijelu vjenost na raspolaganju da bismo pozbrajali svih
beskonano lanova, dodatni parametar e nam biti cijeli broj koji e odreivati broj
iteracija:
double faktorijel(double);
double NaPotenciju(double baza, int eksponent);
430
No prevoditelj prilikom prevoenja predloka uope ne zna za koji e tip predloak biti
iskoriten. Jedino to se zna jest da se tip mora moi inicijalizirati nulom, mora se moi
dijeliti faktorijelama te meusobno zbrajati. Takoer, mora postojati funkcija
NaPotenciju() za taj tip.
Oito se povezivanje funkcije NaPotenciju() te aritmetikih operatora mora
odgoditi do mjesta na kojemu se predloak instancira, tek kada se definira tip T. No to
je primjerice, s funkcijom faktorijel()? Mogli bismo rei da emo sve identifikatore
povezati na onom mjestu na kojemu se predloak instancira. To moe ponekad biti
pogubno, ako bismo u nastavku definirati i long varijantu funkcije faktorijel() (za
neupuene, faktorijele od realnih brojeva se definiraju preko gama-funkcija):
int main() {
double e = OpciExp(1, 20);
cout << e << endl; // mogli bismo se iznenaditi
return 0;
}
class ElementListe {
private:
Tip vrij;
ElementListe *prethodni, *sljedeci;
public:
ElementListe *Prethodni() { return prethodni; }
ElementListe *Sljedeci() { return sljedeci; }
void StaviPrethodni(ElementListe *pret)
{ prethodni = pret; }
void StaviSljedeci(ElementListe *sljed)
{ sljedeci = sljed; }
ElementListe(const Tip &elem) : prethodni(NULL),
sljedeci(NULL),
vrij(elem) {}
Tip &DajVrijednost() { return vrij; }
};
pok->Sljedeci()->StaviPrethodni(pok->Prethodni());
else
rep = pok->Prethodni();
if (pok->Prethodni() != NULL)
pok->Prethodni()->StaviSljedeci(pok->Sljedeci());
else
glava = pok->Sljedeci();
delete pok;
}
13. Imenici
U ovom poglavlju e biti objanjen vaan C++ novitet imenici (engl. namespaces).
Oni su u standard uvedeni razmjerno kasno, a slue prvenstveno kao pomo
programerima prilikom razvoja sloenih programa. Pomou njih je mogue
identifikatore pojedinih klasa, funkcija i objekata upakirati u jednu cjelinu koja se zatim
na proizvoljnom mjestu u programu moe ukljuiti i iskoristiti.
bibl1.h
int Read(int);
bibl2.h
void Read();
glavni.cpp
#include <bibl1.h>
#include <bibl2.h> // problemi: redeklaracija imena Read
435
Zbog gore navedenog razloga mnoge su programerske kue nazivima klasa i funkcija u
biblioteci davali vrlo duga imena kako bi se smanjila vjerojatnost da neka druga
biblioteka koristi isto ime. No za klase koje se vrlo esto koriste nije praktino prilikom
svakog koritenja utipkavati dugako ime.
Gore opisani problem se esto u literaturi naziva zagaenjem globalnog podruja
(engl. global scope pollution). Njegovo rjeenje je u C++ jezik uvedeno u obliku
imenika, ime je uinjen kompromis izmeu oprenih zahtjeva koji se postavljaju na
podruje imena.
namespace RadiSDiskom {
int duljina;
int Citaj(char *buf, int dulj);
int Pisi(char *buf, int dulj);
class Datoteka {
// ovdje ide deklaracija klase
};
class Podaci;
const int samoCitanje = 0x20;
}
class RadiSDiskom::Podaci {
// definicija klase
};
Za razliku od ostalih elemenata C++ jezika koji ne mogu biti definirani na vie mjesta,
imenici se mogu definirati u vie navrata. Ako, primjerice, kasnije u programu
primijetimo da je imeniku RadiSDiskom potrebno dodati jo i klasu Direktorij, to se
moe uiniti tako da se jednostavno ponovi deklaracija imenika u kojoj su navedeni
novi lanovi:
namespace RadiSDiskom {
class Direktorij {
// ...
};
}
namespace RadiSDiskom {
int Pisi(char *buf, int dulj) {
// definicija funkcije
}
}
Imenici se mogu pojaviti samo u globalnom podruju imena ili unutar nekog
drugog imenika. Naziv imenika mora biti jedinstven u podruju u kojem se
pojavljuje.
namespace A {
int i;
namespace B {
int j; // puni naziv objekta je A::B::j
}
}
Objekti koji su ugnijeeni u vie imenika imaju puni naziv koji se dobije tako da se
operatorom :: razdvoje nazivi svih imenika na koje se nailazi na putu do objekta.
Mogue je definirati alternativno ime za neki imenik, tako da se iza kljune rijei
namespace navede alternativno ime, stavi znak = te se navede ime originalnog
imenika. U nastavku je deklariran imenik RSD koji je alternativno ime za imenik
RadiSDiskom:
437
Gornji oblik uvoenja alternativnog imena je vrlo koristan. Naime, ako koristimo
imenike, tada nazivi pojedinih klasa i funkcija vie ne predstavljaju problem. No i dalje
ostaje problem jedinstvenosti naziva imenika. Zbog toga e pojedini imenici opet imati
dugake nazive. Koritenje takvih naziva je vrlo naporno, no zato je mogue definirati
alternativno kratko ime za svaki pojedini imenik iz biblioteke.
Posebna vrsta imenika je bezimeni imenik (engl. nameless namespace). On se
deklarira tako da se u deklaraciji izostavi naziv:
namespace {
int i;
class X;
}
RadiSDiskom::Datoteka dat;
Pojedinom elementu imenika se moe pristupiti samo nakon njegove deklaracije unutar
imenika. Na primjer:
namespace A {
int i;
}
438
void f() {
A::j = 9; // pogreka: j jo nije dio imenika A
}
namespace A {
int j;
}
void g() {
A::j = 10; // OK: j je prethodno uveden u imenik A
}
lanovima imenika bez naziva se pristupa bez eksplicitnog navoenja imenika. Kako
bezimeni imenici nemaju ime, nije mogue eksplicitno navesti lan iz takvog imenika:
int i;
namespace {
int i;
int j;
}
void f() {
j = 0; // OK: pristupa se elementu imenika
i = 0; // pogreka: nije navedeno je li to
// globalni i ili i iz imenika
::i = 0;// OK: globalni i
}
namespace X {
int i;
void f() {
i++; // podrazumijeva se lan X::i
// ...
}
}
Ovakav pristup elementima imenika ima tu manu to je esto potrebno navoditi naziv
imenika. Kako bi se to izbjeglo, u C++ jezik je uvedena kljuna rije using koja
omoguava uvoenje identifikatora iz imenika u drugo podruje imena. Ona dolazi u
dvije varijante: deklaracija using i direktiva using.
439
void UcitajDatoteku() {
using RadiSDiskom::Citaj;
char buf[50];
if (Citaj(buf, 50)) // radi neto korisno, npr. okopaj vrt
}
int i = 0;
namespace X {
int j = 9;
using ::i; // sinonim za globalni i
}
namespace Y {
using X::i; // sinonim za globalni i
using X::j; // sinonim za j iz imenika X
}
int main() {
using Y::j;
cout << Y::i << endl; // ispisuje 0
cout << j << endl; // ispisuje 9
return 0;
}
Evo primjera:
namespace Ispis {
void Ispisi(int);
440
void Ispisi(char);
void Ispisi(char *);
void Ispisi(Kompleksni &);
}
int main() {
using Ispis::Ispisi; // odmah su dostupne sve
// varijante od Ispisi
Ispisi(5);
Ispisi(Kompleksni(5.0, 6.0));
return 0;
}
Pri tome vrijedi imati na umu da using deklaracija uvodi u neko podruje samo
identifikatore koji su deklarirani u imeniku do tog mjesta. Ako se u nastavku imenik
proiruje jo dodatnim preoptereenim funkcijama, one e biti ukljuene u imenik tek
nakon deklaracije:
namespace Ispis {
// isto kao i gore
}
int main() {
using Ispis::Ispisi;
Ispisi(Vektor(6., 7.)); // pogreka: Ispisi(Vektor&)
// na ovom mjestu jo nije lan
// imenika
return 0;
}
namespace Ispis {
void Ispisi(Vektor &); // proirenje imenika je iza
// gornje using deklaracije
}
namespace Ispis {
void Ispisi(int a) { cout << "int: " << a << endl; }
}
using Ispis::Ispisi;
441
namespace Ispis {
void Ispisi(char *s) {
cout << "char *: " << s << endl;
}
}
int main() {
Ispisi(5); // OK: using deklaracija je stvorila
// globalni sinonim
Ispisi("C++"); // pogreka: na mjestu using
// deklaracije Ispisi(char *) jo nije
// bio lan imenika
return 0;
}
class A {
private:
int i;
protected:
int j;
public:
A(int a, int b) : i(a), j(b) {}
};
class B {
public:
int k;
};
class C : public A {
public:
using B::k; // pogreka: B nije osnovna klasa od A
using A::i; // pogreka: i nije dostupan u klasi C
using A::j; // OK: j e u klasi C imati javni
// pristup
};
442
class C : public A {
public:
C() : A(5, 10) {}
using A::j;
};
int main() {
C obj;
cout << obj.j << endl; // ispisuje 10
return 0;
}
Deklaracija using unutar klase posebno je korisna ako se prisjetimo pravila koje kae
da nasljeivanje nije isto to i preoptereenje, te da funkcijski lan nekog naziva u
izvedenoj klasi skriva istoimeni lan drukijeg potpisa u osnovnoj klasi:
class Osnovna {
public:
void func(int a) { cout << a << endl; }
};
int main() {
Izvedena obj;
obj.func(5); // pogreka
return 0;
}
Sada bi se prethodna funkcija main() uspjeno prevela i pozvala lan iz osnovne klase.
Pri tome e funkcijski lanovi osnovne klase zaobii (engl. override) virtualne
funkcijske lanove osnovne klase. To znai da e, iako se u podruje izvedene klase
uvodi virtualan lan osnovne klase, u podruju izvedene klase vrijediti lan naveden u
toj klasi:
class B {
public:
virtual void f(int);
virtual void f(char);
void g(int);
void h(int);
};
class D : public B {
public:
using B::f;
void f(int);// OK: D::f(int) e zaobii B::f(int)
using B::g;
void g(char); // OK: ne postoji B::g(char)
using B::h;
void h(int);// pogreka: D::h(int) je u konfliktu s
// B::h(int) jer B::h(int) nije virtualan
};
void UcitajDatoteku() {
using namespace RadiSDiskom;
Datoteka dat; // poziv klase iz imenika
char buf[50];
if (Citaj(buf, 50)) // poziv funkcije iz imenika
// ...
}
444
namespace Ispis {
void Ispisi(int);
void Ispisi(char);
void Ispisi(char *);
void Ispisi(Kompleksni&);
}
void f() {
Ispisi(5); // OK
Ispisi(Vektor(5., 6.)); // pogreka: lan jo nije
// ubaen u imenik
}
namespace Ispis {
void Ispisi(Vektor &);
}
int main() {
Ispisi(Vektor(5., 6.)); // OK: lan je sada dio
// imenika
return 0;
}
U gornjem primjeru, iako na mjestu direktive using imenik nije sadravao funkciju
Ispisi(Vektor &), im je funkcija dodana ona je automatski dostupna bez ponovnog
navoenja direktive using. To je zato jer direktiva using ne deklarira lanove imenika
na mjestu gdje je navedena, ve samo upuuje prevoditelja da, ako neki identifikator ne
moe raspoznati, neka dodatno pretrai i podruje navedenog imenika. Takvo ponaanje
je u suprotnosti s deklaracijom using iz prethodnog odjeljka, no razlog tome je to
deklaracija using deklarira lan, a direktiva using ne.
Direktiva using moe se navesti i u sklopu deklaracije imenika, ime se svi
identifikatori iz jednog imenika ukljuuju u drugi imenik. Pri tome valja biti oprezan, jer
ako dva imenika sadre lanove istog naziva, lanovi se nee moi jednoznano
razluiti:
namespace X {
int i = 0;
int j = 9;
}
namespace Y {
using namespace X;
445
int i = 2;
}
int main() {
using namespace Y;
cout << i << endl; // pogreka: koji i?
cout << j << endl; // OK: ispisuje 9
return 0;
}
U gornjem primjeru nije jasno kojem i se eli pristupiti: onom iz imenika X ili iz
imenika Y. U tom sluaju je potrebno simbol jednoznano odrediti navodei puno ime:
int main() {
using namespace Y;
cout << X::i << endl; // ispisuje 0
cout << Y::i << endl; // ispisuje 2
cout << j << endl; // ispisuje 9
return 0;
}
14.1. to su iznimke?
Prilikom izvoenja programa esto moe doi do raznih odstupanja od predvienog
tijeka instrukcija. Na primjer, potrebno je alocirati memoriju za neko polje cijelih
brojeva koje se kasnije obrauje u matematikom dijelu programa. No prilikom pisanja
programa ne moemo sa sigurnou ustvrditi da e se dodjela memorije uvijek
uspjeno izvesti. Naime, ovisno o koliini memorije u raunalu, broju aktivnih programa
i slinih okolnosti moe se dogoditi da jednostavno dodjela nije mogua jer nema
slobodne memorije. Programer mora zbog toga provjeriti rezultat dodjele prije nego to
nastavi s izvoenjem kako bi osigurao ispravno funkcioniranje programa. U suprotnom,
program bi vjerojatno izazvao probleme u radu raunala, te moda ak i sruio
raunalo.
Nadalje, esto se mogu pojaviti problemi s neispravnim pristupom korisnikim
podacima. Na primjer, C++ jezik ne obavlja provjeru indeksa prilikom pristupa polju.
Zbog toga, e se nie navedeni kd sasvim uspjeno prevesti, ali e vrlo vjerojatno
prouzroiti probleme prilikom izvoenja:
int main() {
int mat[10];
mat[130] = 0; // vrlo opasna instrukcija!
447
return 0;
}
Da bismo sprijeili takve pogreke, mogue je uvesti klasu Niz i njome opisati polje
koji provodi provjeru granica:
class Niz {
private:
int duljina;
int *pokNiz;
int pogreska;
public:
Niz(int d) : duljina(d), pokNiz(new int[d]) {}
Niz a(10);
a[0] = 80;
a[130] = 0;
448
if (a.JeLiPogreska())
cout << "Pogreka u pristupu nizu a." << endl;
try {
// ovo je blok pokuaja
}
catch (parametar) {
// ovo je blok hvatanja
}
Niz a(10);
try {
// ovdje dolazi problematian dio programa,
// na primjer, na pristup polju:
a[130] = 0;
}
// nije jo gotovo...
449
U ovom sluaju iznimka je tipa const char *. No esto se definira poseban korisniki
objekt koji se baca u trenutku podizanja iznimke:
class NizoveIznimke {
private:
char *pogreska;
public:
NizoveIznimke(char *np) :
pogreska(new char[strlen(np)+1]) {
strcpy(pogreska, np); }
NizoveIznimke(NizoveIznimke &ref) :
pogreska(new char[strlen(ref.pogreska)+1]) {
strcpy(pogreska, ref.pogreska); }
~NizoveIznimke() { delete [] pogreska; }
// ...
throw "Pogrean pristup nizu.";
// ...
U oba sluaja iznimka zapravo sadrava znakovni niz koji opisuje pogreku do koje je
dolo, no prvi primjer pomou zasebnog objekta ima veliku prednost. Naime, u jednom
programu mogue je koristiti mnogo razliitih objekata odjednom. Ako primjerice
istodobno koristimo i skupove, razumno je uvesti klasu Skup koja e opisivati tu
strukturu podataka. Prilikom obrade skupova takoer su mogue iznimke. Ako bi i
klasa Skup i klasa Niz podizale iznimke tipa const char *, ne bi bilo mogue
razlikovati te dvije vrste iznimaka. Naprotiv, ako za iznimke koje baca klasa Skup
uvedemo novu klasu SkupoveIznimke, mogue je zasebno napisati rutinu za obradu
jednih te rutinu za obradu drugih iznimaka.
Nakon svakog bloka pokuaja mora slijediti barem jedan blok za obradu iznimaka.
Taj blok se specificira kljunom rijei catch. Kae se da on hvata (engl. catch)
odgovarajue iznimke iz bloka pokuaja. Nakon kljune rijei catch u okruglim
zagradama navodi se tono jedan formalni parametar (isto kao prilikom deklaracije
funkcije) koji oznaava tip iznimke koji se hvata tim blokom. Nakon toga se u
vitiastim zagradama navodi tijelo rutine:
Niz a(10);
try {
a[130] = 0;
}
catch (NizoveIznimke &pogr) {
cerr << pogr.Pogreska() << endl;
}
U gornjem sluaju rutina za obradu pogreaka jednostavno hvata sve pogreke tipa
NizoveIznimke & (s time da vrijede pravila standardne konverzije!) te ispisuje opis
pogreke. Iza pojedinog bloka pokuaja mogue je navesti i vie blokova hvatanja
iznimaka. Svaki od njih hvatat e iznimke samo odreenog tipa:
451
try {
// problematian dio koda
}
catch (NizoveIznimke &pogr) {
cerr << pogr.Pogreska() << endl;
}
catch (SkupoveIznimke &pogr) {
// obrada pogreaka koje je izazvala klasa skup
}
void Poziv() {
Niz b(6);
try {
Niz a(11);
cout << "Obraunavam se s poljem \'a\'" << endl;
Obracunaj(a);
cout << "Obraunavam se s poljem \'b\'" << endl;
Obracunaj(b);
}
catch (NizoveIznimke& pogr) {
cerr << pogr.Pogreska() << endl;
}
cout << "Kraj programa." << endl;
}
Gore opisani postupak za vraanje poziva funkcija unatrag te unitavanja svih lokalnih
objekata se zove odmatanje stoga (engl. stack unwinding) te funkcionira tako da
programer moe biti siguran da e se do mjesta obrade iznimke unititi svi lokalni i
privremeni objekti koji su se mogli stvoriti do mjesta na kojem je iznimka podignuta.
453
try {
main();
}
catch (...) {
terminate();
}
Pri tome oznaka ... u parametrima kljune rijei catch oznaava hvatanje svih
iznimaka (bit e kasnije detaljnije objanjeno).
Iza pokuajnog bloka moe se navesti vie blokova hvatanja. Pri tome svaki
moe definirati svoj tip i na taj nain obraditi odgovarajue iznimke.
Baeni objekt e se predati bloku hvatanja ako je zadovoljen jedan od sljedeih tri
uvjeta na tip objekta i tip argumenta bloka:
1. Oba tipa su potpuno jednaka. U tom se sluaju ne provodi nikakva konverzija.
2. Tip parametra je klasa koja je javna osnovna klasa baenog objekta. Tada se baeni
objekt svodi na objekt osnovne klase.
3. Ako je baeni objekt pokaziva i ako je argument bloka pokaziva, podudaranje e se
postii ako se baeni pokaziva moe svesti na pokaziva parametra standardnom
konverzijom pokazivaa.
try {
// ...
}
catch (void *pok) {
// sve pokazivake iznimke e zavriti ovdje
}
catch (char *pok) {
// ovaj blok hvatanja nikada se nee dohvatiti, pa ak
// ako se i baci objekt tipa char *
}
Ispravno bi bilo zamijeniti redoslijed posljednja dva bloka tada e iznimke tipa
char * zavriti u odgovarajuem bloku, dok e sve ostale iznimke koje su tipa
pokazivaa biti svedene na void * i proslijeene drugom bloku.
Ponekad je potrebno napisati blok hvatanja koji e preuzimati sve iznimke. To se
moe uiniti tako da se kao parametar catch kljunoj rijei stavi znak ... , slino kao i
prilikom deklaracije funkcije s neodreenim parametrima. Na primjer:
try {
// ...
}
catch (...) {
// ovdje e zavriti sve iznimke
}
Kako nije dano niti ime niti tip parametru, nije mogue pristupiti vrijednosti objekta
koji je baen. Jasno je da ako postoji vie blokova za hvatanje, ovakav blok mora doi
kao posljednji. Kako u njemu zavravaju sve iznimke, eventualni naknadni blokovi
hvatanja nikada nee biti iskoriteni.
Obrada iznimke se esto mora obaviti u nekoliko koraka. Na primjer, zamislimo da
je do iznimke dolo u funkciji koja je pozvana kroz etiri prethodne funkcije. Pri tome
neka je svaka funkcija zauzela odreenu koliinu dinamiki dodijeljene memorije koju
je u sluaju iznimke potrebno osloboditi. U tom sluaju svaka funkcija mora uhvatiti
iznimku, osloboditi memoriju i proslijediti iznimku nadreenoj funkciji kako bi ona
ispravno oslobodila svoje resurse. Iako je mogue iznimku proslijediti nadreenom
bloku tako da se ponovo baci iznimka istog tipa, jednostavnije je navesti kljunu rije
throw bez parametra. Obraivana iznimka e biti ponovno baena i proslijeena
nadreenom bloku. Vano je primijetiti da je takva sintaksa doputena samo unutar
bloka hvatanja izvan njega kljuna rije throw bez parametara prouzroit e pogreku
prilikom prevoenja. Na primjer:
455
void Problematicna() {
throw 10;
}
void Func1() {
char *mem = new char[100];
try {
Problematicna();
}
catch (...) {
delete [] mem; // ienje zauzetih resursa
throw; // prosljeivanje iznimke
}
delete [] mem;
}
void Func2() {
int *pok = new int[50];
try {
Func1();
}
catch (int) {
delete [] pok;
throw;
}
delete [] pok;
}
Potrebno je obratiti panju na razliku izmeu bloka hvatanja u funkciji Func1() i bloka
hvatanja u funkciji Func2(). U oba nije mogue pristupiti vrijednosti baenog objekta
prilikom specifikacije drugog bloka izostavljen je naziv parametra. No prvi blok
hvatanja hvata sve, a drugi samo cjelobrojne iznimke. Takoer, iako se u prvom bloku
hvatanja ne moe identificirati objekt koji je baen, mogue je proslijediti iznimku
nadreenom bloku pomou kljune rijei throw bez parametara.
Posebnu panju je potrebno posvetiti obraivanju iznimaka u sluajevima kada se
koristi dinamika dodjela memorije.
void Func1() {
char *mem = new char[100];
try {
Problematicna();
}
456
catch (...) {
throw; // prosljeivanje iznimke
}
delete [] mem;
}
try {
char *mem = new char[100];
Problematicna();
}
catch (...) {
throw; // prosljeivanje iznimke
}
class PokZnak {
private:
char *pokZn;
public:
PokZnak(int duljina) : pokZn(new char[duljina]) {}
PokZnak(char *pz) : pokZn(pz) {}
~PokZnak() { delete [] pokZn; }
operator char *() { return pokZn; }
};
void Func1() {
PokZnak mem = new char[100];
try {
Problematicna();
}
catch (...) {
throw; // prosljeivanje iznimke
}
}
class Izn {
public:
int izn;
Izn(int i) : izn(i) {}
};
void Func1() {
try {
// ... nekakva obrada
throw Izn(10);
// ostatak koda ...
}
catch (Izn& iznim) {
// sljedea naredba e ispisati 10
cout << "U Func1: " << iznim.izn << endl;
iznim.izn = 20;
throw;
}
}
void Func2() {
try {
Func1();
}
catch (Izn iznim) {
// sljedea naredba e ispisati 20
cout << "U Func2: " << iznim.izn << endl;
iznim.izn = 30;
throw;
}
}
458
void Func3() {
try {
Func2();
}
catch (Izn iznim) {
// sljedea naredba e ispisati opet 20
cout << "U Func3: " << iznim.izn << endl;
}
}
U prvom sluaju parametar bloku hvatanja je definiran kao referenca na objekt klase
Izn. To znai da e blok hvatanja baratati zapravo s baenim objektom, a ne s
njegovom kopijom. To, nadalje, znai da e svaka promjena na objektu biti upisana u
originalni objekt. Ako se taj objekt proslijedi nadreenom bloku, na njemu e biti
upisane sve izmjene.
U drugom sluaju, parametar bloku hvatanja je sam objekt. Vrijednost baenog
objekta e se prekopirati u lokalni objekt koji e se proslijediti bloku hvatanja na
obradu. Za to kopiranje koristi se konstruktor kopije klase. Kako sada blok hvatanja
barata s kopijom, promjena u vrijednosti objekta nee biti proslijeena nadreenom
bloku. Kopija e se unititi prilikom izlaska iz bloka hvatanja (pomou destruktora, ako
postoji), a proslijedit e se originalni objekt.
U prvom sluaju specificirana je funkcija koja moe baciti iznimku cjelobrojnog tipa te
znakovni niz. Ako sluajno neka iznimka drukijeg tipa osim navedenih ipak promakne
obradi te bude baena iz funkcije, pozvat e se predefinirana funkcija unexpected()
koja e tipino izazvati zavretak programa. U drugom sluaju se specificira funkcija
koja ne smije baciti nikakvu iznimku.
Vano je uoiti da lista moguih iznimaka ne definira iznimke koje se smiju
pojaviti unutar funkcije, nego iznimke koje mogu biti proslijeene iz funkcije, bilo da su
baene u samoj funkciji ili su bile baene u nekoj funkciji pozvanoj iz funkcije. To znai
da e se sljedei kod ispravno izvesti:
Iako se iznimka tipa const char * pojavljuje unutar same funkcije, ona se tamo i
obrauje. Izvan funkcije se baca samo iznimka cjelobrojnog tipa, to udovoljava
specifikaciji liste moguih iznimaka.
Naposljetku, potrebno je uoiti da lista moguih iznimaka nije dio potpisa funkcije,
to jest da deklaracije
class LogPogreska {
public:
double argument;
};
460
class ZnakovniNiz {
friend ZnakovniNiz operator +(ZnakovniNiz &a, ZnakovniNiz &b);
private:
char *pokNiz;
public:
ZnakovniNiz(char *niz = "");
ZnakovniNiz(const ZnakovniNiz &ref);
~ZnakovniNiz();
ZnakovniNiz::ZnakovniNiz(char *niz) :
pokNiz(new char[strlen(niz) + 1]) {
if (pokNiz == NULL) throw "Nema dovoljno memorije.";
strcpy(pokNiz,niz);
}
ZnakovniNiz::~ZnakovniNiz() {
delete [] pokNiz;
}
return *this;
}
U naem sluaju, iznimka koja se podie je tipa const char *, jer nismo htjeli
komplicirati primjer uvoenjem posebne klase koja e opisivati tip iznimke. U
stvarnosti bi se taj problem rijeio uvoenjem posebne klase, primjerice,
ZnakovnePogreske, koja e signalizirati sve pogreke prouzroene akcijama u klasi
ZnakovniNiz.
Ako sada elimo sa sigurnou koristiti znakovne nizove, sve naredbe koje ih
stvaraju, prosljeuju funkcijama ili na bilo koji drugi nain njima manipuliraju moraju
biti smjetene u blok pokuaja. Blok hvatanja mora biti podeen da hvata iznimke tipa
const char *:
int main() {
try {
ZnakovniNiz ime("Salvador ");
ZnakovniNiz prezime("Dal");
ZnakovniNiz slikar;
slikar = ime + prezime;
cout << (const char *)slikar << endl;
}
catch (const char *pogr) {
cout << pogr << endl;
return 1;
}
return 0;
}
463
class Linija {
// ...
public:
virtual void Crtaj();
// ...
};
// ...
};
U gornjem primjeru klasa Linija definira liniju na zaslonu raunala. Tu klasu smo
naslijedili i iz nje izveli klasu LinijaSaStrelicama koja opisuje liniju zakljuenu
strelicama na obje strane. Svaka od tih dviju klasa ima funkcijski lan Crtaj() koji
obavlja operaciju crtanja objekta na ekranu. No taj lan je definiran kao virtualan, to
znai da se njegov poziv obavlja dinamiki analizira se tip objekta za koji se trai
poziv, te se poziva ispravan lan:
ln->Crtaj();
#define CLS_LINIJA 1
#define CLS_LINIJASASTRELICAMA 2
class Linija {
// ...
public:
virtual int Tip() { return CLS_LINIJA; }
// ...
};
465
int main() {
Linija *niz[100];
int duljNiza = 0;
// inicijalizacija elemenata niza
niz[duljNiza++] = new Linija;
niz[duljNiza++] = new LinijaSaStrelicama;
// itd...
class type_info {
public:
virtual ~type_info();
bool operator==(const type_info&) const;
bool operator!=(const type_info&) const;
bool before(const type_info&) const;
const char* name() const;
private:
type_info(const type_info&);
type_info& operator=(const type_info&);
// podatkovni lanovi
};
Naziv klase se moe dobiti pomou poziva funkcijskog lana name(). Ponekad je
potrebno leksiki usporeivati type_info objekte. Tome slui funkcijski lan
before() ako je objekt na koji pokazuje parametar leksiki ispred objekta, onda se
vraa logika istina. To moe biti korisno ako bismo podatke o tipovima smjestili u
tablicu koju, radi breg pristupa elimo sortirati i primijeniti, primjerice, binarno
pretraivanje.
Operator typeid se moe primijeniti na proizvoljan izraz. Rezultat operatora
opisuje tip koji ima rezultirajua vrijednost izraza. Takoer, mogue je operator
primijeniti i na sam identifikator tipa, te se tada vraa objekt koji opisuje navedeni tip.
Na problem crtanja samo objekata klase Linija sada bi se mogao rijeiti na ovakav
nain:
Vie nije potreban virtualni funkcijski lan Tip koji e jednoznano opisivati tip
objekta tipovi se usporeuju direktno. Operator typeid se moe primijeniti i na
ugraene tipove. Vano je primijetiti da operator ne razlikuje konstantne i nekonstantne
tipove. Na primjer:
Linija ln1;
const Linija ln2;
dynamic_cast<T>(izr)
gdje izr predstavlja izraz iju je vrijednost potrebno pretvoriti, a T predstavlja eljeni
tip. Kae se da je pretvorba dinamika zato jer se ne obavlja prilikom prevoenja, nego
prilikom izvoenja programa.
Pretvorba e se provesti samo ako je na jednoznaan nain mogue od tipa izraza
izr doi do tipa T. Ovaj operator e obaviti i obinu pretvorbu u osnovnu klasu. Ako je
T pokaziva na osnovnu klasu objekta na koji pokazuje izr, tada je konverziju mogue
provesti statiki prilikom prevoenja.
Promotrimo sluaj kada je T izvedena klasa u odnosu na klasu na koju pokazuje
izr. Napominjemo da je postupak isti bez obzira radi li se o pokazivau ili referenci na
objekt. Najprije e se odrediti tip cijelog objekta na koji izr pokazuje. Ako se za taj tip
moe pronai podobjekt T kao jednoznana javna osnovna klasa, onda e rezultat
pretvorbe biti pokaziva (ili referenca) koja pokazuje na taj podobjekt. Jednostavnije
reeno, provjerit e se je li konverzija u izvedenu klasu dozvoljena s obzirom na stvarni
objekt koji se pretvara. Nul-pokaziva e se pretvoriti u samoga sebe.
469
Dinamike pretvorbe nisu svojstvo C++ jezika koje treba koristiti esto. Kao i operator
typeid, one naruavaju statike provjere tipa prilikom prevoenja te zbog toga
470
jednostavno nisu u duhu C++ jezika. Mnoge stvari za koje bi mogli doi u napast te
primijeniti dinamike pretvorbe, mogu se kvalitetnije rijeiti pomou predloaka.
Intenzivno koritenje dinamikih pretvorbi u veini sluajeva signalizira da korisnik
pokuava simulirati pristup programiranju koji se koristi u drugim objektno
orijentiranim jezicima, kao to je SmallTalk.
Objasnimo to na primjeru kontejnerskih klasa. U jezicima koji se oslanjaju na
dinamiku provjeru tipa kontejnerski objekt se najee rjeava tako da se stvori klasa
koju svi objekti koje elimo smjestiti u kontejner mora naslijediti. Na primjer, to smo i
mi uinili u poglavlju o nasljeivanju: lanove koje smo htjeli smjetati u listu morali
smo naslijediti od klase Atom. Kontejnerska klasa Lista manipulira s objektima klase
Atom, a podatak o tipu stvarnog objekta je izgubljen prilikom smjetanja objekta u
kontejner.
Kako klasa Lista ne zna nita o tipu podataka, prilikom itanja podataka iz
kontejnera potrebno je pomou operatora za dodjelu tipa pretvoriti vraeni pokaziva iz
pokazivaa Atom * u pokaziva na stvarni objekt. U poglavlju 9 o nasljeivanju jo
nismo prezentirali dinamiku dodjelu tipa, pa smo bili prisiljeni koristiti statiku dodjelu
tipa. Iz tamo navedenog primjera se vidi da je kd dosta nerazumljiv i podloan
pogrekama. Bolje je rjeenje pomou predloaka: za svaki mogui tip koji se smjeta u
kontejner generira se posebna klasa koja je u stanju vratiti ispravan pokaziva. Time se
smanjuje zamor programera te se uklanjaju mogui izvori pogreaka. Primjetite da u
SmallTalku to nije problem: tip objekta se ne provjerava prilikom prevoenja, nego
prilikom izvoenja. Ako programer pozove neku funkciju na objektu krivog tipa,
program e se jednostavno prekinuti uz poruku o pogreki. No za takav luksuz cijena je
velika: SmallTalk programi se izvode znatno sporije od C++ ekvivalenata te se mnoge
pogreke uoavaju tek kada se program izvede.
Sigurno si postavljate pitanje zato smo uope naveli takav primjer. Odgovor je u
tome to je opisani nain bio dugo vremena i jedini: mnogi prevoditelji nisu podravali
predloke te su korisnici jednostavno kopirali pristup iz drugih programskih jezika.
Moda tovani itatelj ima upravo takav prevoditelj. ak da to i nije sluaj, osobno
smatramo da je korisno upoznati razliite stilove programiranja, s njihovim prednostima
i manama. Niti jedno znanje nije suvino!
Odmah emo to i dokazati: to ako na kontejner mora uvati objekte razliitog
tipa? U tom sluaju izneseni pristup je i jedini. Zamislimo da korisnimo biblioteku
grafikih elemenata iz poglavlja 9 o nasljeivanju te elimo dodati novi objekt za
crtanje Bezierovih krivulja Bezier. Objekti tog tipa e sigurno imati sasvim razliite
lanove za postavljanje pozicije od klase Linija. Javno suelje tih klasa nikako ne
moemo objediniti virtualnim funkcijama u korijenu hijerarhije, pa da bi nam bilo
svejedno s kojim pokazivaem radimo. tovie, nekada niti ne moemo promijeniti
korijen hijerarhije, jer se nalazi u biblioteci koju ne moemo mijenjati. U tom sluaju
jedino rjeenje je nakon dobavljanja objekta iz kontejnera iskoristiti dinamiku
pretvorbu nanie te pristupiti specifinim lanovima:
Lista lst;
// lista se puni s objektima iz hijerarhije grafikih objekata
471
Ovdje vrijedi isto to smo rekli i za typeid: ako kasnije dodamo novi objekt u
hijerarhiju, morat emo promijeniti program tako da na adekvatan nain podrava nove
klase.
const_cast<T>(izr)
Pri tome izraz izr i tip T moraju biti jednaki (oba moraju biti pokazivai ili reference na
objekt), s time da se mogu razlikovati u kvalifikatoru const i volatile. U tom sluaju
e se pretvorba provesti te e se novonastali objekt tretirati kao da mu je uklonjen (ili
dodan) const odnosno volatile kvalifikator. Na primjer:
static_cast<T>(izr)
reinterpret_cast<T>(izr)
Tip T oznaava ciljni tip dodjele te mora biti pokaziva, referenca, aritmetiki tip ili
pobrojenje. Za veinu tipova gornji operatori e dati isti rezultat kao i obian operator
dodjele tipa
(T)izr
Pretvorba e se obaviti na isti nain. Postavlja se pitanje emu su onda uope uvedeni
ovi novi operatori, te u emu je njihova razlika.
Osnovna razlika je nain na koji se tretiraju klase prilikom nasljeivanja. Naime, tip
T kod operatora static_cast mora u potpunosti biti poznat prilikom prevoenja. To
znai da klasa u koju se pretvara mora biti u cijelosti definirana (a ne samo deklarirana
unaprijed). Pogledajmo to se dogaa prilikom koritenja tog operatora i viestrukog
nasljeivanja:
class Osnovna1 {
public:
int osn1;
};
class Osnovna2 {
public:
int osn2;
};
int main() {
Izvedena i;
i.osn1 = i.osn2 = i.izv = 0;
Osnovna2 *pok = static_cast<Osnovna2 *>(&i);
pok->osn2 = 30;
cout << i.osn1 << endl << i.osn2 << endl;
return 0;
}
int main() {
Izvedena i;
i.osn1 = i.osn2 = i.izv = 0;
Osnovna2 *pok = reinterpret_cast<Osnovna2 *>(&i);
pok->osn2 = 30;
cout << i.osn1 << endl << i.osn2 << endl;
return 0;
}
#include "toupet"
Postoji sutinska razlika izmeu ova dva naina omeivanja imena datoteke. Kada je
ime datoteke navedeno unutar znakova < i >, ona se trai u standardnom imeniku za
476
postaviti vrijednost prvog niza (makro ime NEKAJ) na vrijednost navedenu u nastavku
naredbe (NETO DRUGO UMJESTO NEKAJ). Naie li pretprocesor na simbol NEKAJ u
djelu programa koji slijedi, svaki put e ga zamijeniti nizom
NETO DRUGO UMJESTO NEKAJ.
Radi boljeg razumijevanja prouit emo kako bi gornja naredba #define djelovala na
nekoliko razliitih pojava niza NEKAJ (bez obzira na smisao konanog kda).
Pretpostavimo da gornjoj naredbi slijede naredbe:
a = NEKAJ; // nadomjestit e
char *b = "NEKAJ"; // nee nadomjestiti
c = PONEKAJ; // nee nadomjestiti
#define PI 3.14159
to neete moi napraviti tako da napiete PI, jer on ne postoji kao podatak u programu.
Umjesto toga morat ete napisati broj 3.14159. A tko vam pritom jami da je PI na tom
mjestu zaista nadomjeten tim brojem? Ne postoji nain da to izravno provjerite, osim
posredno, preko tonosti konanog rezultata.
U naredbi #define mogue je navesti samo makro ime u tom sluaju se ono
definira na neku neodreenu vrijednost:
#define SOLO
#undef POZDRAV
cout << POZDRAV << endl; // pogreka: nema vie POZDRAV-a
Vrijednosti svih makro imena ostaju nepromijenjena kroz cijelu datoteku izvornog
kda, s izuzetkom _ _LINE_ _ i _ _FILE_ _ . Vrijednost makro imena _ _LINE_ _ se
automatski mijenja tijekom obrade izvornog kda, ali se moe promijeniti i
pretprocesorskom naredbom #line (vidi odjeljak 16.5). Vrijednost makro imena
_ _FILE_ _ se takoer moe promijeniti naredbom #line.
Rezervirana makro imena prvenstveno se koriste tijekom razvoja programa, za
otkrivanje i lociranje pogreaka u kdu. elimo li da se prilikom izvoenja izvedbenog
kda nastalog prevoenjem neke naredbe ispie redni broj retka te naredbe u datoteci
izvornog kda, ime datoteke izvornog kda, te datum i vrijeme prevoenja (koliko god
to bilo besmisleno), iza te naredbe dodat emo naredbu:
c = KVADRAT(10); // izvorni kd
Meutim, makro funkcije znaju vrlo malo o sintaksi C++ jezika, ve vrte svoj film.
Stoga e sljedei poziv makro funkcije:
pretprocesor razviti u kd
480
Kao to vidimo, argument e se uveati dva puta, a rezultat pouzdano nee biti kvadrat
(ak ni tako uveanog) broja. Konani ishod e se oito razlikovati od onoga to bi
naivni korisnik u prvi mah oekivao.
itatelj je zacijelo primijetio mnotvo zagrada u definiciji makro funkcije. One su
neophodne da bi se sauvala hijerarhija operacija u sluaju da se kao argument makro
funkcije navede neki sloeni izraz. Da smo izostavili zagrade u definiciji, na primjer:
#define KVADRAT_UPITNI(x) x * x
d = KVADRAT_UPITNI(2 + 3);
U ovako razvijenom kdu, zbog razliitog prioriteta operacija, rezultat nee odgovarati
oekivanom.
Kao to vidimo, makro funkcije nemaju uvida u sintaksu jezika C++, pa tako ne
znaju nita o pravilima konverzije tipova, dozvoljenim operatorima, hijerarhiji
operacija. Budui da prevoditelj ne obrauje izvornu datoteku, ve datoteku koju je
generirao pretprocesor (a korisnik u normalnim okolnostima nema uvid u tu
meudatoteku), vrlo je teko ui u trag pogrekama koje nastaju zbog nepravilno
definiranih makro funkcija. Uostalom, programski jezik C++ prua pogodnosti poput
predloaka funkcija, preoptereenja funkcija i umetnutih funkcija, koje ine makro
funkcije potpuno nepotrebnima. Primjena makro funkcija, naprotiv, jest opravdana za
traenje pogreaka u programu, o emu e biti rijei kasnije u ovom poglavlju.
na svakom eljenom mjestu u kdu. Naravno, gore navedeni naziv varijable varijabla
je proizvoljan i njega treba varirati shodno imenu varijable koju elimo ispisati.
Postupak moemo djelomino pojednostavniti tako da definiramo makro funkciju
koja e kao argument prihvaati ime varijable koju elimo ispisati:
#define PROVJERA(varijabla) \
cout << #varijabla " = " << varijabla << endl
PROVJERA(a);
#define NESTO_SASVIM_NOVO(i, j) (i ## j)
poziv u naredbi
int x6;
#endif naredbom. Unutar tog bloka moe postojati vie neovisnih grana, odvojenih s
jednom ili vie naredbi #elif ili (maksimalno) jednom naredbom #else. Kao to
vidimo, blokovi za uvjetno ukljuivanje kda podsjeaju strukturom na if-blokove u
jeziku C++, s time da se blokovi ne ograuju vitiastim zagradama, ve
pretprocesorskim naredbama.
Naredbom #if mogue je na makro ime primijeniti neki poredbeni operator. Na
primjer, elimo li napraviti vie inaica naeg programa s ispisom poruka na razliitim
jezicima, moemo na poetku programa definirati makro ime JEZIK i postaviti ga na
odreenu vrijednost. Unutar #if-#elif/#else-#endif blokova definirat emo poruke
za pojedine jezike:
Svaki puta kada elimo generirati inaicu programa za drugi jezik, dovoljno je
promijeniti definiciju niza JEZIK i ponovo prevesti i povezati program prevoditelj e
sam ukljuiti poruke na odgovarajuem jeziku.
Kada ne bismo koristili gornji pristup, trebali bismo generirati zasebne datoteke
izvornog kda za svaki jezik ili bismo, prilikom promjene jezika, trebali prepisivati
sadraje svih znakovnih nizova za taj jezik. Osim toga je i proirenje za nove jezike
(posebice ako se tekstovi svih poruka grupiraju) vrlo jednostavno.
U ispitivanjima uvjeta mogu se koristiti svi logiki i poredbeni operatori koji su
dozvoljeni i u jeziku C++ (!, &&, ||, ==, <, >, !=).
Pretprocesorskom naredbom #ifdef ispituje se je li neko makro ime definirano.
Ako jest, naredbe koje slijede se prevode; u protivnom se kd do pripadajue #elif,
#else ili #endif naredbe ne ukljuuje u prevoenje.
Pretpostavimo da razvijamo neki program za koji elimo prirediti i pokaznu (demo)
inaicu, koja e se od radne razlikovati po tome to nee omoguavati ispis rezultata na
pisa. Pomou pretprocesorskih naredbi moemo to rijeiti na sljedei nain:
#define DEMO
//...
#ifdef DEMO
cout << "Ovo je pokazna inaica mog programa za rjeavanje "
"Besselove diferencijalne jednadbe koja ne "
483
#ifdef NDEBUG
# define KONTROLA(izraz) ((void)0)
#else
# define KONTROLA(izraz) cerr << izraz << endl
#endif
KONTROLA(a);
484
assert(a != 0);
#error poruka
gdje je poruka tekst koji elimo ispisati. Ona e prilikom prevoenja prouzroiti ispis
poruke oblika:
Ova naredba se uglavnom koristi za prekid prevoenja ako se u nekom ispitivanju utvrdi
da nisu zadovoljeni svi neophodni uvjeti. Naredbu treba u tom sluaju umetnuti unutar
bloka za uvjetno prevoenje koji ispituje traene uvjete.
Za ilustraciju, osvrnimo se na primjer naeg multi-jezinog programa na stranici
482. elimo li se osigurati da makro ime JEZIK uvijek bude definirano, ubacit emo
sljedee naredbe:
#ifndef JEZIK
#error Makro ime JEZIK mora biti definirano.
#endif
#pragma parametar
// ...
#line 1 "umetnuto.cpp"
// slijedi umetnuti kd
Gotovo svaki ozbiljniji program se sastoji od nekoliko tisua ili desetaka tisua
redaka izvornog kda. Zbog sloenosti i duljine, takve je programe neophodno pisati u
odvojenim modulima. Pojedini modul se moe razvijati nezavisno, pri emu se
programer koncentrira samo na problem koji taj modul rjeava, a ne brine o sloenosti
cijelog sustava.
Takoer, takve programe nerijetko pie vie programera ili ak cijeli timovi
istovremeno, pa je i zbog efikasnosti posla neophodno rascjepkati kd. U ovom
poglavlju bit e dani upute i pravila kako dobro organizirati izvorni kd u sluaju da ga
je potrebno rasporediti u nekoliko datoteka. Takoer e biti govora o povezivanju kda
pisanog u C++ jeziku s programskim kdom u drugim jezicima.
objektnom kdu. Ako je broj tih naziva prevelik, memorijski prostor namijenjen za
njihovo pohranjivanje e se prepuniti, to e onemoguiti daljnje prevoenje kda.
Istina, veliina tog prostora moe se poveati, ali to rjeenje nije vjeno.
Trei argument za rasporeivanje izvornog kda u vie datoteka jest preglednost.
Navedu li se definicije svih klasa u jednoj datoteci kd e biti nepregledan puno je
praktinije ako se definicije pojedinih klasa strpaju u zasebne datoteke. Time ete si
kasnije znaajno olakati moebitno ukljuivanje pojedinih klasa u neki drugi program.
etvrti argument je vezan uz timski rad: ako program ne pie jedan programer ve
itava ekipa, tada je daleko praktinije ako svaki programer pie kd u vlastitoj datoteci.
Uostalom, i programeri znaju biti sitne due koje ne vole da im se brlja po njihovim
umotvorinama.
Poneki itatelj e kao argument protiv navesti injenicu da je daleko tee
kontrolirati vei broj datoteka. Sreom, svi dananji prevoditelji/povezivai
omoguavaju objedinjavanje vie datoteka u cjeline projekte. Takvi projekti
dozvoljavaju ak da su datoteke izvornog kda razbacane na vie mjesta (u razliitim
imenicima, pa i na razliitim diskovima).
Oito je da e prije ili kasnije svakom programeru datotena koa postati
pretijesna i da e, unato svim pokuajima odgaanja, u sudbonosnom trenutku u
afektu otvoriti novu datoteku izvornog kda. Kako se datoteke izvornog koda prevode
zasebno, svakoj datoteci moraju biti dostupne deklaracije objekata i funkcija koje se u
njoj koriste. Sutinski gledano, izvorni kd mora biti jednako konzistentan kao kada je
cijeli program napisan u jednoj datoteci. Ako se ta konzistentnost izgubi, poveziva e
prijaviti pogreku prilikom povezivanja.
Budui da postupak naknadnog razbijanja kda moe biti vrlo mukotrpan, od
presudnog je znaaja pravovremeno sagledati kompleksnost nekog problema i od
poetka krenuti s pravilnim pristupom.
17.2. Povezivanje
U poglavlju 1 opisano je kako se prevoenje izvornog kda u izvedbeni sastoji od dva
koraka: prevoenja i povezivanja. U prvi mah poetniku nije jasna razlika izmeu ta dva
koraka. tovie, za programe iji je izvorni kd u cijelosti smjeten u jednu datoteku
nema svrhe razluivati ta dva koraka slika postaje jasnija u sluaju kada je program
razbijen u vie modula.
Tijekom prevoenja provodi se sintaksna i semantika provjera, pri emu se meu
ostalim provjerava odgovaraju li pozivi funkcija njihovim deklaracijama. Budui da se
prevoenje provodi za svaki imbeni modul zasebno, svaki od njih mora sadravati
deklaracije funkcija koje se unutar tog modula pozivaju. Na primjer, u datoteci
poziv.cpp poziva se funkcija cijaFunk(), koja je definirana u datoteci
definic.cpp. Da bi prevoditelj mogao pravilno prevesti kd datoteke poziv.cpp,
njemu prije poziva treba na neki nain predoiti deklaraciju te funkcije:
488
poziv.cpp
definic.cpp
Sada se deklaracija i definicija funkcije meusobno slau u potpisu (tj. broju i tipovima
argumenata funkcije), ali se razlikuju u tipu povratne vrijednosti. Poziv funkcije u
datoteci poziv.cpp bit e preveden u skladu s deklaracijom funkcije u toj datoteci, tj.
kao void cijaFunk(int), dok e definicija funkcije u datoteci definic.cpp biti
prevedena kao int cijaFunk(int). Funkcije su jednake po potpisu i poveziva nee
uoiti da se deklaracija i definicija funkcije razlikuju po tipu povratne vrijednosti.
Posljedica toga e biti pogreka u izvoenju programa, pri pokuaju povratka iz
Moete varati sve ljude neko vrijeme, moete ak varati neke ljude cijelo vrijeme, ali ne
moete varati sve ljude cijelo vrijeme Abraham Lincoln (1809-1865)
489
funkcije. Naime, generirani kd funkcije e pri izlasku iz nje na stog staviti cjelobrojni
povratni broj kojeg, meutim, pozivajui kd nee pokupiti budui da on za dotinu
funkcija oekuje da je tipa void.
prima.cpp
seconda.cpp
tertima.cpp
prva.cpp
druga.cpp
void funkcija() {
class LokalnaKlasa { // klasa bez povezivanja
//...
};
}
suveren1.cpp
suveren2.cpp
svecnst1.cpp
svecnst2.cpp
johann.cpp
bastian.cpp
bwv = toccata();
}
int bwv;
e postati definicijom globalne varijable bwv. Poput svih globalnih i statikih objekata,
budui da nema eksplicitno pridruene vrijednosti, varijabla bwv e se inicijalizirati na
podrazumijevanu vrijednost 0. Kd u datoteci bastian.cpp e biti preveden bez
pogreke, ali e prilikom povezivanja biti dojavljena pogreka da u programu postoji
jo jedan objekt s istim imenom, onaj definiran u datoteci johann.cpp.
Naglasimo da sama kljuna rije extern nije dovoljni jamac da bi se naredba
interpretirala kao deklaracija. Naime, ako unato kljunoj rijei extern, varijabli
pridruimo vrijednost:
modul_1.cpp
int crniVrag = 1;
int zeleniVrag = 5;
extern float zutiVrag;
494
modul_2.cpp
pripadajue definicije, ali se imenu datoteka zaglavlja umjesto nastavaka .cpp, .c ili
.cp dodaje nastavak .h (ponekad .hpp).
to valja staviti u datoteke zaglavlja? Budui da su objekti i funkcije s vanjskim
povezivanjem vidljivi u svim modulima, ako elimo omoguiti pravilno prevoenje
pojedinih modula, oito je neophodno navesti deklaracije svih objekata i funkcija koji e
biti koriteni izvan modula u kojem su definirani. Kako esto ne moemo unaprijed
znati hoe li neka funkcija ili objekt zatrebati u nekom drugom modulu, nije na odmet u
datotekama zaglavlja navesti deklaracije svih (nestatikih) globalnih funkcija i objekata.
Osim toga, navoenjem svih deklaracija u datoteci zaglavlja kd e biti pregledniji, jer
e sve deklaracije (osim za statike objekte i funkcije) biti na jednom mjestu.
Vjerujemo da je svakom jasno da u datoteke zaglavlja takoer treba staviti
definicije objekata i funkcija s unutarnjim povezivanjem (inline funkcije i simbolike
konstante), elimo li da oni budu dohvatljivi i iz drugih datoteka. Isto tako, u
datotekama zaglavlja treba navesti definicije tipova (deklaracije klasa) i predloke.
Radi preglednosti, nabrojimo po tokama glavne kandidate za navoenje u
datotekama zaglavlja:
Komentari, ukljuujui osnovne generalije vezane uz datoteku (autori, opis,
prepravke):
/**********************************************************
**********************************************************/
#include <iostream.h>
#include "MyString.h"
class KompleksniBroj {
private:
double Re;
double Im;
public:
//...
};
496
Pobrojenja:
#define PI 3.14159265359
#define abs(a) ((a < 0) ? -a : a)
Gore navedene toke ine samo prijedlog one nisu odreene pravilima jezika C++, no
do gornjih pravila su doli programeri tokom dugih zimskih veeri provedenih uz
raunalo. Iako u deklaracijama klasa i funkcija u datotekama zaglavlja nije neophodno
navoditi imena argumenata (dovoljno je navesti tipove), zbog preglednosti i
razumljivosti deklaracija one se stavljaju. Neki programeri preferiraju da se u deklaraciji
navede dulje ime da bi se lake shvatilo znaenje pojedinog argumenta, dok u definiciji
koriste skraena imena da bi si skratili muke utipkavanja prilikom pisanja tijela
funkcije.
Sada nakon to znamo to se obino stavlja u datoteku zaglavlja, navedimo i to ne
valja tamo stavljati :
Definicije funkcija:
Definicije objekata:
int a;
Ako u datoteku zaglavlja stavimo definiciju objekta, onda emo u svakoj datoteci u
koju ukljuimo datoteku zaglavlja dobiti po jedan globalni simbol navedenog imena
(u gornjem primjeru to je varijabla i). Takav program e prouzroiti pogreke
prilikom prevoenja.
Definicije konstantnih agregata, na primjer polja:
kompl.h
#include <iostream.h>
kompl.cpp
#include <iostream.h>
#include "kompl.h"
pozovi.cpp
#include <iostream.h>
#include "kompl.h"
int main() {
Kompleksni<double> c1(5.0, 9.0);
Kompleksni<int> c2(4, 3);
cout << "Prvi broj je: " << c1 << ", a drugi je " << c2;
}
kompl.cpp
kompldbl.cpp
komplint.cpp
tocka.h
class Tocka {
public:
Tocka(double x = 0, double y = 0);
double x() const { return x_koord; }
double y() const { return y_koord; }
502
#endif
tocka.cpp
#include "tocka.h"
Tocka::Tocka(double x, double y) {
x_koord = x;
y_koord = y;
}
tocka.cpp pravac.cpp
#include "tocka.h" #include "pravac.h"
// definicije // definicije
// lanova klase // lanova klase
// Tocka // Pravac
presjek.h
#include "pravac.h"
poglavar.cpp #include "kruznica.h"
#include "tocka.h" // deklaracije
#include "pravac.h" // funkcija iz
#include "kruznica.h" // presjek.cpp
#include "presjek.h"
// definicije
// funkcija
pravac.h
#ifndef PRAVAC_H
#define PRAVAC_H
504
#include "tocka.h"
class Pravac {
public:
Pravac(Tocka t, double dx, double dy);
double ka() const { return a; }
double kb() const { return b; }
double kc() const { return c; }
private:
double a;
double b;
double c;
};
#endif
pravac.cpp
#include "pravac.h"
Klasa Kruznica opisuje krunicu pomou sredita krunice i njenog polumjera. Klasa
sadri konstruktor, koji inicijalizira sredite i polumjer, te lanove za dohvaanje
koordinata sredita i polumjera. Kako je klasa Kruznica jednostavna te su se svi
lanovi mogli ostvariti umetnutima, cijela je smjetena u datoteku kruznica.h
uope nema potrebe za posebnom datotekom definicije.
kruznica.h
#ifndef KRUZNICA_H
#define KRUZNICA_H
#include "tocka.h"
#include "pravac.h"
class Kruznica {
public:
Kruznica(Tocka s, double r) : srediste(s), polumjer(r) {}
double r() const { return polumjer; }
double x0() const { return srediste.x(); }
double y0() const { return srediste.y(); }
private:
Tocka srediste;
505
double polumjer;
};
#endif
presjek.h
#ifndef PRESJEK_H
#define PRESJEK_H
#include "pravac.h"
#include "kruznica.h"
#endif
presjek.cpp
#include "presjek.h"
#include <math.h>
double x1 = -B / 2. / A;
double y1 = -a_b * x1 - c_b;
t1.pomakni(x1, y1); // obje tocke jednake
t2.pomakni(x1, y1);
return PravacDiraKruznicu;
}
double x1 = (-B + sqrt(diskr)) / 2. / A;
double y1 = -a_b * x1 - c_b;
t1.pomakni(x1, y1);
x1 = (-B - sqrt(diskr)) / 2. / A;
y1 = -a_b * x1 - c_b;
t2.pomakni(x1, y1);
return DvaPresjecista;
}
else {
// kd za vertikalni pravac ete,
// naravno, napisati sami!
}
}
poglavar.cpp
#include <iostream.h>
#include "tocka.h"
#include "pravac.h"
#include "kruznica.h"
#include "presjek.h"
}
}
int main() {
Tocka t0(1., 1.); // postavljamo toku
double r = 5.; // polumjer krunice
Kruznica k(t0, r); // definiramo krunicu
Pravac p(t0, 1., 1.); // pravac kroz sredite krunice
// pod 45 stupnjeva
PozoviPaIspisi(k, p);
return 0;
}
#ifndef TOCKA_H
#define TOCKA_H
// deklaracija i definicije
#endif
#define TOCKA_H
508
tocka.h pravac.h
#include "tocka.h"
// deklaracija
// klase Tocka // deklaracija
// klase Pravac
poglavar.cpp
#include "tocka.h"
#include "pravac.h"
//...
kojom se makro ime TOCKA_H definira (na neku neodreenu vrijednost). Zbog toga e
kod sljedeeg ukljuivanja datoteke tocka.h (na primjer, prilikom ukljuivanja
datoteke pravac.h) makro ime TOCKA_H biti definirano pa e se ponovno prevoenje
deklaracije klase Tocka.
(osim naravno kineskog, kojeg govori treina ovjeanstva). Kako je jezik C++
nadogradnja jezika C (C++: As close as possible to C but no closer), prevoenje
izvornog kda pisanog u C-u na C++ prevoditelju ne bi trebala predstavljati vei
problem. Primijetite naglasak na ne bi trebala: stopostotna kompatibilnost s C-om nije
ostvarena, te su oko toga bile voene une polemike. Na kraju, utvreno je da se ipak
dio kompatibilnosti mora rtvovati kako bi se ostvarile nove mogunosti.
Dio nekompatibilnosti je uzrokovan time to C++ ima nove kljune rijei, iji se
nazivi ne smiju koristiti za nazive identifikatora. Ako C program deklarira, primjerice,
cjelobrojnu varijablu naziva class, takav program se nee moi prevesti C++
prevoditeljem. Nadalje, podruja pojedinih imena se neznatno razlikuju u jezicima C i
C++. Na primjer, u jeziku C se ugnijeene strukture mogu koristiti u globalnom
podruju bez navoenja imena podruja u koje je struktura ugnijeena. Zbog takvih i
slinih primjera, ponekad moe biti jednostavnije ne prevoditi C kd C++
prevoditeljem, nego jednostavno C kd prevesti C prevoditeljem i povezati ga s C++
kdom. Potekoe takoer nastaju kada izvorni kd u C-u nije dostupan, pa ga se ne
moe ponovo prevesti C++ prevoditeljem.
Povezivanje objektnih kdova pisanih u razliitim jezicima nije trivijalno. Naime,
objektni kdovi dobiveni razliitim prevoditeljima, pa ak i objektni kdovi dobiveni
prevoenjem istog jezika, ali na prevoditeljima raznih proizvoaa, meusobno nisu
kompatibilni. Primjerice, redoslijed prenoenja argumenata funkciji ili struktura
pohranjivanja pojedinih tipova podataka se razlikuju za C i Pascal. Standard jezika C++
podrava povezivanje samo s jezikom C, ali prevoditelji pojedinih proizvoaa esto
podravaju povezivanje s drugim jezicima za to morate pogledati upute za
prevoditelj/poveziva koji koristite.
C++: Blizu jeziku C koliko je god mogue, ali nita blie naslov lanka A. Koeniga i
B. Stroustrupa u asopisu C++ Report.
510
extern "C" {
float funkcijaCSimplex(int);
int rand(); // standardne C-funkcije
void srand(unsigned int seed); // za generiranje
// sluajnih brojeva
}
extern "C" {
#include "mojeCfun.h"
}
#ifdef _ _cplusplus
extern "C" {
#endif
// slijede deklaracije...
#ifdef _ _cplusplus
}
#endif
asm ( znakovni_niz );
Svaki program ima smisla samo ako moe komunicirati s vanjskim svijetom nas ne
zanimaju programi koji hiberniraju sami za sebe (poput Mog prvog C++ programa). U
najjednostavnijem sluaju elimo da program koji pokreemo barem ispie neku poruku
na zaslonu raunala (Dobar dan, gazda. Drago mi je da Vas opet vidim. Kako Vai
bubreni kamenii?).
Ulazni i izlazni tokovi (engl. input and output streams) osiguravaju vezu izmeu
naeg programa i vanjskih jedinica na raunalu: tipkovnice, zaslona, disketne jedinice,
diska, CD-jedinice. U ovom poglavlju upoznat emo s osnovama tokova: upoznat emo
se s upisom podataka preko tipkovnice, ispisom na zaslonu raunala te s itanjem i
pisanjem datoteke.
18.1. to su tokovi
U mnogim dosadanjim primjerima smo u kd ukljuivali iostream.h datoteku
zaglavlja. U njoj su deklarirani ulazni i izlazni tokovi koji su nam omoguavali upis
podataka s tipkovnice ili ispis na zaslonu raunala. Meutim, do sada nismo ulazili u
sutinu ulazno-izlaznih tokova, budui da nam njihovo poznavanje nije bilo neophodno
za razumijevanje jezika.
Sam programski jezik C++ ne definira naredbe za upis i ispis podataka. Naime,
ulazno-izlazne naredbe u pojedinim jezicima podravaju samo ugraene tipove
podataka. Meutim, svaki sloeniji C++ program sadri i sloenije, korisniki definirane
podatke, koji iziskuju njima svojstvenu obradu. Da bi se osigurala fleksibilnost jezika,
izbjegnuta je ugradnja ulazno-izlaznih naredbi u jezik.
Za ostvarenje komunikacije programa s okolinom, programer mora na raspolaganju
imati rutine koje e podatke svakog pojedinog tipa pretvoriti u raunalu u nizove bitova
te ih poslati na vanjsku jedinicu, odnosno obrnuto, nizove bitova s vanjske jedinice
pretvoriti u podatke u raunalu. U jeziku C++ to se ostvaruje pomou ulaznih i izlaznih
tokova. Tokovi su zapravo klase definirane u standardnim bibliotekama koje se
isporuuju s prevoditeljem. Sadraj tih klasa (svi funkcijski i podatkovni lanovi) je
standardiziran i omoguava gotovo sve osnovne ulazno-izlazne operacije. Osim toga,
zahvaljujui svojstvima jezika C++, taj se set operacija po potrebi moe proirivati.
Sam koncept tokova se nadovezuje na princip enkapsulacije nain ispisa podataka
je neovisan o tipu jedinice. Tako e se pojedini podatak na gotovo isti nain ispisati na
514
zaslon ili na disk (slika 18.1). Takoer, unos podataka je praktiki istovjetan radi li se o
unosu s tipkovnice ili diska. Nakon to se stvori odreeni tok, program e komunicirati
preko tokova, njima ostavljajui glavninu prljavog posla.
tipkovnica
monitor
ulazno-izlazni
tok
disk disk
streambuf ios
istream ostream
ifstream ofstream
if (cout)
cout << "Teem dalje" << endl;
else
cout << "Rijeka je presuila, tok vie ne tee." << endl;
517
Klasa ios definira i operator !. On vraa vrijednost razliitu od nule ako je tok u stanju
pogreke (failbit, badbit, eofbit), odnosno nula ako je tok u ispravnom stanju
(goodbit). Tako je test toka mogue i ovako obaviti:
if (!cin)
cerr << "Pukla je brana, tok curi.";
Mogue je dohvatiti stanje bilo kojeg izlaznog ili ulaznog toka (pa tako i za unos s
tipkovnice ili ispis na zaslon). Kod ulaznog toka stanje se najee koristi kako bi se
detektiralo da je korisnik upisao podatak koji tipom ne odgovara oekivanom (na
primjer, oekuje se cijeli broj, a korisnik unese znak). Kod tokova vezanih na datoteke
stanja su jo korisnija, jer se njima moe ustanoviti je li pojedina ulazno-izlazna
operacija uspjela. Stanje eofbit se esto ispituje prilikom itanja toka ono signalizira
da se stiglo do kraja datoteke. Stanja badbit, odnosno failbit se obino testira
prilikom upisa kako bi se provjerilo je li upis uspjeno proveden.
Za ispis pokazivaa (izuzev char * za ispis znakovnih nizova) bit e pozvan operator
ostream::operator<<(const void *), koji e ispisati heksadekadsku adresu
objekta u memoriji. Zato e izvoenje naredbi
int i;
cout << &i;
na zaslonu raunala ispisati neto poput 0x7f24. Operator umetanja moe se dodatno
preopteretiti za korisniki definirane tipove podataka, to e biti opisano u sljedeem
odjeljku.
Kao rezultat, operator << vraa referencu na objekt tipa ostream, a kako se
operator izvodi s lijeva na desno, mogue je ulanavanje vie operatora umetanja:
(cout.operator<<(1001)).operator<<(" no");
Meutim, pri ispisu izraza u kojima se koriste logiki, bitovni i operatori pridruivanja
je potrebno paziti jer oni imaju nii prioritet. Na primjer, izvoenje sljedeih naredbi
int prvi = 5;
int drugi = 7;
cout << "Vei od brojeva " << prvi << " i " << drugi << " je "
// problematian ispis:
cout << (prvi > drugi) ? prvi : drugi;
Vei od brojeva 5 i 7 je 0
tj. ispisat e rezultat usporedbe varijabli prvi i drugi, a tek potom e pristupiti
izvoenju uvjetnog operatora ?:. Kako se taj operator zapravo primjenjuje na objekt
tipa ostream &, poziva se operator za konverziju u void *, te se zapravo testira tok.
519
Primijenjeno na gornji primjer to znai da smo naredbu za ispis trebali napisati kao:
int i = 7, j = 0;
cout << i * j;
cout << (i && j);
cout << i && j;
class Kompleksni {
private:
double realni, imaginarni;
public:
Kompleksni(double r = 0, double i = 0) {
realni = r;
imaginarni = i;
}
double Re() {return realni;}
double Im() {return imaginarni;}
//...
};
2-8i
Zadatak. Deklarirajte klasu Tocka koja e imati dva privatna podatkovna lana za
pohranjivanje koordinate toke u pravokutnim koordinatama, te javne funkcijske
lanove x() i y() za dohvaanje tih lanova. Preopteretite operator umetanja << tako
da ispisuje koordinate toke u obliku: (x, y).
Zadatak. Program iz prethodnog zadatka proirite deklaracijom klase Krug koja e
imati dva privatna podatkovna lana: objekt srediste klase Tocka, te polumjer
(npr. tipa double). Preopteretite operator umetanja tako da parametre kruga ispisuje u
sljedeem obliku: (r = polumjer, t0(x0, y0)). Iskoristite pritom ve
preoptereeni operator za ispis koordinata toke iz prethodnog zadatka.
cout.flush();
ili preko manipulatora, koji se pomou operatora umetanja ubacuje u objekt cout:
Klasa ostream sadri jo dva manipulatora koja se vrlo esto koriste: endl i ends.
endl smo ve dosad koristili u naim primjerima; on umee znak za novi redak ('\n')
u tok i prazni tok funkcijskim lanom flush(). Manipulator ends umee zakljuni
nul-znak.
U klasi ostream definirana su i dva funkcijska lana koji slue za kontrolu
poloaja unutar toka. Funkcijski lan tellp() vraa trenutanu poziciju u toku, a
seekp() se postavlja na zadanu poziciju. Ti lanovi se koriste primarno za kontrolu
pristupa datotekama, jer omoguavaju proizvoljno pomicanje unutar datoteke, pa e biti
objanjeni kasnije u ovom poglavlju, u odsjecima posveenim radu s datotekama.
int i;
long l;
float f;
double d;
int i;
12 27 34.2 5
Upisan je broj 12
Upisan je broj 27
Upisan je broj 34
Fajrunat!
realni_dio + imaginarni_dio i
gdje realni_dio i imaginarni_dio mogu biti bilo kakvi realni brojevi. Praznine oko
realnog i imaginarnog dijela su proizvoljne jer je ulazni tok postavljen tako da jede
praznine oko uitanih podataka.
Ovo je tek rudimentarna definicija operatorske funkcije, koja slui samo kao
pokazni primjer. Funkcija obavlja samo osnovne provjere ispravnosti ulaznih podataka.
Ako se detektira neispravnost ulaznih podataka (npr. umjesto i, za imaginarnu jedinicu
se upie neki drugi znak), stanje toka se postavlja u failbit, a operacija se prekida.
Takoer, uoimo prvu naredbu u funkciji kojom se provjerava stanje toka prije
524
uitavanja. Ako je, zbog nepravilnih prethodnih operacija uitavanja, tok u stanju
pogreke, operacija uitavanja se odmah prekida. Tako e se, u sluaju ulananog
uitavanja podataka s toka, uitavanje prekinuti im se naie na prvu nepravilnost.
char text[80];
Za upisani tekst
Svakim
danom
u
svakom
pogledu
napredujemo.
char niz[80];
int i = 0;
while (cin.get(niz[i]))
if (niz[i++] == '\n')
break;
niz[i - 1] = '\0';
Nakon pokretanja programa, treba utipkati eljeni niz tek po pritisku na tipku za
unos poet e se izvoditi petlja koja e pojedine znakove uitavati u niz. Valja
525
paziti da duljina upisanog teksta mora biti manja od duljine polja niz. Takoer,
uoimo da nakon zavrenog uitavanja znakova, polju niz treba dodati zakljuni nul-
znak. Ovaj lan ima daleko praktiniju primjenu kod uitavanje podataka iz datoteke.
2. get() bez argumenata koji izluuje znak s ulaznog toka. lan je tipa int, a vraa
kd znaka koji je uitao. Nailaskom na znak EOF funkcijski lan e vratiti vrijednost
-1. Pomou ovog funkcijskog lana petlja u prethodnom primjeru bi se mogla
napisati na sljedei nain:
char niz[80];
cin.get(niz, sizeof(niz));
To znai da ako ulanimo nekoliko operacija uitavanja, drugi niz se nee uspjeti uitati
jer e sljedei get() odmah naii na '\n'. Isto tako e sljedei kd prouzroiti
beskonanu petlju:
Prvi i trei oblik funkcijskog lana get() vraaju kao rezultat pripadajui objekt tipa
istream, to omoguava njihovo ulanavanje, jednako kao i kod operatora <<,
odnosno >>.
Funkcijski lan getline() po potpisu je identian treem obliku funkcijskog lana
get(); deklariran je kao
Stoga se taj funkcijski lan poziva jednako kao i trei oblik lana get(), a i djeluje na
slian nain, s jednom znaajnom razlikom getline() uklanja zavrni znak '\n' s
ulaznog toka. Tako je mogue ulanavati operacije uitavanja pomou getline().
Funkcijski lan read() takoer uitava niz znakova zadane duljine, ali se njegovo
izvoenje prekida samo u sluaju nailaska na znak EOF. Deklariran je kao:
char podaci[n][11];
Kada u gornjem kdu ne bi bilo poziva funkcije ignore(), tok bi se nakon prvog
prolaza for-petlje zaustavio na desetom znaku. Ako podaci u prvom retku imaju vie
od deset znakova, u sljedeem prolazu petlje bi se itanje nastavilo od tog znaka pa bi
kao idui podatak bio uitan nastavak prvog retka.
Funkcijski lan peek() oitava sljedei znak, ali pritom taj znak i dalje ostaje na
toku, a lan putback() vraa eljeni znak na tok. Ilustrirajmo njihovu primjenu
jednostavnim primjerom u kojem se uitava tekst utipkan u jednom retku.
Pretpostavimo da e tekst biti proizvoljna kombinacija rijei i brojeva, meusobno
razmaknutih prazninama. Program e te rijei i brojeve ispisati u zasebnim recima. Da
bismo znali koji tip podatka slijedi, trebamo oitati prvi znak niza: ako je slovo ili znak
podcrtavanja, tada slijedi rije; ako je broj, decimalna toka ili predznak, imamo broj.
Poetni znak, meutim, ne smijemo izluiti s toka, jer on mora ui u niz koji tek treba
izluiti. U primjeru emo koristiti funkcije za razluivanje tipa znaka, deklarirane u
standardnoj datoteci zaglavlja ctype.h, tako da to zaglavlje valja ukljuiti na poetku
programa.
char znak;
char rijec[80];
double broj;
isalpha() vraa true ako je znak naveden kao argument funkciji slovo
('a'...'', 'A'...'').
isalnum() vraa true ako je znak naveden kao argument funkciji slovo ili
decimalna znamenka (tj. isalpha() || isdigit())
Jednostavnije je (a u veini sluajeva i daleko pouzdanije) koristiti ove funkcije nego
provoditi ispitivanja oblika:
('a' <= znak && znak <= 'z') || ('A' <= znak && znak <= 'Z')
Hoe li i koji nacionalni znakovi biti identificirani kao slova ovisi o parametrima definiranima
u klasi locale.
530
char zaporka[15];
cout << "Zaporka: ";
cin >> zaporka;
> 1234<
Podeena irina traje samo jedan ispis nakon toga ona se postavlja na
podrazumijevanu irinu. Zbog toga e izvoenje kda
531
> 1234<
>1234<
biti ispisan kompletan broj 1234, unato tome to je kao argument funkcijskom lanu
width() navedena irina od 2 znaka:
>1234<
Funkcija width() kao rezultat vraa prethodno zadanu irinu ispisa. Ako se ne navede
argument, funkcija width() vraa trenutno vaeu irinu.
Sva navedena razmatranja vrijede i za znakovne nizove. Funkcijski lan width()
se moe pozvati i za ulazne tokove u tom sluaju se njime ograniava broj uitanih
znakova, slino kao to smo to mogli postii funkcijskim lanom get().
irine ispisa podataka mogu se podeavati i pomou manipulatora setw(), koji e
biti opisan kasnije u ovom poglavlju.
>********1234<
cout.width(6);
// oktalni ispis
cout.setf(ios::oct);
cout << 16 << endl;
+11
+ 12
d
0xe
0x000f
020
+17
18
Prije prvog ispisa postavljena je zastavica za ispis predznaka '+' ispred pozitivnih
brojeva pomou zastavice setpos. Unato tome to je irina polja za ispis postavljena
na 8, prvi broj je ispisan uz predznak, jer je podrazumijevano poravnavanje ispisa
ulijevo. Tek nakon postavljanja zastavice internal (ili eventualno right) broj e biti
pomaknut udesno ove dvije zastavice oito imaju smisla samo u kombinaciji s lanom
width().
Postavljanjem zastavica hex i oct, dobiven je ispis cijelih brojeva u
heksadekadskom, odnosno oktalnom formatu, s time da je zastavicom showbase
ukljueno ispisivanje niza "0x" ispred heksadekadskog, odnosno znaka '0' ispred
oktalnog broja. Za ispis realnih brojeva zastavice hex, oct i showbase nemaju
znaenja.
Na kraju primjera naveden je i jedan poziv funkcije unsetf() kojom se brie
zastavica za ispis '+' ispred pozitivnih brojeva.
535
setf(0, ios::floatfield);
cout.setf(ios::scientific, ios::floatfield);
// u znanstvenom zapisu
cout << 13e4 << endl;
cout << 134 << endl;
10
11.0000
12
12000.000000
1234560000000.000000
1.300000e+05
134
1.400000E+01
ispisati
12.34
1.23457e+15
1.2345e-07
cout.precision(2);
12
1.2e+15
1.2e-07
Ako su postavljene zastavice fixed ili scientific, tada precision() odreuje broj
znamenki koje e se prikazati desno od decimalne toke. To znai da e izvoenje
naredbi
537
cout.precision(2);
fout.setf(ios::fixed, ios::floatfield);
fout << 12.34 << endl;
fout << 1234567890123456. << endl;
fout << 0.00000012345 << endl;
fout.setf(ios::scientific, ios::floatfield);
fout << 12.34 << endl;
fout << 1234567890123456. << endl;
fout << 0.00000012345 << endl;
12.34
1234567890123456.00
0.00
1.23e+01
1.23e+15
1.23e-07
1.2
1234.
9.123
18.6.6. Manipulatori
U prethodnim odjeljcima smo vidjeli kako moemo pozivima pojedinih funkcijskih
lanova kontrolirati format ispisa. Iako ovakav nain omoguava potpunu kontrolu nad
formatom, on ima jedan veliki nedostatak: naredbe za postavljanje formata morali smo
pisati kao zasebne naredbe. Na primjer:
cout.width(12);
cout.setf(ios::showpos);
cout << 1.;
Ovakvim pristupom gubi se nit kontinuiteta izlazne naredbe iako su sve tri naredbe
vezane za ispis jednog podatka, morali smo ih napisati odvojeno.
Uvoenjem manipulatora (engl. manipulators) omoguena je bolja povezanost
kontrole formata i operatora izluivanja, odnosno umetanja. Umjesto da se naredba za
538
formatiranje poziva kao zaseban funkcijski lan, ona se jednostavno umee na tok. Tako
gornje naredbe moemo pomou manipulatora napisati preglednije, na sljedei nain:
Napomenimo da manipulatori flush, endl i ends djeluju samo za izlazne tokove, dok
manipulator ws djeluje samo za ulazne tokove.
Zadatak. Napiite programske odsjeke iz prethodna dva odjeljka tako da umjesto
funkcijskih lanova upotrijebite manipulatore.
Obratimo panju na to kako su manipulatori realizirani. Naime, i mnogim C++ znalcima
je trebalo dosta vremena dok su uspjeli shvatiti kojeg je tipa zapravo taj endl koji se
539
upisuje u izlaznu listu. Radi se, naime, o jednom lukavom triku: svi manipulatori bez
parametra su realizirani kao funkcija koja kao parametar uzima referencu na tok. Tako u
datoteci zaglavlja iostream.h postoji deklaracija te funkcije koja ispisuje znak '\n'.
No kada se u ispisnoj listi navede samo endl bez zagrada, to je zapravo pokaziva na
funkciju. Operator << je preoptereen tako da prihvaa pokaziva na funkciju, te on u
biti poziva funkciju navodei tok kao parametar. Evo kako je taj operator definiran:
Tip Omanip je pokaziva na funkciju koja kao parametar uzima referencu na ostream i
vraa referencu na ostream. Izraz
se interpretira kao
operator<<(cout, endl);
Na taj nain mogue je dodavati vlastite manipulatore. Na primjer, mogli bismo dodati
manipulator cajti koji ispisuje vrijeme na tok. Napominjemo da nije potrebno
dodavati novi operator << za ispis; on je ve definiran u iostream.h. Evo kda
manipulatora:
#include <time.h>
#include <iostream.h>
#include <iostream.h>
#include <iomanip.h>
int main() {
cout << "Amo" << razmaka(5) << "!" << endl;
return 0;
}
Pogledajmo za poetak vrlo jednostavan primjer program koji e uitati sadraj neke
tekstovne datoteke i ispisati ga na zaslonu:
#include <fstream.h>
#include <stdlib.h> // ukljueno zbog funkcije exit()
542
Ime datoteke iji sadraj se eli ispisati navodi se kao parametar pri pokretanju
programa. U sluaju da nije naveden parametar ispisuje se kratka uputa, a ako se navede
previe parametara ispisuje se poruka o pogreki.
Usredotoimo se na nama najvaniji dio programa: inicijalizaciju ulaznog toka i
uitavanje podataka iz datoteke. Ulazni tok je objekt datnica klase ifstream.
Prilikom inicijalizacije kao argument konstruktoru se prenosi ime datoteke. Zatim se
ispituje da je li inicijalizacija uspjela. Ako inicijalizacija nije uspjela (na primjer ako
navedena datoteka nije pronaena ili se ne moe itati), program ispisuje poruku o
pogreki i prekida daljnje izvoenje.
datoteke na raspolaganju su nam svi lanovi klase istream, od kojih smo veinu
upoznali.
Koritenje izlaznih tokova za upis u datoteku predoit emo sljedeim primjerom
programom koji kopira sadraj tekstovne datoteke, izbacujui pritom sve viestruke
praznine i prazne retke. Ime izvorne datoteke neka se navodi kao prvi parametar
prilikom pokretanja programa (slino kao u prethodnom primjeru), a ime preslikane
datoteke neka je drugi parametar. Zbog slinosti s prethodnim primjerom, izostavit
emo provjere parametara i navesti samo efektivni dio kda:
//...
ifstream izvornik(argv[1]);
if (!izvornik) {
cerr << "Ne mogu otvoriti ulaznu datoteku "
<< argv[1] << endl;
exit(1);
}
ofstream ponornik(argv[2]);
if (!ponornik) {
cerr << "Ne mogu otvoriti izlaznu datoteku "
<< argv[2] << endl;
exit(1);
}
char znNiz[80];;
while (izvornik >> znNiz) {
ponornik << znNiz;
if (izvornik.peek() == '\n')
ponornik << endl;
else if (izvornik.peek() != EOF)
ponornik << " ";
}
if (!izvornik.eof() || ponornik.bad())
cerr << "Uuups! Nekaj ne tima" << endl;
//...
U ovom primjeru otvaraju se dva toka: izvornik klase ifstream koji se pridruuje
datoteci iz koje se sadraj iitava, te ponornik klase ofstream koji se pridruuje
datoteci u koju se upisuje novi sadraj. Prilikom inicijalizacije oba toka, konstruktorima
se kao argumenti prenose imena pripadajuih datoteka, a potom se provjerava je li
inicijalizacija uspjeno provedena.
U while-petlji se sadraj izvorne datoteke uitava pomou operatora >> u polje
znakova znNiz. To znai da e se izvorna datoteka itati rije po rije. Budui da
operator >> preskae sve praznine izmeu rijei, u izlazni tok treba umetati po jednu
bjelinu nakon svake rijei. Ako se iza rijei pojavio znak za novi redak on se upisuje
umjesto praznine u protivnom bi cijela izlazna datoteka bila ispisana u jednom retku.
Bjelina se ne smije dodati iza zadnje rijei u datoteci, to se provjerava ispitivanjem
544
if (izvornik.peek() != EOF)
EOF je simboliko ime znaka koji oznaava kraj datoteke (engl. End-Of-File), definirano
u zaglavlju stdio.h.
Na kraju primjera uoimo pozive dva funkcijska lana koji provjeravaju stanje
tokova: eof() i bad(). Tim se ispitivanjem provjerava je li nastupila pogreka u
izlaznom toku prije nego to je dosegnut kraj ulaznog toka.
Zadatak. Napiite program koji kopira sadraj datoteke u drugu datoteku, znak po znak.
ifstream();
ifstream(const char *ime_datoteke, openmode mod = in);
ofstream();
ofstream(const char *ime_datoteke, openmode mod = out);
Verzije koje smo dosad koristili kao argument prihvaaju ime datoteke na koju se tok
vezuje. Koristi li se konstruktor bez parametara, tada se tok vee na datoteku naknadno,
pomou funkcijskog lana open(). Tako smo u primjeru sa stranice 543 mogli pisati:
ifstream izvornik;
izvornik.open(argv[1]);
ofstream ponornik;
ponornik.open(argv[2]);
545
Uspjeh otvaranja datoteke ispituje se tek nakon poziva funkcijskog lana open() na
potpuno identian nain kao i u primjeru na stranici 543.
Ako datoteke treba zatvoriti prije (implicitnog ili eksplicitnog) poziva destruktora,
to moemo uiniti funkcijskim lanom close():
izvornik.close();
ponornik.close();
To nam omoguava da tok veemo na neku drugu datoteku, iako se to vrlo rijetko
koristi.
Kao to se vidi iz gore navedenih deklaracija konstruktora, verzije konstruktora
klasa ifstream i ofstream koje prihvaaju ime datoteteke, prihvaaju i drugi
argument mod. On ima podrazumijevanu vrijednost za pojedini tip toka, tako da ga
nije neophodno uvijek navoditi. Za ifstream je podrazumijevani mod in, tj. itavanje
iz datoteke, a za ofstream podrazumijevani mod je out, tj. upis u datoteku. Potpuno
isto vrijedi i za funkcijske lanove open() ako se datoteka eli otvoriti u modu
razliitom od podrazumijevanog, uz ime datoteke moe se proslijediti mod.
Modovi za otvaranje datoteka definirani su kao bitovna maska u klasi ios (tablica
18.3). Vie razliitih modova moe se meusobno kombinirati koristei operator |
(bitovni ili). Na primjer, elimo li da se prilikom upisivanja u datoteku novi sadraj
mod znaenje
app upis se obavlja na kraju datoteke (append)
ate datoteka se otvara i skae se na njen kraj (at-end)
binary upis i uitavanje se provode u binarnom modu
in otvaranje za itanje
nocreate vraa se pogreka ako datoteka ne postoji
noreplace vraa se pogreka ako datoteka postoji
out datoteka se otvara za pisanje
trunc sadraj postojee datoteke se brie prilikom
otvaranja (truncate)
Suprotan efekt ima mod trunc koji kree datoteku prilikom njenog otvaranja.
Budui da je to podrazumijevano ponaanje prilikom otvaranja datoteke za upis, ne
treba ga posebno navoditi. Binarni mod e biti opisan u odjeljku 18.7.4.
Matoviti itatelj bi se mogao dosjetiti i pokuati promijeniti podrazumijevane
modove tokova ifsream i ofstream (in, odnosno out). I to je mogue; prevoditelj
nee prijaviti pogreku napiemo li sljedee naredbe:
Meutim, od toga nee biti neke vajde, jer su u klasi ofstream (odnosno u klasi
ostream koju ova nasljeuje) definirani samo funkcijski lanovi i operator << za upis u
datoteku, dok su u klasi ifstream (odnosno njenoj osnovnoj klasi istream) definirani
samo funkcijski lanovi i operator >> za uitavanje. Nije potrebno mudrovati:
jednostavno koristite ulazne tokove za ulazne operacije, a izlazne tokove za izlazne
operacije.
//...
float staroStanje;
fstream strujiStruja("MojaTuga.dat",
ios::in | ios::out | ios::nocreate);
if (!strujiStruja) {
cerr << "Ne postoji datoteka \"MojaTuga.dat\"" << endl
<< "Bit e stvorena nova." << endl;
strujiStruja.open("MojaTuga.dat", ios::out);
staroStanje = 0.;
}
else {
do {
strujiStruja >> staroStanje;
} while(strujiStruja);
}
cout << setiosflags(ios::fixed) << setprecision(2)
<< setw(15) << "staro stanje:"
<< setw(8) << staroStanje << endl;
strujiStruja.open("MojaTuga.dat", ios::out);
Ako datoteka postoji, ita se njen sadraj, sve do kraja datoteke. Zadnji podatak u
datoteci je posljednje upisano stanje. Kraj datoteke provjerava se preko pokazivaa koji
vraa strujiStruja sve dok je uitavanje u redu, taj je pokaziva razliit od nule.
Nailaskom na kraj datoteke postavlja se eofbit pa pokaziva postaje jednak nuli. Uvjet
za ponavljanje petlje uitavanja mogli smo napisati i kao:
548
do {
//...
} while(!strujiStruja.eof());
strujiStruja.clear();
prije upisa novog podatka. Da nema te naredbe, podatak se ne bi mogao upisati! Naime,
prilikom uitavanja, nailazak na kraj datoteke postavio je eofbit sve dok se to stanje
ne obrie, upis u datoteku se ne moe obaviti.
Ovako napisani program ima jedan oiti nedostatak: pri svakom pohranjivanju
novog stanja datoteka se poveava i u njoj su pohranjeni podaci koji korisniku gotovo
sigurno nee vie nikad trebati. Elegantnije rjeenje je da se stari podatak jednostavno
prepie novim podatkom, ali da bismo to mogli napraviti, moramo se nakon uitavanja
starog stanja vratiti na poetak datoteke i tamo upisati novo stanje. Kako to ostvariti bit
e prikazano u sljedeem odjeljku.
nazivima lanova (p, odnosno g) dolaze od naziva funkcijskih lanova put() i get() u
pripadajuim klasama.
Funkcijski lanovi seekp() i seekg() definirani su u po dvije preoptereene
inaice:
Prve dvije inaice funkcijskih lanova seekp(), odnosno seekg(), prihvaaju kao
argument apsolutnu poziciju na koju se valja postaviti unutar datoteke. Kao stvarni
argument najee se prenosi vrijednost dohvaena funkcijama tellp(), odnosno
tellg().
Druge dvije verzije kao prvi argument prihvaaju pomak u odnosu na poziciju
definiranu drugim argumentom. Tip streamoff je definiran u zaglavlju fstream.h i
slui za specifikaciju pomaka pokazivaa. seekdir je pobrojenje koje oznaava u
odnosu na koje mjesto se pomak rauna, te ima tri mogue vrijednosti:
beg pomak u odnosu na poetak datoteke,
cur pomak u odnosu na trenutanu poziciju u datoteci, te
end pomak u odnosu na kraj datoteke.
itatelju e znaenje gornjih naredbi biti zasigurno jasnije nakon sljedeeg primjera:
ifstream ulTok("slova.dat");
// pomie pokaziva datoteke na jedno mjesto prije kraja
ulTok.seekg(-1, ios::end);
// pohrani trenutni poloaj pokazivaa
pos_type pozic = ulTok.tellg();
char zn;
ulTok >> zn;
cout << zn;
// pomie pokaziva na apsolutni poetak datoteke
ulTok.seekg(0);
ulTok >> zn;
cout << zn;
// vraa pokaziva za jedno mjesto prema poetku
ulTok.seekg(-1, ios::cur);
ulTok >> zn;
cout << zn;
// vraa pokaziva na jedno mjesto ispred zapamenog poloaja
ulTok.seekg(pozic - 1);
ulTok >> zn;
cout << zn << endl;
550
Ako datoteka slova.dat sadri samo niz "abcdefgh", tada e izvoenje gornjih
naredbi na zaslonu ispisati:
haag
Zadatak. Napiite program koji kopira sadraj datoteke, znak po znak, od njena kraja
prema poetku, u novu datoteku.
biti smjeten na kraj datoteke, ali emo pronai mjesto u listi na koje se podatak dodaje,
te emo aurirati pokazivae. Na taj nain izbjegavamo uitavanje kompletne datoteke u
memoriju, njeno sortiranje i zapisivanje upis e biti bri. tovie, za veliku bazu bi
nam sigurno ponestalo memorije za uitavanje cijele baze, pa operaciju niti ne bismo
mogli obaviti.
Prvi podatak u datoteci bit e glava liste pokaziva na poziciju na koju je zapisan
prvi lan liste. Ako je lista prazna, pokaziva e sadravati nulu. Svaki se lan liste
sastoji od pokazivaa na poloaj sljedeeg lana te od samog sadraja lana. lan koji je
zadnji u listi ima pokaziva jednak nuli. U donjem kdu je rijeeno umetanje novog
lana; itatelju preputamo da rijei problem brisanja lana.
pos_type pozSljed = 0;
fstream bazaImena("imenabin.dat", ios::in | ios::out |
ios::nocreate | ios::binary);
if (!bazaImena) {
bazaImena.open("imenabin.dat", ios::in | ios::out |
ios::binary);
bazaImena.write((char *) &pozSljed, sizeof(pos_type));
}
char novoIme[duljImena];
cin.getline(novoIme, duljImena);
// ponavlja unos novih imena sve dok je duljina utipkanog
// imena razliita od nule
while (cin.gcount() > 1) {
pos_type pozPreth = 0;
pos_type pozTemp = 0;
// na poetak datoteke
bazaImena.seekg(0);
// uita poziciju sljedeeg
bazaImena.read((char *) &pozSljed, sizeof(pos_type));
// prolazi datoteku sve dok postoji sljedei
while (pozSljed) {
// pomakne na sljedei zapis
bazaImena.seekg(pozSljed);
char imeNaDisku[duljImena];
// uita pokaziva na sljedei zapis
bazaImena.read((char *) &pozTemp, sizeof(pos_type));
// uita ime u tekuem zapisu
bazaImena.get(imeNaDisku, duljImena, '\0');
if (strcmp(imeNaDisku, novoIme) >= 1)
break; // ako novo treba doi ispred, prekida
pozPreth = pozSljed;
pozSljed = pozTemp;
}
// prvo dopisuje novo ime na kraj datoteke
bazaImena.seekg(0, ios::end);
pozTemp = bazaImena.tellg();
552
Inicijalizacija toka je identina kao u primjeru sa strane 546, osim to dodatno navodimo
binarni mod. Ako datoteka ne postoji, tada se ona generira i upisuje se glava liste kao
nula. Zatim zapoinje petlja za unos novih podataka.
Podaci se unose u zasebnim recima pritiskom na tipku za unos izvodi se funkcija
getline(), koja oitava utipkani redak teksta; ako se tipka za unos pritisne na samom
poetku retka, petlja e se prekinuti. Zatim se provjerava je li neki podatak uope
unesen. To se moe obaviti pomou funkcije gcount() ona vraa broj uitanih
znakova u prethodnoj get(), getline() ili read() funkciji. Budui da getline()
uitava i znak za novi redak, u sluaju da nije utipkan nikakav tekst, funkcija gcount()
e vratiti broj 1 te je to signal da korisnik nije unio podatak.
Uoimo nain na koji se binarno pohranjuju i uitavaju pokazivai datoteke. Za
upis pokazivaa koriten je funkcijski lan write(). On kao prvi argument prihvaa
pokaziva char * koji pokazuje na niz bajtova koje elimo upisati, a drugi argument je
broj bajtova koje treba upisati. Budui da pokazivae elimo upisati binarno, bajt po
bajt, pokaziva operatorom dodjele tipa (char *) pretvaramo u niz bajtova i kao takav
ga zapisujemo. Za upis teksta takoer koristimo lan write(), jer on omoguava da
jednostavno zapiemo i zakljuni nul-znak.
Za usporedbu znakovnih nizova koritena je funkcija strcmp(), deklarirana u
zaglavlju string.h. Navedimo jo kako bi izgledao kd za ispis lanova liste:
bajtova kamo treba uitane podatke pohraniti, a drugi argument je broj bajtova koje
treba uitati. Tekstovni niz se uitava funkcijskim lanom get() kojemu je kao
graninik, kod kojeg prekida uitavanje, naveden nul-znak.
U svakom sluaju valja uoiti da kod binarno zapisanih podataka, prilikom
uitavanja treba unaprijed znati kakvog je tipa podatak koji se uitava. U binarnom
obliku svi podaci izgledaju jednako, pa program nee razlikovati uitava li se broj ili
znakovni niz.
Zadatak. Razmislite i pokuajte rijeiti gornji zadatak tako da pokazivae zapisujete u
tekstovnom obliku. Usporedite duljine datoteka sa deset jednakih nizova podataka za
obje izvedbe programa.
Zadatak. Napiite program koji pohranjuje i uitava polje cijelih brojeva u binarnom
obliku. Duljinu polja (takoer u binarnom obliku) pohranite kao prvi podatak.
delete sadrzaj;
Objektno orijentirano programiranje nije samo novi nain zapisivanja programa ono
iziskuje potpuno novu koncepciju razmiljanja. Stari proceduralni programi se, dodue,
mogu prepisati u objektnom jeziku, no time se ne iskoritavaju puni potencijal jezika.
C++ nije zamiljen zato da bi se stari programi bolje preveli, ili da bismo dobili bri
izvedbeni kd. Njegova osnovna namjena je pomoi programeru da formulira svoje
apstraktne ideje na nain razumljiv raunalu. Njegova uporabna vrijednost posebice
dolazi do izraaja pri razvoju novih, sloenih programa.
je napisati C++ program koji ne deklarira niti jedan objekt te dosljedno prati
proceduralni nain pisanja programa. No ipak, s pojavom novih operacijskih sustava
koji tee objektnom ustroju, objektno programiranje se jednostavno ne smije zanemariti.
Jezik C++ je jezik ope namjene, za poslove od sistemskog programiranja do
razvoja korisnikih suelja te isto apstraktnih primjena kao to su matematiki
prorauni i predstavlja najkoriteniji programski jezik za pisanje komercijalnih
aplikacija. To je mjesto stekao dobrim dijelom zahvaljujui svom prethodniku jeziku
C. Svi C programi se mogu prevesti C++ prevoditeljem potpuno ispravno, te se na ve
postojee projekte lako moe dograditi objektno orijentirani modul. I dok je ouvanje
kompatibilnosti s C-om sigurno jedna od velikih prednosti C++ jezika, to mu je
istodobno i jedan veliki kamen smutnje. Naime, mnogi C-ovski koncepti, kao to su
pokazivai, nizovi te dodjele tipova ugraeni su u C++ te dozvoljavaju zaobilaenje
objektnih svojstava. Tako se C++ nalazi izmeu klasinih proceduralnih jezika, kao to
su C i Pascal, i istih objektnih jezika, kao to je SmallTalk te objedinjava svojstva i
jednih i drugih. Takva dvostruka svojstva ponekad dodaju jeziku nepotrebnu
kompliciranost.
U prethodnim poglavljima iznesena su svojstva jezika s naznakama kako se ta
svojstva mogu iskoristiti u objektno orijentiranim programima. No do istinskog
poznavanja objektno orijentiranog principa programiranja potrebno je prevaliti jo dug
put. Naime, ako programer poznaje kljunu rije virtual, ne znai da e ju ujedno
znati i upotrijebiti na pravom mjestu na pravi nain, po principu Stavi pravi virtual
na pravo mjesto... (uini to esto). Dapae, ima dosta primjera nenamjerne
zloupotrebe slobode koju prua OOP. Rezultat je lo izvorni kd koji ima dosta
pogreaka, neitak je, sporo se izvodi te se dosta teko prilagoava promijenjenim
zahtjevima.
Zbog toga emo u ovom poglavlju pokuati prikazati nain na koji se elementi
jezika upoznati u prethodnim poglavljima mogu uinkovito iskoristiti. Ovo poglavlje
nee uvesti niti jednu novu kljunu rije niti proiriti steeni C++ vokabular. No
ispravna i dosljedna primjena principa programiranja je ak vanija od poznavanja
sintakse i semantike jezika.
Ovdje e se spomenuti samo osnovni principi modeliranja objekata na kraju, u
podruju kao to je C++ ne moe se oekivati da svo znanje bude kondenzirano na
jednom mjestu. Naposljetku, iskustvo je vrlo znaajna dimenzija objektnog
programiranja koja se stie iskljuivo radom. Neke stvari, ma koliko puta ih proitali,
shvatit ete tek nakon tri dana i noi provedenih analizirajui program koji bez pardona
rui sve teorije o pisanju programa bez pogreaka.
Iako biblioteka ima veze s rjeavanjem problema, ona je objekt koji e objedinjavati
sve preostale objekte. Nije potrebno uvoditi posebnu apstrakciju za biblioteku, jer e
sam izvedbeni kd dobiven nakon prevoenja predstavljati biblioteku.
Jezik nema direktne veze s rjeavanjem problema. On je samo sredstvo koje
koristimo za formulaciju rjeenja, a ne dio rjeenja.
Prozor, izbornik i ekran su objekti koji su direktno pogodni za prikazivanje pomou
klase. Na primjer, ekran je fiziki dio raunala koji omoguava prikaz podataka u
vidljivom obliku. Njegova svojstva, kao na primjer razluivost u horizontalnom i
vertikalnom smjeru, broj boja koji se odjednom moe prikazati, frekvencija
osvjeavanja i slino, mogu se vrlo efikasno upakirati u objekt klase Ekran koji e biti
apstrakcija stvarnog ekrana.
Prozor i izbornik su elementi korisnikog suelja koji su ve sami po sebi
apstrakcije. Oni ne postoje u stvarnom svijetu, ve su dio apstrakcije u komunikaciji
izmeu raunala i korisnika.
Iako se moda na prvi pogled ini da je raunalo objekt koji nema veze s rjeenjem
problema, paljivo razmatranje nam moe rei da to nije tako. Raunalo je objekt koji se
sastoji od mnogo razliitih cjelina: tipkovnice, memorije, mia, ekrana, diskova i slino.
Neki od njih slue za interakciju izmeu programa i korisnika, poput mia, tipkovnice i
ekrana. Na slian nain na koji emo u klasu Ekran upakirati svojstva ekrana u
raunalu, mogue je definirati i klase Mis i Tipkovnica koji e predstavljati apstraktni
model tih ureaja. Dakle, nije nam potrebno direktno modelirati raunalo, no modelirat
emo neke njegove dijelove. Modeliranje raunala koje objedinjuje sve druge objekte za
svaki dio raunala bio bi posao operacijskog sustava.
Primjenom gornjeg postupka dobili smo ve nekoliko klasa koje emo primijeniti u
rjeavanju problema. Time popis nije nipoto zakljuen daljnjom analizom dobivenih
objekata vidjet emo da emo morati uvesti nove klase za nove apstrakcije.
za prikaz slike treba monitor i grafika kartica, pod nazivom ekran podrazumijevat
emo cjelokupni podsustav raunala za prikaz slike.
informatike mogu biti ukljueni primjerice i dizajneri koji e odrediti vizualni identitet
izbornika. Zbog toga emo preskoiti daljnju analizu ove klase.
Postoji tehnika koju su razvili ameriki strunjaci K. Beck i W. Cunningham koja
pomae razvoju apstrakcije u velikim sloenim projektima. Ta tehnika koristi takozvane
CRC kartice (kratica od engl. class, responsibility, collaboration klasa, odgovornost,
suradnja). Ona se sastoji u tome da se za svaku klasu koja se pojavljuje u projektu
formira jedna kartonska kartica tono odreene veliine (kako se ne bi pretjerivalo s
glomaznim definicijama) na koju se napie naziv klase i njena glavna svojstva. Ljudi
koji rade na projektu se zatim okupe na sastanku te svaki dobije jednu ili vie kartica.
Oni zatim uzimaju ulogu pojedinih klasa te pokuavaju odigrati vie scenarija
interakcije izmeu objekata. Pri tome esto postaje jasno da pojedine apstrakcije nisu
adekvatne te ih se zatim mijenja u skladu sa zahtjevima. Sve te promjene se unose na
kartice kako bi se na kraju mogla izvui bit svake klase. Takoer, pojedini sudionici
mogu sugerirati da su neke apstrakcije previe komplicirane i zbunjujue te predloiti
nove. Male kartice prisiljavaju sudionike na jednostavne i razumljive apstrakcije.
Pojedine kartice mogu biti smjetene u grupe ime se oznaava pripadnost pojedinoj
funkcionalnoj cjelini.
Iako takav postupak moe ponekad izmaknuti kontroli i pretvoriti se u
poslijepodnevno ludilo isfrustriranih i premalo plaenih informatiara, pokazao se vrlo
korisnim u poetnom postupku razvoja apstrakcija nekog sloenog sustava. Ovakvim
neformalnim sastankom mnogi ljudi esto izraze svoje zamisli koje inae nikada ne bi
ispriali na slinom sastanku formalnog karaktera.
LCDEkran
Ekran
Relacija biti oznaava definiciju novog tipa na osnovi ve postojeeg tipa. Njena
najea implementacija u C++ jeziku je pomou javnog nasljeivanja. Prilikom
nasljeivanja objekt klase LCDEkran e sadravati podobjekt klase Ekran i ta se
injenica vrlo esto interpretira na krivi nain pomou relacije posjedovati.
Relacija posjedovati opisuje odnos kada neki objekt kao dio svog internog ustroja
posjeduje ili sadri neki drugi objekt. Na primjer, Ekran e sadravati objekt Paleta
koji e definirati listu boja koje se trenutno mogu prikazati na ekranu. Ekran nije podtip
klase Paleta (niti obrnuto), on jednostavno sadrava paletu. Paleta e biti
implementirana kao podatkovni lan objekta Ekran.
Vidjeli smo da svaki objekt izvedene klase sadrava podobjekt osnovne klase.
Neiskusan korisnik objektno orijentirane tehnologije bi mogao zbog toga zakljuiti da se
Ekran jednostavno moe implementirati tako da se naslijedi klasa Paleta. Iako takvo
rjeenje moe korektno raditi, ono je nekorektno sa stanovita dizajna sustava. Svrha
nasljeivanja je opisati podtipizaciju, a ne sadravanje objekata. Takvu manu u dizajnu
sustava moglo bi se pokuati ispraviti tako da se Ekran izvede zatieno ili privatno, no
niti to nije korektno. Takav program moe raditi ispravno, ali e sigurno biti
nerazumljiv. Programeri koji rade na razvoju takvog sustava bit e zasigurno zbunjeni
navedenim pristupom rjeavanju problema. Zbog toga e biti vrlo jednostavno prilikom
pisanja programa nainiti pogreku, te e dobiveni kd gotovo sigurno biti neispravan.
Takoer, ako se prilikom razvoja sustava ustanovi da dotina apstrakcija nije sasvim
prikladna, bit e teko provesti izmjenu. Pomicanje pojedine klase u hijerarhiji prema
gore ili dolje e takoer biti vrlo teko.
Relacija koristiti je najopenitija ona specificira da jedan objekt u svome radu samo
koristi neki drugi objekt. Pri tome ta dva objekta koji ulaze u interakciju nisu niti na
jedan drugi nain meusobno povezani (primjerice tako da je jedan objekt podatkovni
lan drugoga ili da postoje hijerarhijski odnosi izmeu njih). Ta dva objekta mogu
komunicirati putem njihovog javnog suelja, ili moda ak pomou privatnog suelja u
565
sluaju da je jedan deklariran kao prijatelj drugog. Ovakav odnos se u C++ jeziku
najee implementira pokazivaima i referencama izmeu objekata. U naem primjeru
moemo rei da e svaki Prozor koristiti Ekran za ispisivanje na njemu.
Osim tih osnovnih tipova odnosa izmeu objekata postoji niz podtipova tih odnosa.
Nakon identifikacije pojedinih odnosa moramo se odmah zatim zapitati je li taj odnos
jednosmjeran ili dvosmjeran. Jednosmjerni odnosi se lake implementiraju i zahtijevaju
manje izvornog kda za implementaciju. Zbog toga se oni i izvode znatno bre. No
dvosmjerni odnosi su mnogo fleksibilniji, pa je zato posao korisnika objekata znatno
pojednostavljen.
U naem sluaju odnos izmeu Ekrana i Prozora e gotovo sigurno biti
implementiran kao jednosmjeran Prozor e koristiti Ekran za ispis svojih podataka,
dok Ekran nee imati potrebe koristiti Prozor (barem na sadanjem stupnju razvoja
projekta toga nismo svjesni).
Gornji primjer odmah nas vodi na dodatna etiri podtipa odnosa: jedan na jedan,
jedan na vie, vie na jedan i vie na vie. U naem sluaju odnos izmeu prozora i
ekrana je vie na jedan: u nekom trenutku imat emo vie aktivnih prozora koji e
eljeti crtati na ekranu. Takoer, moemo se zapitati koliko je to vie: je li to neki fiksni
broj (na primjer dva) ili neki promjenjivi broj.
Vaan je i nain na koji se odnosi mijenjaju tijekom vremena. Pojedine veze vrijede
samo dok postoje povezani objekti. Na primjer, prozori se mogu u toku rada raunala
stvarati i unitavati. Veza prozora s ekranom postoji samo dok postoji prozor, kada
prozor biva uniten, veza takoer prestaje. Nadalje, veza prozora s ekranom nije
potrebna cijelo vrijeme; prozor pristupa ekranu dok eli obaviti nekakav ispis. Nakon
toga veza do sljedeeg ispisa nije potrebna. Svaki prozor moe prepustiti ekran za
vrijeme perioda dok ne obavlja ispis nekom drugom prozoru. Na taj nain se moe
odrediti protokol po kojem u svakom trenutku samo jedan prozor ima mogunost ispisa.
Da li e se takva implementacija primijeniti ovisi o nizu imbenika, kao to su fizika
svojstva stvarnog ekrana s kojim raspolaemo, operacijskim sustavom koji se koristi u
raunalu i slino. Radi jednostavnije implementacije neemo uvoditi kontrolu pristupa,
nego emo se zadrati na originalnom rjeenju kod kojeg vie prozora moe pristupati
ekranu odjednom.
MojProzor
Prozor
MojProzor
Gornje rjeenje je korektno, ali jo uvijek uvodi dodatne komplikacije u hijerarhiju. Kao
i uvijek prilikom viestrukog nasljeivanja, tri klase koje uvode elemente za kontrolu
prozora mogu definirati lan istog naziva. Zbog toga e za pristup pojedinim lanovima
klase MojProzor biti neophodno navesti osnovnu klasu u kojoj je lan definiran.
Osnovnu dizajnersku pogreku smo napravili ve pri izradi apstraktnog modela.
Prozor s elementom za zatvaranje ili za promjenu veliine nije sutinski razliit od bilo
kojeg drugog prozora. Element za zatvaranje se moe promatrati kao objekt koji prozor
posjeduje, ili jednostavno kao modifikator koji mijenja nain na koji prozor funkcionira.
U gornjem rjeenju smo relaciju posjedovati pokuali realizirati pomou relacije biti.
Prvo bi rjeenje bilo generirati dodatne klase izvan hijerarhije prozora. Zatim bi
klasa UokvireniProzor morala uvesti metode za manipulaciju pojedinih elemenata
(dodavanje, uklanjanje, promjenu poloaja i slino) te za komunikaciju izmeu njih.
Takvo rjeenje je znatno korektnije od simulacije posjedovanja pomou nasljeivanja.
569
class Boja {
private:
unsigned short udioR, udioG, udioB;
public:
unsigned short B) {
udioR = R; udioG = G; udioB = B;
}
};
Boja nizBoja[10];
nizBoja[0] = Boja(10, 250, 70);
Ovo moemo protumaiti kao da je Boja(10, 250, 70) konstanta koja se nalazi s
desne strane operatora pridruivanja, te se pridruuje nekom elementu niza nizBoja.
Valja uoiti da zbog implementacije klase nije potrebno uvoditi posebne operatore
pridruivanja ili konstruktore kopije.
Prilikom definicije suelja vrlo je vano ispravno odrediti atribute pojedinih
elemenata suelja. U C++ jeziku pod atributima se primarno misli na virtualnost,
statinost, konstantnost i javnost. Pozabavimo se malo pravilima kojima se odreuju ti
atributi.
Vrlo je vano ispravno odrediti je li neki funkcijski lan klase virtualan ili ne.
Pravilo od palca (engl. rule of the thumb) koje se nalazi u veini knjiga o objektnom
programiranju kae da sve funkcijske lanove, za koje se oekuje da e biti promijenjeni
prilikom nasljeivanja, treba obavezno uiniti virtualnima. Lijepo reeno, no ne ba
previe korisno! Naime, sada je odgovor prebaen na drugo pitanje: A koji e
funkcijski lanovi biti promijenjeni prilikom nasljeivanja?!
Odgovor na gornje pitanje moe se dobiti ako se shvati bit virtualnosti. Virtualan
funkcijski lan specificira akciju koja je vezana uz tip podataka na kojemu se obavlja.
Ako se akcija za razliite tipove obavlja na isti nain, tada ona nije vezana uz tip te ju
nije potrebno uiniti virtualnom. tovie, preporuka je da se virtualni funkcijski lanovi
koriste samo kada je to nuno.
boja, pa je veliinu palete potrebno mijenjati i u toku rada. Uvest emo stoga funkcijski
lan BrojBoja() koji e podesiti veliinu palete na eljeni broj boja. lan
DajBrojBoja() e omoguiti itanje broja boja u paleti. Pristup pojedinoj boji moe
se realizirati na razne naine. Jedan od vrlo prikladnih je adresiranje palete pomou
operatora [] time se naglaava itatelju programskog kda da je paleta neka vrsta
niza. Napisat emo kako bi izgledala deklaracija klase Paleta:
class Paleta {
public:
Paleta(int brBoja);
};
Razmotrimo koji bi tip bilo najprikladnije vratiti iz operatora []. Jedno od moguih
rjeenja koje se namee samo po sebi ve je opisano u poglavlju o preoptereenju
operatora kod definicije klase Matrica, a to je vratiti referencu na objekt Boja:
class Paleta {
// ...
Boja &operator [](int indeks);
// ...
};
Paleta p;
p[0] = Boja(127, 127, 127);
No takvo rjeenje ne mora uvijek odgovarati. Ono zapravo pretpostavlja tono odreenu
implementaciju koja ne mora uvijek biti ispravna. Naime, u gornjem sluaju
najjednostavnija implementacije bi bila kada bi klasa Paleta sadravala niz objekata
klase Boja. Operator [] jednostavno vraa referencu na odreeni element niza. Gornje
pridruivanje se tada provodi tako da se pozove operator = za klasu Boja. Klasa
Paleta vie nema nikakvog nadzora nad pridruivanjem svojim lanovima nakon
pridruivanja kontrola se ne vraa vie u klasu pa dodatna obrada podataka nije
mogua.
Meutim, u naem bi sluaju upravo to bilo potrebno: nakon dodjele odreenom
elementu palete potrebno je uskladiti sistemske registre na grafikoj kartici s RGB
575
vrijednostima dodane boje. tovie, paleta uope ne mora biti realizirana tako da ona
sadri niz objekata Boja boje u tekuoj paleti se nalaze zapisane u registrima grafike
kartice. Implementacija palete moe prilikom dohvaanja pojedine boje jednostavno
proitati boju iz registra te vratiti privremeni objekt klase Boja koji e sadravati
proitane vrijednosti. Klasa Paleta zapravo slui kao omota (engl. wrapper) oko
sistemskih poziva grafike kartice te omoguava koritenje jednostavnog objektnog
suelja umjesto sloenih sistemskih poziva.
Zbog svega gore navedenoga, operator [] e vraati privremeni objekt klase Boja.
Postavljanje boje e se obavljati pomou funkcijskog lana PostaviBoju() koji kao
parametre ima cijeli broj za identifikaciju mjesta u paleti na koje se boja postavlja, te
referencu na objekt klase Boja. Konana deklaracija klase Paleta izgledat e ovako:
class Paleta {
public:
Paleta(int brBoja);
};
Sljedei zadatak koji nas eka jest definiranje suelja klase Slika. Ta klasa mora
uvesti mehanizme kojima se moe crtati na izlaznoj jedinici raunala. Slika se sastoji iz
niza toaka poredanih okomito i vodoravno. Klasa Tocka opisuje svaku toku na slici.
Osnovno svojstvo toke je boja (tonije reeno, indeks boje u paleti), pa e ta klasa
sadravati funkcijske lanove za postavljanje i itanje boje. Takoer, kako je indeks u
biti cijeli broj, uvest emo i operator konverzije klase Tocka u cijeli broj, te konverziju
cijelog broja u objekt klase Tocka. Evo mogue deklaracije suelja:
class Tocka {
public:
int DajIndeks();
void PostaviIndeks(int ind);
};
Budui da nije za oekivati da e klasa Tocka ikad biti naslijeena, niti jedan
funkcijski lan nije virtualan.
576
class Slika {
friend class Ekran;
protected:
void PostaviPaletu(Paleta *pal);
public:
577
Slika(int pocetniNacin);
virtual ~Slika();
};
Gornja deklaracija je samo primjer koji pokazuje kako bi se deklaracija mogla obaviti
to nipoto nije potpuna deklaracija koja bi obuhvatila svu potrebnu funkcionalnost klase
Slika. Objasnit emo ukratko pojedine elemente suelja.
Konstruktor klase Slika ima jedan cjelobrojni parametar kojim se identificira
poetni nain prikaza. Neemo ulaziti dublje u problematiku to sainjava pojedini
nain prikaza te kako se oni numeriraju to ovisi o konkretnom raunalu na kojemu
radimo. Recimo samo da se pojedini nain prikaza identificira cijelim brojem kojim se
odreuje razluivost u x i y smjeru te broj raspoloivih boja.
Destruktor ima ulogu oslobaanja svih resursa koje je objekt zauzeo, a definiran je
virtualnim kako bi se omoguilo nasljeivanje klase. Tako sve izvedene klase mogu
definirati svoje destruktore, a virtualni mehanizam e omoguiti ispravno unitavanje
objekata.
U zatienom dijelu suelja naveden je lan PostaviPaletu(). Njegova je uloga
uspostavljanje veze izmeu palete i slike. lan je potrebno pozvati prije nego to se
Slika pone koristiti i kao parametar mu proslijediti pokaziva na objekt klase
Paleta. Klasa Slika e se time konfigurirati tako da e koristiti proslijeeni objekt za
identifikaciju pojedinih boja. Klasa Ekran je uinjena prijateljem klase Slika primarno
zato da bi se omoguio pristup tom zatienom funkcijskom lanu.
Slijedi niz lanova: DajXRazlucivost(), DajYRazlucivost() te
DajBrojBoja() koji omoguavaju itanje podataka o trenutno postavljenom nainu
prikaza. I oni su uinjeni virtualnima kako bi se osiguralo da se za svaku posebnu
grafiku karticu ispravno odredi traeni podatak. Funkcijski lan
PostaviNacinPrikaza() omoguava naknadnu promjenu naina prikaza tako da mu
se kao parametar proslijedi identifikator naina prikaza.
Slijede lanovi za crtanje. lan CrtajTocku() e nacrtati toku na zadanom
mjestu na ekranu tako da mu se kao parametar proslijede koordinate toke i sam objekt
klase Tocka koji e identificirati toku. lan CitajTocku() radi upravo obrnuto: uz
zadane koordinate toke on e vratiti objekt klase Tocka koji e identificirati toku na
578
zadanom mjestu. lan Linija iscrtava liniju tako da mu se zadaju koordinate poetne i
zavrne toke i indeks boje iz palete. lan Kvadrat() e nacrtati kvadrat tako da mu se
zadaju koordinate gornjeg lijevog i donjeg desnog kuta te indeks boje iz palete.
Klasa Ekran slui objedinjavanju rada slike i palete. Ona e sadravati konstruktor
u kojemu e se stvoriti po jedan objekt klase Slika i jedan klase Paleta. Javno suelje
klase e sadravati samo dva funkcijska lana pomou kojih e se moi dobiti referenca
na objekt Slika i na objekt Paleta, tako da se moe pristupati njihovim funkcijskim
lanovima. Evo deklaracije:
class Ekran {
public:
Ekran();
virtual ~Ekran();
Slika &DajSliku();
Paleta &DajPaletu();
};
Pomone klase Mis i Tipkovnica takoer slue tome da se sklop raunala uini
apstraktnim te prikae pomou objekta. Klasa Mis mora sadravati funkcijske lanove
za inicijalizaciju i itanje poloaja mia. Klasa Tipkovnica e sadravati funkcijske
lanove za itanje kdova tipki koje su pritisnute. Kao i prilikom razvoja klase Slika,
moemo ustanoviti da postoje razne vrste mieva i tipkovnica, pa emo funkcijske
lanove definirati virtualnima, a u izvedenim klasama navesti kd za rad s konkretnim
sklopom. Tono i precizno formiranje suelja tih klasa previe bi zadiralo u podruje
sistemskog programiranja, pa emo njihove deklaracije izostaviti i zadovoljiti se
gornjim opisom.
Na slian nain bi se definirala suelja i preostalih klasa Prozor,
UokvireniProzor, Izbornik i Prometnik. Definirati suelje tih klasa ne bi bilo vrlo
jednostavno iz razloga to uope nismo dovoljno precizno definirali svojstva tih klasa.
Koje su sve operacije dozvoljene na prozoru? Kako on odgovara na pojedinu operaciju?
Kako izgleda prozor na ekranu? Kakav operacijski sustav se koristi na naem raunalu?
Sve su to stvari o kojima treba voditi rauna prilikom izrade biblioteke grafikog
suelja. Zbog toga emo ovdje zavriti s definicijom suelja, a daljnju razradu problema
prepustiti nadobudnim itateljima.
Prilog
581
571
A. Standardna biblioteka
ANSI standard jezika C++ prvenstveno opisuje sintaksu naredbi jezika. Meutim, zbog
to bolje prenosivosti kda, ANSI komitet za standardizaciju je odluio u standard
ukljuiti i definicije elemenata standardne biblioteke. Osnovna namjena standardne
biblioteke jest pruiti efikasne i pouzdane predloke, klase i funkcije te time oslobodi
pisca kda od napornog pisanja trivijalnih struktura podataka i algoritama. Standardna
C++ biblioteka sadri definicije:
makro funkcija i makro imena,
simbolikih konstanti (vrijednosti),
tipova,
predloaka,
klasa,
funkcija,
objekata.
Deklaracije i definicije elemenata standardne C++ biblioteke raspodijeljene su kroz
trideset dvije datoteke zaglavlja, navedene u tablici A.1.
571
572
Postoji znaajna razlika u odnosu na standard C jezika. Naime, svi nazivi C datoteka
zaglavlja imaju prefiks c, kako bi se naznailo da dotina datoteka pripada C standardu.
Na primjer, sadraj zaglavlja cmath odgovara sadraju C zaglavlja math.h. Takoer,
standard C++ jezika dozvoljava i ukljuivanje starih datoteka zaglavlja koje imaju
nastavak .h. Razlika je u tome to su u datotekama koje poinju sa c svi identifikatori
smjeteni u imenik std, ime su uklonjeni iz globalnog podruja imena. Radi
kompatibilnosti preporuuje se koritenje novih datoteka. Krasno, jo samo da su nam
dostupne...
Prema namjeni pojedinih komponenti, standardna biblioteka podijeljena je u deset
kategorija:
podrka C++ jeziku (language support), koja ukljuuje komponente za dinamiko
rukovanje memorijom (zaglavlje new), dinamiku identifikaciju tipa (typeinfo) te
rukovanje iznimkama (exception). U ovu kategoriju ukljuene su i komponente iz
pet C zaglavlja: cstdarg, csetjmp, csignal, ctime i cstdlib.
dijagnostika, koja obuhvaa komponente za otkrivanje i prijavu pogreaka,
definirane u zaglavlju stdexcept. Te komponente obuhvaaju iznimke koje se
bacaju u sluaju pogreke. Ona takoer ukljuuje standardne C komponente iz
zaglavlja cassert i cerrno.
ope pomone komponente (general utilities), koje obuhvaaju komponente i
funkcije iz jezgre standardnih klasa zadanih predlocima (standard template library,
STL), komponente za dinamiko rukovanje memorijom, te funkcije za rukovanje
vremenom i datumom. Ove komponente su opisane u zaglavljima utility i
memory, te standardnim C zaglavljima cstring, cstdlib i ctime.
nizovi. U ovu kategoriju ukljuene su komponente za rukovanje nizovima znakova
tipa char, wchar_t ili nekog drugog tipa, deklarirane u zaglavlju string. Osnovu
ini klasa basic_string definirana predlokom, iz kojeg su instancirane klase
string i wstring. Dostupne su i komponente iz C zaglavlja cctype, cwctype,
cstring, cwchar, te viebajtne konverzije iz cstdlib.
573
koji svakom lanu dodjeljuje jedinstveni klju (jedan lan je vezan s kljuem koji je
jedinstven na nivou kontejnera) i osigurava brzo dohvaanje kljua. multiset
dozvoljava jednake kljueve (vie lanova mogu imati isti klju). Nasuprot tome, map i
multimap osiguravaju dohvaanje podataka nekog drugog tipa prema zadanom kljuu.
Klasa basic_string, deklarirana u zaglavlju string, namijenjena je za
rukovanje znakovnim nizovima. Za nju su definirani i neki vaniji operatori, poput +,
!=, ==, << i >>, to moe znaajno pojednostavniti operacije sa znakovnim nizovima.
I na kraju, spomenimo jo i klasu complex (deklariranu u istoimenom zaglavlju
complex), koja pojednostavnjuje raun sa kompleksnim brojevima. Za nju su takoer
definirani svi aritmetiki operatori i operatori za ispis na tok (!=, *, *=, +, +=, -, -=, /,
/=, <<, ==, >>) i funkcije (npr. abs(), acos(), exp(), log()) koji su dozvoljeni s
kompleksnim brojevima.
Osim klasa definiranih predlocima, standardna biblioteka sadri i strukture
definirane predlocima (tablica A.7), standardne klase (tablica A.8) i standardne
strukture (tablica A.9). Standardne klase (npr. bad_alloc, bad_cast,
domain_error) uglavnom su namijenjene za hvatanje iznimki.
579
B. Standardne funkcije
Standardna C++ biblioteka prua 208 standardnih funkcija iz C biblioteke (tablica B.1).
Veina matematikih funkcija je preoptereena za objekte klase complex. Takoer,
C++ standard definira posebne tipove kojima se podrava vrlo brza paralelna obrada
numerikih podataka na jakim strojevima. Za tu svrhu uveden je predloak valarray
koji definira polje matematikih vrijednosti. Parametar predloka je tip vrijednosti, pa
emo tako polje realnih brojeva dobiti kao valarray<float>, a polje kompleksnih
brojeva sa valarray<complex>. Za pojedine funkcije su u matematikim
bibliotekama definirani i predloci funkcija koje barataju poljem valarray. Tako
postoji poseban predloak, primjerice, funkcije sqrt(), koji kao parametar uzima
valarray te rauna korijen iz pojedine vrijednosti. Definirani su i predloci operatora
koji omoguavaju raunanje s poljima vrijednosti istog tipa. Na taj nain C++ standard
omoguava da se za pojedinu raunalnu platformu optimizira raunanje s poljem
podataka na najprikladniji nain (pojedine arhitekture procesora posjeduju zaseban set
instrukcija za obradu polja numerikih podataka), tako da se specijalizira pojedini
predloak za odreeni tip.
Slijedi kratak opis vanijih funkcija, grupiranih prema operacijama koje izvode.
Budui da je veina funkcija preuzeta iz standardne C biblioteke, pri navoenju naredbe
za ukljuivanje, osim imena zaglavlja C++ biblioteka, navedena su i imena zaglavlja iz
C biblioteke. Njih se moe prepoznati po nastavku .h. Ovo je praktino ako su
korisniku na raspolaganju starije biblioteke. Iz opisa su izuzete standardne C funkcije za
ulazno-izlazne tokove, budui da tokovi iz iostream biblioteke (opisane u poglavlju
16) podravaju sve operacije koje su na raspolaganju C programerima u stdio.h
biblioteci. tovie, iostream prua mnotvo dodatnih pogodnosti pa vjerujemo da,
kada se jednom saivi sa iostream bibliotekom, programeru uope nee nedostajati C-
funkcije printf(), scanf() i sline. Isto tako, iskusni C++ programer e vrlo rijetko
posegnuti za C funkcijama za alokaciju i oslobaanje dinamiki alocirane memorije
(malloc(), calloc(), free()), budui da jezik C++ ima za to ugraene operatore
new i delete.
#include <locale>
template<class charT> bool isalnum(charT znak, const locale &lc) const;
template<class charT> bool isalpha(charT znak, const locale &lc) const;
template<class charT> bool iscntrl(charT znak, const locale &lc) const;
template<class charT> bool isdigit(charT znak, const locale &lc) const;
template<class charT> bool isgraph(charT znak, const locale &lc) const;
template<class charT> bool islower(charT znak, const locale &lc) const;
template<class charT> bool isprcharT(charT znak, const locale &lc) const;
template<class charT> bool ispunct(charT znak, const locale &lc) const;
template<class charT> bool isspace(charT znak, const locale &lc) const;
template<class charT> bool isupper(charT znak, const locale &lc) const;
template<class charT> bool isxdigit(charT znak, const locale &lc) const;
Ove funkcije slue za klasifikaciju znaka. Argument znak je kd znaka kojeg elimo
klasificirati. Funkcije vraaju true ako je zadovoljen uvjet ispitivanja; u protivnom je
582 Principi objektno orijentiranog dizajna
poziva funkcije treba alocirati dovoljan prostor za preslikani niz, jer funkcija ne
provjerava je li za preslikani niz na mjestu odrediste odvojeno dovoljno mjesta.
Funkcija strncpy() preslikava do najvie maxDuljina znakova niza izvornik
na mjesto odrediste. Treba uoiti da, ako je maxDuljina manja ili jednaka duljini
niza izvornik, nul-znak nee biti preslikan pa niz odrediste moe ostati
nezakljuen.
ispisat e se broj 3, jer se tek znak 'a' u prvom nizu ne pojavljuje u drugom nizu, a
nalazi se na poziciji 3.
Funkcija strcspn() radi upravo suprotno: ona e vratiti indeks prvog znaka niza
niz koji je sadran u nizu podNiz. Na primjer, donja naredba e ispisati 2, jer je znak
'n' prvi znak iz drugog niza koji se pojavljuje u prvome te se nalazi na poziciji 2:
I
cvri
586 Principi objektno orijentiranog dizajna
cvri
cvrak
#include <locale>
template <class charT> tolower(charT znak, const locale &loc) const;
template <class charT> toupper(charT znak, const locale &loc) const;
#include <cctype> // stari naziv: <ctype.h>
int tolower(int znak);
int toupper(int znak);
Ako je znak veliko slovo, funkcija tolower() ga pretvara u njegovo malo slovo
('A'...'' u 'a'...''). Funkcija toupper() ini obrnuto: ako je znak malo slovo,
pretvara ga u njegovo veliko slovo. Ostale znakove funkcije ostavljaju
nepromijenjenima. Povratna vrijednost je kd (eventualno) pretvorenog znaka. Funkcije
iz standardne C++ biblioteke deklarirane su predlocima, gdje charT moe biti bilo koji
tip koji se moe svesti na char ili wchar_t. Drugi argument jest referenca na objekt
klase locale koja specificira mjesne (zemljopisne) parametre, kao to su klasifikacija
znakova, formati zapisa brojeva, datuma i vremena i sl. Istoimene funkcije iz standardne
C biblioteke su tipa int.
Funkcija strtod() slina je funkciji atof(), ali potonja bolje prepoznaje pogreke
prilikom pretvorbe.
'0' (za sluaj da niz sadri oktalni prikaz broja) ili 'x', odnosno 'X' (za sluaj da niz
sadri heksadekadski prikaz broja). Baza moe biti cijeli broj izmeu ukljuivo 2 do
ukljuivo 36. Ako je pokazKraja prilikom ulaska u funkciju razliit od nul-
pokazivaa, tada ga funkcija usmjerava na znak koji je prekinuo slijed uitavanja. Slijed
uitavanja se prekida im se naie na neki znak koji nije neki od gore spomenutih
znakova ili ne moe biti znamenka u pripadajuoj bazi. Na primjer, za heksadekadske
brojeve (baza 16) dozvoljeni znakovi su svi dekadski brojevi '0'...'9' te slova
'a'...'f', odnosno 'A'...'F'. Tako e u sljedeem primjeru biti uitane samo prve tri
znamenke, jer u oktalnom prikazu znamenka 8 nije dozvoljena:
Uoimo da funkcija na kraj niza, neposredno ispred zakljunog nul-znaka dodaje znak
za novi redak '\n', tako da za ispis nije koriten manipulator endl.
Budui da je niz koji vraa funkcija asctime(), odnosno ctime() statika
varijabla, on e biti prepisan pri svakom novom pozivu funkcija asctime(), odnosno
ctime() ako ga elimo sauvati, valja ga preslikati u neki drugi znakovni niz.
struct tm {
int tm_sec; // sekundi nakon pune minute (0...59)
int tm_min; // minuta nakon punog sata (0...59)
int tm_hour; // sati nakon ponoi (0...23)
int tm_mday; // dan u mjesecu (1...31)
int tm_mon; // mjesec (0...11)
int tm_year; // godina poevi od 1900.
int tm_wday; // dana od nedjelje (0...6)
int tm_yday; // dana od poetka godine (0...365)
int tm_isdst; // zastavica za ljetni pomak vremena
};
Ako je tm_isdst pozitivan, tada je aktiviran ljetni pomak vremena, ako je nula tada je
ljetni pomak vremena neaktivan, a ako je negativan tada informacija nije dostupna.
Valja paziti da je struktura koju stvaraju gmtime(), odnosno localtime() statiki
alocirana, te se prepisuje pri svakom novom pozivu funkcije. Stoga, ako elimo sauvati
oitano vrijeme, moramo strukturu preslikati u neki drugi objekt.
Sljedee naredbe e ispisati dananji datum i tono vrijeme:
time_t ura;
tm *cajt;
ura = time(NULL);
cajt = localtime(&ura);
cout << "Danas je "
<< cajt->tm_mday << "."
<< (cajt->tm_mon + 1) << "."
<< (1900 + cajt->tm_year) << "." << endl;
cout << "Tono je "
<< cajt->tm_hour << ":"
<< cajt->tm_min << " sati" << endl;
typedef struct {
int quot; // kvocijent
int rem; // ostatak
} div_t;
odnosno long:
typedef struct {
long quot; // kvocijent
long rem; // ostatak
} ldiv_t;
double x = 5.0;
double y = 2.2;
cout << fmod(x, y) << endl;
ispisat e:
4 = 0.5 * 2 na 3
Korak 6: Implementacija 595
ispisati na zaslonu:
94.3 = 0.3 + 94
596 Principi objektno orijentiranog dizajna
#include <stdlib.h>
#include <iostream.h>
int main() {
for (int i = 0; i < 10; i++)
cout << rand() << endl;
return 0;
}
uvijek na nekom stroju ispisati potpuno isti niz brojeva (zato se govori o
pseudosluajnim brojevima). Da bi se prilikom svakog pokretanja programa dobili
razliiti nizovi sluajnih brojeva, generator sluajnih brojeva treba inicijalizirati nekim
brojem koji e se mijenjati od izvoenja do izvoenja programa. Najzgodnije je koristiti
sistemsko vrijeme, koje se mijenja dovoljno brzo da korisnik ne moe kontrolirati
njegovu vrijednost.
Funkcija srand() slui za inicijalizaciju generatora sluajnih brojeva. Argument je
klica pomou koje se generira prvi broj u nizu sluajnih brojeva. Ako gornji primjer
modificiramo, dodajui mu izmeu ostalog poziv funkcije srand() ispred for-petlje:
Korak 6: Implementacija 597
#include <stdlib.h>
#include <iostream.h>
#include <time.h>
int main() {
time_t cajt;
srand((unsigned) time(&cajt));
for (int i = 0; i < 10; i++)
cout << (rand() % 6 + 1) << endl;
return 0;
}
void izlaz1() {
cerr << "Izlaz br. 1" << endl;
}
void izlaz2() {
cerr << "Izlaz br. 2" << endl;
}
int main() {
atexit(izlaz1);
atexit(izlaz2);
return 0;
}
na zaslonu ispisati:
Izlaz br. 2
Izlaz br. 1
Korak 6: Implementacija 599
Ona mora biti definirana tako da vraa sljedee vrijednosti tipa int:
< 0 ako je arg1 manji od (tj. mora u slijedu biti prije) arg2,
0 ako su arg1 i arg2 meusobno jednaki,
> 0 ako je arg1 vei od (tj. mora u slijedu biti iza) arg2
Evo i primjera:
#include <stdlib.h>
#include <iostream.h>
int main() {
int podaci[] = {123, 234, 345, 456};
int trazeni = 345;
int brelem = sizeof(podaci) / sizeof(podaci[0]);
int *pok = (int *)bsearch (&trazeni, podaci, brelem,
sizeof(podaci[0]), (fpok)usporedba);
if (pok)
cout << "U polju ve postoji taj podatak na mjestu "
<< (pok - podaci) << endl;
else
cout << "Ovakvog podatka jo nije vidjelo ovo polje!"
<< endl;
return 0;
}
600 Principi objektno orijentiranog dizajna
struct lconv {
char *decimal_point; // znak za razdvajanje cijelih i
// decimalnih mjesta u brojevima
char *thousands_sep; // znak za razdvajanje grupa
// znamenki po tisuu
char *grouping; // veliina svake grupe znamenki
char *int_curr_symbol; // meunarodna oznaka valute
char *currency_symbol; // lokalna oznaka valute
char *mon_decimal_point; // znak za razdvajanje cijelih i
// dec. mjesta u novanim iznosima
char *mon_thousands_sep; // znak za razdvajanje grupa
// znamenki u novanim iznosima
char *mon_grouping; // veliina svake grupe
char *positive_sign; // oznaka za pozitivne nov.iznose
char *negative_sign; // oznaka za negativne nov.iznose
char int_frac_digits; // broj decimalnih znamenki u me-
// unarodnom prikazu nov.iznosa
char frac_digits; // broj decimalnih znamenki u
Korak 6: Implementacija 601
Ona mora biti definirana tako da vraa sljedee vrijednosti tipa int:
<0 ako je arg1 manji od (tj. mora u slijedu biti prije) arg2,
0 ako su arg1 i arg2 meusobno jednaki,
> 0 ako je arg1 vei od (tj. mora u slijedu biti iza) arg2
Funkcija qsort() primjenjuje quick sort algoritam koji se smatra najbrim za sortiranje
openitih podataka. Sljedei program e sortirati slova u nizu podaci obrnutim
abecednim slijedom:
int main() {
char podaci[] = "adiorwgoerg";
Korak 6: Implementacija 603
return 0;
}
Uoimo kako je ovdje poziv funkcije usporedba() drugaije rijeen nego kod
primjera s funkcijom bsearch(): unutar same funkcije usporedba() pokazivaima
na void se dodjeljuje tip char * te se provodi usporedba takvih tipova.
return;
}
int main() {
int a = 10;
int b = 0;
if (signal(SIGFPE, hvataljka) == SIG_ERR)
cout << "Hvatanje pogreke nije postavljeno - "
<< "nastavak programa na vlastitu odgovornost."
<< endl;
if (b == 0)
raise(SIGFPE);
else
a = a / b;
return 0;
}
U jeziku C++ funkcije ove funkcije se rijetko koriste, jer se rukovanje izvanrednim
situacijama elegantnije rjeava hvatenjem iznimki. Takoer, signali su ostaci UNIX
biblioteke. U modernim, suvremenim i naprednim sustavima signali imaju bolju i
kvalitetniju implementaciju.
#include <exception>
terminate_handler set_terminate(terminate_handler funkcija);
void terminate();
Funkcija terminate() vana je za obradu iznimaka. Ona se automatski poziva u
sljedeim sluajevima:
ako neka baena iznimka nije uhvaena te je izletila iz funkcije main(),
ako mehanizam za rukovanje iznimkama utvrdi da je stog poremeen,
kada destruktor pozvan tijekom odmatanja stoga izazvanog iznimkom pokua izai
pomou iznimke.
Korak 6: Implementacija 605
system("blabla");
#include <exception>
unexpected_handler set_unexpected(unexpected _handler funkcija);
void unexpected();
Funkcija unexpected() vana je za obradu iznimaka. Ona se automatski poziva ako
iznimka izleti iz neke funkcije, a da pri tome nije navedena u listi moguih iznimaka.
Ako nije drugaije specificirano, funkcija unexpected() e pozvati funkciju
terminate(), a ova e prekinuti izvoenje programa pozivom funkcije abort().
Ako ne elimo prekid programa na ovaj nain, moemo sami zadati funkciji pomou
funkcije set_unexpected(). Prilikom poziva funkcije set_unexpected(), ona kao
rezultat vraa pokaziva na prethodno instaliranu funkciju.
606 Principi objektno orijentiranog dizajna
Hrvatsko-engleski rjenik
anonimna unija anonymous union
apstrakcija podataka data abstraction
apstraktna klasa abstract class
argument argument
asembler assembler
automatska smjetajna klasa automatic storage class
automatski objekt automatic object
bacanje iznimke throwing an exception
bajt byte
bezimena unija nameless union
bezimeni imenik nameless namespace
biblioteka library
blok pokuaja try block
cijeli broj integer
cjelobrojna promocija integral promotion
isti virtualni funkcijski lan pure virtual function member
datoteni imenik directory
datoteno podruje file scope
datoteka file
datoteka zaglavlja header file
definicija funkcije function definition
deklaracija unaprijed forward declaration
destruktor destructor
dinamiki objekt dynamic object
dinamiki poziv dynamic call
dinamiko povezivanje dynamic binding
diskriminanta unije union discriminant
djelomina specijalizacija partial specialization
dodjela tipa type cast
dominacija dominance
duboka kopija deep copy
enkapsulacija encapsulation
formalni argument formal argument
funkcijski lan member functions
globalno podruje global scope
hrpa heap
hvatati catch
identifikacija tipova tijekom izvoenja run-time type identification
Korak 6: Implementacija 607
imenik namespace
implementacija objekta implementation
instanciranje predloka template instantiation
integrirana razvojne okoline integrated development environment,
IDE
iskljuivi ili exclusive or
isprazniti flush
izbornik menu
izlazni tok output stream
iznimka exception
izvedbeni program executable
izvedena klasa derived class
izvorni kd source code
javni public
javna osnovna klasa public base class
javno suelje public interface
kasno povezivanje late binding
klasa class
konstantnost constness
konstruktor constructor
konstruktor kopije copy constructor
kontejnerska klasa container class
kurzor cursor
lvrijednost lvalue
makro funkcija macro function
makro ime macro name
manipulator manipulator
meupohranjivanje buffering
meuspremnik buffer
memorijska napuklina memory leak
metoda methods
mjesto instantacije point of instantiation
najdalje izvedena klasa most derived class
nasljeivanje inheritance
neimenovani privremeni objekt unnamed temporary
nevezano prijateljstvo unbound template friendship
nul-pokaziva null-pointer
nul-znak null-character
objektni kd object code
objektno orijentirano programiranje object oriented programming
obnavljajue pridruivanje update assignment
odbaciti konstantnost cast away constness
odmatanje stoga stack unwinding
omota wrapper
operator izluivanja extraction operator
608 Principi objektno orijentiranog dizajna
Englesko-hrvatski rjenik
abstract class apstraktna klasa
actual argument stvarni argument
anonymous union anonimna unija
application programming interface programersko suelje
argument argument
array polje podataka
assembler asembler
automatic object automatski objekt
automatic storage class automatska smjetajna klasa
base class osnovna klasa
bit-fields polje bitova
bound template friendship vezano prijateljstvo
buffer meuspremnik
buffering meupohranjivanje
bug pogreka
byte bajt
cast away constness odbaciti konstantnost
catch hvatati
central processing unit, CPU sredinja procesorska jedinica
character string znakovni niz
class klasa
class member pointer pokaziva na lan klase
class template predloak klase
code bloat prekomjerno bujanje kda
code reusability ponovnu iskoristivost kda
compile-time error pogreka pri prevoenju
compiler prevoditelj
constness konstantnost
constructor konstruktor
container class kontejnerska klasa
copy constructor konstruktor kopije
cursor kurzor
dangling pointer visei pokaziva
dangling reference visea referenca
data abstraction apstrakcija podataka
data hiding skrivanje podataka
data segment podatkovni segment
debugger program za lociranje pogreaka
deep copy duboka kopija
default argument value podrazumijevana vrijednost
argumenta
default constructor podrazumijevani konstruktor
612 Principi objektno orijentiranog dizajna
pointer pokaziva
polymorphism polimorfizam
private privatni
private base class privatna osnovna klasa
profiler program za praenje efikasnosti kda
protected zatien
protected base class zatiena osnovna klasa
public javni
public base class javna osnovna klasa
public interface javno suelje
pure virtual function member isti virtualni funkcijski lan
raising an exception podizanje iznimke
recursion rekurzija
register registar
register storage class registarska smjetajna klasa
relational operator relacijski operator
return value povratna vrijednost
rule of the thumb pravilo od palca
run-time error pogreka pri izvoenju
run-time type identification identifikacija tipova tijekom izvoenja
scope podruje
scope resolution razluivanje podruja
scope resolution operator operator za odreivanje podruja
shallow copy plitka kopija
shift left pomak ulijevo
shift right pomak udesno
side-effect popratna pojava
smart pointer pametni pokaziva
source code izvorni kd
stack stog
stack pointer pokaziva stoga
stack unwinding odmatanje stoga
standard template library, STL standardna biblioteka predloaka
state stanje
static binding statiko povezivanje
static call statiki poziv
static object statiki objekt
storage class smjetajna klasa
stream tok
structure struktura
template predloak
template instantiation instanciranje predloka
template specialization specijalizacija predloka
text editor program za ureivanje teksta
throwing an exception bacanje iznimke
Korak 6: Implementacija 615
true tono
try block blok pokuaja
type tip
type cast dodjela tipa
type-checking rules pravila provjere tipa
unbound template friendship nevezano prijateljstvo
union unija
union discriminant diskriminanta unije
unnamed temporary neimenovani privremeni objekt
upcast pretvorba navie
update assignment obnavljajue pridruivanje
virtual base class virtualna osnovna klasa
virtual call virtualni poziv
virtual function member virtualni funkcijski lan
void prazan
volatile promjenjiv
wrapper omota
616 Principi objektno orijentiranog dizajna
Abecedno kazalo
hvatanje iznimke, 454
/, 46
# /* */ (komentar), 29
!, 57 // (komentar), 29
!=, 59 /=, 69
" (#include), 475 :, 95, 297
" (znakovni niz), 26, 60, 139 ::, 186, 223, 259, 262, 270, 342, 435, 436,
"C", 509 437
"C++", 511 i virtualni lan, 379
#, 480 :>, 70
##, 480 ;, 25, 219
%, 46 <, 59
%:, 70 <%, 70
%:%:, 70 <:, 70
%=, 69 << (izlazni operator), 26, 517
%>, 70 preoptereenje, 519
&, 63, 114 razlikovanje << i >>, 28
&&, 57 << (pomak ulijevo), 65
&=, 69 <<=, 69
', 60 <=, 59
(), 76, 152, 154, 310 <> (#include), 475
preoptereenje, 317 <> (predloak), 211, 392, 398, 409, 412
* (mnoenje), 46 =, 39, 69, 310
* (pokaziva), 114 i konstruktor konverzije, 370
*=, 69 nasljeivanje, 369
+, 46 preoptereenje, 313
++, 45, 125 =0, 380
preoptereenje, 321 ==, 59
+=, 69 >, 59
,, 74, 88 >=, 59
-, 46 >> (pomak udesno), 65
--, 45, 125 >> (ulazni operator), 27, 521
preoptereenje, 321 preoptereenje, 523
-=, 69 razlikovanje << i >>, 28
->, 221, 310 >>=, 69
preoptereenje, 318 ?:, 83
->*, 274, 278 ??!, 71
., 221 ??', 71
.*, 274, 278 ??(, 71
... (funkcije) ??), 71
argumenti funkcije, 176 ??-, 71
... (iznimke) ??/, 71
Korak 6: Implementacija 617
??<, 71 argument
??=, 71 formalni, 154
??>, 71 klasa kao, 363
[], 104, 310 konstantni, 172
preoptereenje, 315 konstruktora, 238, 240
\ (nastavak reda), 31, 474 konverzija, 163
\ (posebna, escape sekvenca), 60 neodreeni, 176
podrazumijevani, 173, 192, 201
\", 60
pokaziva kao, 164
\', 60
polje kao, 168
\0, 60, 140 predloka funkcije, 211, 392
\?, 60 predloka funkcije, konstantni izraz, 396
\\, 60 predloka klase, 409
^, 64 predloka klase, konstantni izraz, 420
^=, 69 preoptereene funkcije, 191
{}, 77, 153 prijenos po referenci, 165
|, 64 prijenos po vrijednosti, 162, 181
|=, 69 privremeni objekt za prijenos po
||, 57 vrijednosti, 284
~, 62, 246 redoslijed izraunavanja, 163
referenca kao, 165
stvarni, 154
A tono podudaranje, 364
znakovni niz kao, 169
\a, 60
argv, 202
abort(), 574, 598
array. Vidi polje
abs(), 591 ASCII, 61
abstract class. Vidi apstraktna klasa asctime(), 588
acos(), 591 asembler, 10, 511
adjustfield, 533 ukljuivanje u C++ kd, 511
algorithm, 573 asin(), 592
and, 70 asm, 511
and_eq, 70 assert(), 236, 248, 483, 574
anonymous unions. Vidi unija, anonimna assert.h, 236, 483
ANSI C++, 18 atan(), 205, 592
API, 558
atan2(), 204, 592
app, 545
ate, 545
application programming interface. Vidi
atexit(), 598
programersko suelje (API)
apstrakcija atof(), 203, 586
definicija, 560 atoi(), 587
definicija javnog suelja, 571 atol(), 587
implementacija, 578 auto, 182
implementacijski zavisna, 569 automatic storage class. Vidi smjetajna
ocjenjivanje, 559 klasa, automatska
odnosi i veze, 563 automatski objekti, 128
pronalaenje, 559
apstrakcija podataka, 555 B
apstraktna klasa, 330, 380
argc, 202 \b, 60
618 Principi objektno orijentiranog dizajna
bad(), 516 C
bad_cast, 469
badbit, 516 .c, 22, 495
base class. Vidi osnovna klasa case, 84
basefield, 533 cassert, 572
basic_string, 572, 578 cast. Vidi dodjela tipa
before(), 466 catch, 448, 450
beg, 549 catching an exception. Vidi iznimka,
bezimeni imenik, 437 hvatanje
biblioteka, 22 cctype, 572
funkcija, 204, 580. Vidi standardna ceil(), 592
funkcija cerr, 516
standardna, 571 cerrno, 572
binary, 545 char, 44, 60
bitand, 70 cijeli broj, 40
bit-fields. Vidi polje bitova dijeljenje, 48
bitor, 70 cin, 27, 515, 521
bitovni operator class, 211, 218, 392, 409. Vidi klasa
i, 62, 63 class member pointer. Vidi pokaziva, na
ili, 62, 64 lan klase
iskljuivi ili, 62, 64 class template. Vidi predloak klase
komplement, 62 clear(), 516, 548
pomak udesno, 65 clocale, 573
pomak ulijevo, 65 clock(), 589
bits, 573 clock_t, 588, 589
blok CLOCKS_PER_SEC, 589
hvatanja, 448, 452, 453 clog, 516
hvatanja, odreivanje odgovarajueg
close(), 545
bloka, 453
cmath, 573
pokuaja, 448, 452
blok naredbi, 38, 77 code reusability. Vidi ponovna iskoristivost
bool, 57 compiler. Vidi prevoditelj
compl, 70
boolalpha, 533
complex, 573, 578
bound template friendship. Vidi vezano
prijateljstvo complex.h, 205
break, 84, 93 const, 53, 136, 172, 413. Vidi simbolika
broj konstanta
cijeli, 40 argument funkcije, 172
dekadski, 41 const_cast, 471
heksadekadski, 41 funkcijski lan, 253
oktalni, 41 odbacivanje konstantnosti, 139
preljev, 46 podatkovni lan, 244
realni, 40 pokaziva, 137
s pominom decimalnom tokom. Vidi pokaziva na, 137
broj, realni pokaziva na const, 138
brojana konstanta, 35, 51 razlika const i #define, 477
buffer. Vidi meuspremnik reference na, 145
buffering. Vidi meupohranjivanje const_cast, 471
constructor. Vidi konstruktor
container class. Vidi kontejnerska klasa
Korak 6: Implementacija 619
<<=, 69 logiki, 57
<=, 59 new, 128
=, 39, 69 new, nasljeivanje, 371
= i konstruktor konverzije, 370 new, preoptereenje, 323
=, nasljeivanje, 369 new [], 129, 134, 250
=, preoptereenje, 313 new [], preoptereenje, 323
==, 59 not, 70
>, 59 not_eq, 70
>=, 59 obnavljajueg pridruivanja, 69, 125
>>, 27 operator, 308
>>, 28 or, 70
>> (pomak udesno), 65 or_eq, 70
>> (ulazni operator), 521 poredbeni, 58, 125
>>=, 69 postfiks, 45
?:, 83 poziv operatorske funkcije, 310
[], 104 prefiks, 45
[], preoptereenje, 315 preoptereenje, 299, 307
^, 64 preoptereenje i nasljeivanje, 368
^=, 69 pridruivanja, 39
razlika = i ==, 59, 83
|, 64
|=, 69 razlikovanje << i >>, 28
razluivanje operatora konverzije, 304
||, 57
redoslijed izvoenja operatora, 74
~, 62
reinterpret_cast, 473
alternativne oznake, 35, 70
sizeof, 43, 73
and, 70
static_cast, 471
and_eq, 70
typeid, 465
aritmetiki, 45
binarni, 45, 308 umetanja, 517
unarni, 45, 308
bitand, 70
uvjetni, 83
bitor, 70
xor, 70
bitovni, 62
xor_eq, 70
compl, 70
za odreivanje podruja, 186, 270, 342,
const_cast, 471
379
definicija preoptereenog operatora, 309 za pristup lanu, 220
dekrement, 45 za razluivanje imena, 223
delete, 129, 247, 381 za razluivanje podruja, 435, 436, 437
delete, nasljeivanje, 371 operator overloading. Vidi operator,
delete, preoptereenje, 323 preoptereenje
delete [], 129, 250 oporavak od iznimke, 448
delete [], preoptereenje, 324 or, 70
dodjele tipa, 50, 139, 400 or_eq, 70
dozvoljeni operatori za preoptereenje, osnovna klasa, 332, 335
307 deinicijalizacija, 356
dynamic_cast, 468 dominacija, 387
hijerarhija, 74 inicijalizacija, 355
inkrement, 45 javna, 335, 345, 365
izluivanja, 521 konverzija pokazivaa, 358
konverzije, 302
630 Principi objektno orijentiranog dizajna
R S
\r, 60 scientific, 533, 535
raise(), 603 scope. Vidi podruje
raising an exception. Vidi iznimka, scope resolution operator. Vidi operator, za
podizanje odreivanje podruja
rand(), 93, 187, 575, 596 seekdir, 549
RAND_MAX, 93, 575, 596 seekg(), 529, 548
randomize(), 93 seekp(), 521, 548
razlika klase i objekta, 215 set, 573, 577
rdstate(), 516 set_new_handler(), 604
read(), 526, 552 set_terminate(), 605
realni broj, 40. Vidi tip, double set_unexpected(), 605
redoslijed izvoenja operatora, 74 setbase(), 538
referenca, 144 setf(), 532
deklaracija, 144 setfill(), 538
inicijalizacija, 145 setiosflags(), 538
kao povratna vrijednost funkcije, 179 setlocale(), 601
na konstantan objekt, 145 setprecision(), 538
na pokaziva, 168
setw(), 90, 538
slinost s pokazivaem, 145
shallow copy. Vidi plitka kopija
standardna konverzija, 347, 348, 350
short, 44
trivijalna konverzija, 364
virtualni poziv, 378 short int, 44
visea, 411 showbase, 533
za hakere, 145 showpoint, 533, 535, 536
registarska smjetajna klasa. Vidi showpos, 533
smjetajna klasa, registarska side-effects. Vidi popratne pojave
register, 182 SIG_DFL, 603
reinterpret_cast, 473 SIG_ERR, 603
rekurzija, 195 SIG_IGN, 603
relacija SIGABRT, 603
biti, 354, 563 SIGFPE, 603
jedan na jedan, 565 SIGILL, 603
jedan na vie, 565 SIGINT, 603
korisiti, 564 signal(), 603
posjedovati, 564 SIGSEGV, 603
sadri, 354 SIGTERM, 603
vie na jedan, 565
simbolika konstanta, 52, 489. Vidi const
vie na vie, 565
inicijalizacija, 54
resetiosflag(), 538
sin(), 199, 597
return, 153, 158
sinh(), 597
reusability. Vidi ponovna iskoristivost
size_t, 74, 323, 371
right, 533
sizeof, 43, 73
rukovanje iznimkama, 448
run-time type identification. Vidi skipws, 533
identifikacija tipa skrivanje podataka, 12, 557, 571
skupljanje smea, 11, 320
smart pointer. Vidi pametni pokaziva
smjetajna klasa, 181
Korak 6: Implementacija 635
F, 51 pobrojani, 55
l, 51 pokaziva. Vidi pokaziva
L, 51 pokaziva na lan klase, 271
u, 51 pokaziva na funkciju, 196
U, 51 polje, 102
switch, 84 pravila konverzije, 40, 47
pravila provjere tipa, 40
system(), 605
razlika izmeu char i char *, 142
referenca. Vidi referenca
T short, 44
short int, 44
\t, 60
sinonim za pokazivaki tip, 148
tablica virtualnih lanova, 375
type_info, 465
tan(), 597
typedef, 71, 490
tanh(), 598
ugnjeen u predloku, 422
tellg(), 529, 548 ugnjeeni, i nasljeivanje, 361
tellp(), 521, 548 ugraeni, 40
template unsigned, 44
Vidi predloak funkcije. usporedba, 465
Vidi predloak klase. void, 159
template, 211, 392, 405, 409
wchar_t, 62, 515
terminate(), 453, 604
wchar_t *, 143
this, 224, 261, 277 znakovni, 60
throw, 449 znakovni niz, 60, 139
bez parametara, 454 tm, 590
lista moguih iznimaka, 458 tok
throwing an exception. Vidi iznimka, bad(), 516
bacanje
badbit, 516
tie(), 530
beg, 549
_ _TIME_ _, 478
binarno pisanje i itanje, 550
time(), 591
cerr, 516
time.h, 205, 588
cin, 27, 515, 521
time_t, 588
clear(), 516, 548
tip, 37
clog, 516
bool, 57
close(), 545
broj, 40
cout, 26, 516, 517
char, 44, 60
cur, 549
char *, 139
itanje i pisanje u datoteku, 541
cjelobrojna promocija, 47
end, 549
double, 43, 44
eof(), 516
float, 42, 44
eofbit, 516, 548
identifikacija. Vidi identifikacija tipa
int, 41, 44 fail(), 516
iznimke, 449 failbit, 516, 522
klasa, 215 fill(), 531
logiki, 57 flags(), 532
long, 41, 44 flush(), 520
long double, 43, 44 fstream, 515, 541, 546
long int, 41, 44 gcount(), 526, 552
638 Principi objektno orijentiranog dizajna