You are on page 1of 653

1

Boris Motik i Julijan ribar

Demistificirani

Dobro upoznajte protivnika


da biste njime ovladali

Zagreb, 1997.

Roditeljima i bratu
Boris
Roditeljima, supruzi, a naroito Dori i Mateju
Julijan

Sadraj
2. To C++ or not to C++?.......................................................... 10
2.1.
2.2.
2.3.
2.4.
2.5.
2.6.

Povijesni pregled razvoja programskih jezika ..................................................10


Osnovna svojstva jezika C++ ..........................................................................12
Usporedba s C-om ..........................................................................................14
Zato primjer iz knjige ne radi na mom raunalu? ...........................................16
Literatura .........................................................................................................16
Zahvale............................................................................................................18

3. Oluja kroz jezik C++.......................................................... 19


3.1.
3.2.
3.3.
3.4.
3.5.

to je program i kako ga napisati ....................................................................19


Moj prvi i drugi C++ program ...........................................................................23
Moj trei C++ program....................................................................................26
Komentari ........................................................................................................28
Rastavljanje naredbi ........................................................................................29

4. Osnovni tipovi podataka...................................................... 33


4.1.
4.2.
4.3.
4.4.

Identifikatori .....................................................................................................33
Varijable, objekti i tipovi ...................................................................................35
Operator pridruivanja .....................................................................................37
Tipovi podataka i operatori ..............................................................................38
4.4.1.
Brojevi......................................................................................................38
4.4.2.
Aritmetiki operatori.................................................................................43
4.4.3.
Operator dodjele tipa ...............................................................................48
4.4.4.
Dodjeljivanje tipa brojanim konstantama ...............................................49
4.4.5.
Simbolike konstante...............................................................................50
4.4.6.
Kvalifikator volatile.............................................................................52
4.4.7.
Pobrojenja ...............................................................................................53
4.4.8.
Logiki tipovi i operatori ...........................................................................55
4.4.9.
Poredbeni operatori .................................................................................56
4.4.10.
Znakovi ....................................................................................................58
4.4.11.
Bitovni operatori.......................................................................................60
4.4.12.
Operatori pridruivanja (2 ) ...................................................................67
4.4.13.
Alternativne oznake operatora.................................................................68
4.4.14.
Korisniki definirani tipovi i operatori .......................................................69
4.4.15.
Deklaracija typedef...............................................................................69
4.5.
Operator sizeof ............................................................................................71
4.6.
Operator razdvajanja .......................................................................................72
4.7.
Hijerarhija i redoslijed izvoenja operatora ......................................................72

5. Naredbe za kontrolu toka programa................................... 77


5.1.
5.2.
5.3.
5.4.
5.5.
5.6.
5.7.
5.8.
5.9.
5.10.
5.11.

Blokovi naredbi ................................................................................................77


Grananje toka naredbom if............................................................................79
Uvjetni operator ? : .........................................................................................83
Grananje toka naredbom switch ...................................................................84
Petlja for ........................................................................................................86
Naredba while ...............................................................................................90
Blok do-while ...............................................................................................92
Naredbe break i continue ...........................................................................93
Ostale naredbe za skok ...................................................................................94
O strukturiranju izvornog kda.....................................................................95
Kutak za budue C++ gurue ....................................................................97

6. Polja, pokazivai, reference .............................................. 101


6.1.
6.2.

6.3.
6.4.
6.5.
6.6.
6.7.

Polja podataka...............................................................................................102
6.1.1.
Jednodimenzionalna polja .....................................................................102
6.1.2.
Dvodimenzionalna polja.........................................................................108
Pokazivai .....................................................................................................114
6.2.1.
Nul-pokazivai .......................................................................................119
6.2.2.
Kamo sa zvijezdom (petokrakom) .........................................................120
6.2.3.
Tajna veza izmeu pokazivaa i polja ...................................................121
6.2.4.
Aritmetike operacije s pokazivaima....................................................124
6.2.5.
Dinamiko alociranje memorije operatorom new ...................................127
6.2.6.
Dinamika alokacija polja ......................................................................129
6.2.7.
Dinamika alokacija viedimenzionalnih polja .......................................133
6.2.8.
Pokazivai na pokazivae .....................................................................134
Nepromjenjivi pokazivai i pokazivai na nepromjenjive objekte...................136
Znakovni nizovi..............................................................................................139
6.4.1.
Polja znakovnih nizova ..........................................................................143
Reference ......................................................................................................144
6.5.1.
Reference za hakere .............................................................................145
Nevolje s pokazivaima .................................................................................147
Skraeno oznaavanje izvedenih tipova ......................................................148

7. Funkcije............................................................................... 150
7.1.
7.2.

to su i zato koristiti funkcije........................................................................150


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

7.4.8.
Funkcije s neodreenim argumentima...................................................176
Pokazivai i reference kao povratne vrijednosti.............................................179
ivot jednog objekta ......................................................................................181
7.6.1.
Lokalni objekti........................................................................................181
7.6.2.
Globalni objekti ......................................................................................182
7.6.3.
Statiki objekti u funkcijama ..................................................................186
7.7.
Umetnute funkcije..........................................................................................187
7.8.
Preoptereenje funkcija................................................................................189
7.9.
Rekurzija........................................................................................................195
7.10.
Pokazivai na funkcije ...............................................................................196
7.11.
Funkcija main() .......................................................................................202
7.12.
Standardne funkcije...................................................................................204
7.12.1.
Funkcije za rukovanje znakovnim nizovima...........................................206
7.12.2.
Funkcija exit() ...................................................................................210
7.13.
Predloci funkcija.......................................................................................211
7.14.
Pogled na funkcije ispod haube ..............................................................212
7.5.
7.6.

8. Klase i objekti ..................................................................... 215


8.1.
8.2.

Kako prepoznati klase? .................................................................................215


Deklaracija klase ...........................................................................................218
8.2.1.
Podatkovni lanovi.................................................................................219
8.2.2.
Dohvaanje lanova objekta ................................................................220
8.2.3.
Funkcijski lanovi ..................................................................................221
8.2.4.
Kljuna rije this .................................................................................224
8.2.5.
Umetnuti funkcijski lanovi ....................................................................225
8.2.6.
Dodjela prava pristupa...........................................................................226
8.2.7.
Formiranje javnog suelja koritenjem prava pristupa...........................228
8.2.8.
Prijatelji klase.........................................................................................230
8.3.
Deklaracija objekata klase .............................................................................232
8.4.
Stvaranje i unitavanje objekata ....................................................................233
8.4.1.
Konstruktor ............................................................................................233
8.4.2.
Podrazumijevani konstruktor .................................................................239
8.4.3.
Poziv konstruktora prilikom definiranja objekata....................................240
8.4.4.
Konstruktor kopije ..................................................................................241
8.4.5.
Inicijalizacija referenci i konstantnih lanova .........................................243
8.4.6.
Konstruktori i prava pristupa ..................................................................245
8.4.7.
Destruktor ..............................................................................................246
8.4.8.
Globalni i statiki objekti ........................................................................248
8.5.
Polja objekata ................................................................................................249
8.6.
Konstantni funkcijski lanovi..........................................................................253
8.7.
Funkcijski lanovi deklarirani kao volatile ................................................257
8.8.
Statiki lanovi klase .....................................................................................257
8.8.1.
Statiki podatkovni lanovi ....................................................................257
8.8.2.
Statiki funkcijski lanovi .......................................................................260
8.9.
Podruje klase...............................................................................................262
8.9.1.
Razluivanje podruja ...........................................................................264
8.10.
Ugnijeene klase .....................................................................................265
8.11.
Lokalne klase.............................................................................................270

8.12.
Pokazivai na lanove klase .....................................................................271
8.12.1.
Pokazivai na podatkovne lanove .......................................................272
8.12.2.
Pokazivai na funkcijske lanove ..........................................................277
8.13.
Privremeni objekti ......................................................................................280
8.13.1.
Eksplicitno stvoreni privremeni objekti ...................................................281
8.13.2.
Privremeni objekti kod prijenosa parametara u funkciju.........................284
8.13.3.
Privremeni objekti kod vraanja vrijednosti ..........................................288

9. Strukture i unije.................................................................. 292


9.1.
9.2.
9.3.

Struktura ili klasa? .........................................................................................292


Unije ..............................................................................................................293
Polja bitova ....................................................................................................297

10.

Preoptereenje operatora ............................................. 299

10.1.
10.1.1.
10.1.2.
10.1.3.
10.2.
10.3.
10.3.1.
10.3.2.
10.3.3.
10.3.4.
10.3.5.
10.3.6.
10.4.

11.

Korisniki definirane konverzije .................................................................299


Konverzija konstruktorom ......................................................................300
Eksplicitni konstruktori ...........................................................................302
Operatori konverzije...............................................................................302
Osnove preoptereenja operatora............................................................307
Definicija operatorske funkcije ...................................................................309
Operator = .............................................................................................313
Operator [] ...........................................................................................315
Operator () ...........................................................................................317
Operator -> ...........................................................................................318
Prefiks i postfiks operatori ++ i -- .........................................................321
Operatori new i delete.........................................................................323
Ope napomene o preoptereenju operatora .........................................328

Nasljeivanje i hijerarhija klasa..................................... 330

11.1.
11.2.
11.3.
11.4.
11.4.1.
11.4.2.
11.4.3.
11.4.4.
11.4.5.
11.4.6.
11.5.
11.6.
11.7.
11.8.
11.8.1.
11.8.2.
11.9.
11.9.1.

Ima li klasa bogatog strica u Ameriki? .......................................................330


Specificiranje nasljeivanja........................................................................335
Pristup naslijeenim lanovima .................................................................341
Nasljeivanje i prava pristupa....................................................................343
Zatieno pravo pristupa ......................................................................344
Javne osnovne klase .............................................................................345
Privatne osnovne klase..........................................................................347
Zatiene osnovne klase......................................................................349
Posebne napomene o pravima pristupa ................................................350
Iskljuivanje lanova..............................................................................353
Nasljeivanje i pripadnost..........................................................................354
Inicijalizacija i unitavanje izvedenih klasa ................................................355
Standardne pretvorbe i nasljeivanje ........................................................357
Podruje klase i nasljeivanje ...................................................................360
Razlika nasljeivanja i preoptereenja .................................................361
Ugnijeeni tipovi i nasljeivanje ...........................................................362
Klase kao argumenti funkcija.....................................................................363
Tono podudaranje tipova .....................................................................364

11.9.2.
Standardne konverzije ...........................................................................364
11.9.3.
Korisniki definirane pretvorbe ..............................................................366
11.10.
Nasljeivanje preoptereenih operatora...................................................368
11.11.
Principi polimorfizma .................................................................................371
11.11.1.
Virtualni funkcijski lanovi..................................................................374
11.11.2.
Poziv virtualnih funkcijskih lanova....................................................378
11.11.3.
iste virtualne funkcije .......................................................................380
11.11.4.
Virtualni destruktori ............................................................................380
11.12.
Virtualne osnovne klase.............................................................................382
11.12.1.
Deklaracija virtualnih osnovnih klasa .................................................384
11.12.2.
Pristup lanovima virtualnih osnovnih klasa ......................................385
11.12.3.
Inicijalizacija osnovnih virtualnih klasa...............................................387

12.

Predloci funkcija i klasa ............................................... 390

12.1.
Uporabna vrijednost predloaka ................................................................390
12.2.
Predloci funkcija.......................................................................................391
12.2.1.
Definicija predloka funkcije ..................................................................392
12.2.2.
Parametri predloka funkcije .................................................................394
12.2.3.
Instanciranje predloka funkcije.............................................................397
12.2.4.
Eksplicitna instantacija predloka ..........................................................402
12.2.5.
Preoptereivanje predloaka funkcija...................................................403
12.2.6.
Specijalizacije predloaka funkcija ........................................................405
12.2.7.
Primjer predloka funkcije za bubble sort ..............................................406
12.3.
Predloci klasa ..........................................................................................408
12.3.1.
Definicija predloka klase ......................................................................409
12.3.2.
Instanciranje predloaka klasa ..............................................................412
12.3.3.
Eksplicitna instantacija predloaka klasa...............................................416
12.3.4.
Specijalizacije predloaka klasa ............................................................416
12.3.5.
Predloci klasa sa statikim lanovima .................................................419
12.3.6.
Konstantni izrazi kao parametri predloaka ...........................................420
12.3.7.
Predloci i ugnijeeni tipovi..................................................................422
12.3.8.
Ugnijeeni predloci.............................................................................423
12.3.9.
Predloci i prijatelji klasa........................................................................426
12.3.10.
Predloci i nasljeivanje ....................................................................427
12.4.
Mjesto instantacije .....................................................................................429
12.5.
Realizacija klase Lista predlokom.........................................................431

13.

Imenici ............................................................................. 434

13.1.
Problem podruja imena............................................................................434
13.2.
Deklaracija imenika ...................................................................................435
13.3.
Pristup elementima imenika.......................................................................437
13.3.1.
Deklaracija using .................................................................................439
13.3.2.
Direktiva using.....................................................................................443

14.
14.1.
14.2.
14.3.

Rukovanje iznimkama .................................................... 446


to su iznimke? .........................................................................................446
Blokovi pokuaja i hvatanja iznimaka ........................................................448
Tijek obrade iznimaka................................................................................451

14.4.
14.5.
14.6.

15.

Detaljnije o kljunoj rijei catch ...............................................................453


Navoenje liste moguih iznimaka ...........................................................458
Obrada pogreaka konstruktora ................................................................460

Identifikacija tipa tijekom izvoenja.............................. 463

15.1.
15.2.
15.3.
15.4.
15.4.1.
15.4.2.

16.

Pretprocesorske naredbe .............................................. 474

16.1.
16.2.
16.3.
16.3.1.
16.3.2.
16.3.3.
16.3.4.
16.4.
16.4.1.
16.5.
16.6.

17.

U poetku bijae pretprocesor...................................................................474


Naredba #include...................................................................................475
Naredba #define.....................................................................................476
Trajanje definicije...................................................................................477
Rezervirana makro imena......................................................................478
Makro funkcije .......................................................................................479
Operatori za rukovanje nizovima ...........................................................480
Uvjetno prevoenje....................................................................................481
Primjena uvjetnog prevoenja za pronalaenje pogreaka ...................483
Ostale pretprocesorske naredbe ...............................................................484
Ma a e meni pretprocesor?...................................................................485

Organizacija kda u sloenim programima.................. 486

17.1.
17.2.
17.2.1.
17.3.
17.4.
17.5.
17.6.
17.6.1.
17.6.2.

18.

Statiki i dinamiki tipovi............................................................................463


Operator typeid ......................................................................................465
Sigurna pretvorba ......................................................................................467
Ostali operatori konverzije .........................................................................471
Promjena konstantnosti objekta.............................................................471
Statike dodjele .....................................................................................471

Zato u vie datoteka? ..............................................................................486


Povezivanje ...............................................................................................487
Specifikatori static i extern..............................................................491
Datoteke zaglavlja .....................................................................................494
Organizacija predloaka ............................................................................497
Primjer raspodjele deklaracija i definicija u vie datoteka ..........................501
Povezivanje s kdom drugih programskih jezika .......................................508
Poziv C funkcija iz C++ kda .................................................................509
Ukljuivanje asemblerskog kda ...........................................................511

Ulazni i izlazni tokovi...................................................... 513

18.1.
18.2.
18.3.
18.4.
18.4.1.
18.4.2.
18.4.3.
18.5.

to su tokovi ..............................................................................................513
Biblioteka iostream................................................................................515
Stanje toka.................................................................................................516
Ispis pomou cout ..................................................................................517
Operator umetanja << ...........................................................................517
Ispis korisniki definiranih tipova ...........................................................519
Ostali lanovi klase ostream................................................................520
Uitavanje pomou cin ...........................................................................521

18.5.1.
18.5.2.
18.5.3.
18.5.4.
18.6.
18.6.1.
18.6.2.
18.6.3.
18.6.4.
18.6.5.
18.6.6.
18.7.
18.7.1.
18.7.2.
18.7.3.
18.7.4.
18.7.5.
18.8.
18.9.

19.

Uitavanje pomou operatora >> .........................................................521


Uitavanje korisniki definiranih tipova..................................................523
Uitavanje znakovnih nizova .................................................................524
Ostali lanovi klase istream................................................................526
Kontrola uitavanja i ispisa ........................................................................529
Vezivanje tokova ...................................................................................529
irina ispisa ...........................................................................................530
Popunjavanje praznina ..........................................................................531
Zastavice za formatiranje.......................................................................532
Formatirani prikaz realnih brojeva .........................................................535
Manipulatori ...........................................................................................537
Datoteni ispis i uitavanje ........................................................................541
Klase ifstream i ofstream ...............................................................541
Otvaranje i zatvaranje datoteke .............................................................544
Klasa fstream......................................................................................546
Odreivanje i postavljanje poloaja unutar datoteke .............................548
Binarni zapis i uitavanje .......................................................................550
Tokovi vezani na znakovne nizove ............................................................553
Ulijeva li se svaki tok u more? ...................................................................554

Principi objektno orijentiranog dizajna......................... 555

19.1.
19.2.
19.3.
19.4.
19.5.
19.5.1.
19.5.2.
19.5.3.
19.6.
19.6.1.
19.7.
19.8.
19.9.

Zato uope C++?....................................................................................555


Objektna paradigma ..................................................................................556
Ponovna iskoristivost.................................................................................558
Korak 1: Pronalaenje odgovarajue apstrakcije .....................................559
Korak 2: Definicija apstrakcije....................................................................560
Definicija ekrana ....................................................................................560
Definicija prozora ...................................................................................562
Definicija izbornika.................................................................................562
Korak 3: Definicija odnosa i veza izmeu klasa.........................................563
Odnosi objekata u korisnikom suelju .................................................565
Korak 4: Definicija implementacijski zavisnih apstrakcija ..........................569
Korak 5: Definicija suelja .........................................................................571
Korak 6: Implementacija ............................................................................578

10

2. To C++ or not to C++?


Vie nego bilo kada u povijesti, ovjeanstvo
se nalazi na razmei. Jedan put vodi u oaj i
krajnje beznae, a drugi u potpuno
istrebljenje. Pomolimo se da emo imati
mudrosti izabrati ispravno.
Woody Allen, Side Effects (1980)

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++.

2.1. Povijesni pregled razvoja programskih jezika


Prva raunala koja su se pojavila bila su vrlo sloena za koritenje. Njih su koristili
iskljuivo strunjaci koji su bili osposobljeni za komunikaciju s raunalom. Ta
komunikacija se sastojala od dva osnovna koraka: davanje uputa raunalu i itanje
rezultata obrade. I dok se itanje rezultata vrlo brzo uinilo koliko-toliko snoljivim
uvoenjem pisaa na kojima su se rezultati ispisivali, unoenje uputa programiranje
se sastojalo od mukotrpnog unosa niza nula i jedinica. Ti nizovi su davali raunalu
upute kao to su: zbroji dva broja, premjesti podatak s neke memorijske lokacije na
drugu, skoi na neku instrukciju izvan normalnog slijeda instrukcija i slino. Kako je
takve programe bilo vrlo sloeno pisati, a jo sloenije itati i ispravljati, ubrzo su se
pojavili prvi programerski alati nazvani asembleri (engl. assemblers).
U asemblerskom jeziku svaka strojna instrukcija predstavljena je mnemonikom koji
je razumljiv ljudima koji itaju program. Tako se zbrajanje najee obavlja
mnemonikom ADD, dok se premjetanje podataka obavlja mnemonikom MOV. Time se
postigla bolja itljivost programa, no i dalje je bilo vrlo sloeno pisati programe i
ispravljati ih jer je bilo potrebno davati sve, pa i najmanje upute raunalu za svaku
pojedinu operaciju. Javlja se problem koji e kasnije, nakon niza godina, dovesti i do

11

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.

2.2. Osnovna svojstva jezika C++


etiri su vana svojstva jezika C++ koja ga ine objektno orijentiranim: enkapsulacija
(engl. encapsulation), skrivanje podataka (engl. data hiding), nasljeivanje (engl.
inheritance) i polimorfizam (engl. polymorphism). Sva ta tri svojstva doprinose
ostvarenju takozvane objektno orijentirane paradigme programiranja.
Da bismo to bolje razumjeli, pogledajmo koji su se programski modeli koristili u
prolosti. Pri tome je svakako najvaniji model proceduralno strukturiranog
programiranja.
Proceduralno programiranje se zasniva na promatranju programa kao niza
jednostavnih programskih odsjeaka: procedura. Svaka procedura je konstruirana tako
da obavlja jedan manji zadatak, a cijeli se program sastoji od niza procedura koje
meusobno sudjeluju u rjeavanju zadatka.
Kako bi koristi od ovakve podjele programa bile to izraenije, smatrano je dobrom
programerskom taktikom odvojiti proceduru od podataka koje ona obrauje: time je bilo
mogue pozvati proceduru za razliite ulazne podatke i na taj nain iskoristiti je na vie
mjesta. Strukturirano programiranje je samo dodatak na proceduralni model: ono
definira niz osnovnih jezinih konstrukcija, kao petlje, grananja i pozive procedura koje
unose red u programe i ine samo programiranje daleko jednostavnijim.
Princip kojim bismo mogli obiljeiti proceduralno strukturirani model jest podijelipa-vladaj: cjelokupni program je presloen da bi ga se moglo razumjeti pa se zbog toga
on rastavlja na niz manjih zadataka procedura koje su dovoljno jednostavne da bi se
mogle izraziti pomou naredbi programskog jezika. Pri tome, pojedina procedura
takoer ne mora biti rijeena monolitno: ona moe svoj posao obaviti kombinirajui rad
niza drugih procedura.
Ilustrirat emo to primjerom: zamislimo da elimo izraditi kompleksan program za
obradu trodimenzionalnih objekata. Kao jednu od mogunosti koje moramo ponuditi
korisnicima jest rotacija objekata oko neke toke u prostoru. Koristei proceduralno
programiranje, taj zadatak bi se mogao rijeiti ovako:
1. Listaj sve objekte redom.
2. Za pojedini objekt odredi njegov tip.
3. Ovisno o tipu, pozovi ispravnu proceduru koja e izraunati novu poziciju objekta.
4. U skladu s tipom podataka auriraj koordinate objekta.
Operacije odreivanja tipa, izraunavanje nove pozicije objekta i auriranje koordinata
se mogu dalje predstaviti pomou procedura koje sadravaju niz jednostavnijih akcija.
Ovakav programski pristup je bio vrlo uspjean do kasnih osamdesetih, kada su
njegovi nedostaci postajali sve oitiji. Naime, odvajanje podataka i procedura ini
programski kd teim za itanje i razumijevanje. Prirodnije je o podacima razmiljati
preko operacija koje moemo obaviti nad njima u gornjem primjeru to znai da o

13

kocki ne razmiljamo pomou koordinata njenih kutova ve pomou moguih operacija,


kao to je rotacija kocke.
Nadalje, pokazalo se sloenim istodobno razmiljati o problemu i odmah
strukturirati rjeenje. Umjesto rjeavanja problema, programeri su mnogo vremena
provodili pronalazei naine da programe usklade sa zadanom strukturom.
Takoer, dananji programi se pokreu pomou mia, prozora, izbornika i dijaloga.
Programiranje je pogonjeno dogaajima (engl. event-driven) za razliku od starog,
sekvencijalnog naina. Proceduralni programi su korisniku u pojedinom trenutku
prikazivali niz ekrana nudei mu pojedine opcije u odreenom redoslijedu. No
pogonjeno dogaajima znai da se program ne odvija po unaprijed odreenom slijedu,
ve se programom upravlja pomou niza dogaaja. Dogaaja ima raznih: pomicanje
mia, pritisak na tipku, izbor stavke iz izbornika i slino. Sada su sve opcije dostupne
istodobno, a program postaje interaktivan, to znai da promptno odgovara na
korisnikove zahtjeve i odmah (ovo ipak treba uvjetno shvatiti) prikazuje rezultat svoje
akcije na zaslonu raunala.
Kako bi se takvi zahtjevi jednostavnije proveli u praksi, razvijen je objektni pristup
programiranju. Osnovna ideja je razbiti program u niz zatvorenih cjelina koje zatim
meusobno surauju u rjeavanju problema. Umjesto specijaliziranih procedura koje
barataju podacima, radimo s objektima koji objedinjavaju i operacije i podatke. Pri tome
je vano to objekt radi, a ne kako on to radi. Jedan objekt se moe izbaciti i zamijeniti
drugim, boljim, ako oba rade istu stvar.
Klju za postizanje takvog cilja jest spajanje podataka i operacija, poznato pod
nazivom enkapsulacija. Takoer, podaci su privatni za svaki objekt te ne smiju biti
dostupni ostalim dijelovima programa. To svojstvo se naziva skrivanje podataka. Svaki
objekt svojoj okolini prua iskljuivo podatke koji su joj potrebni da bi se objekt mogao
iskoristiti. Korisnik se ne mora zamarati razmiljajui o nainu na koji objekt
funkcionira on jednostavno trai od objekta odreenu uslugu.
Kada PC preprodava, vlasnik poduzea Taiwan/tavan-Commerce sklapa
raunalo, on zasigurno treba kuite (iako se i to ponekad pokazuje nepotrebnim). No to
ne znai da e on morati poeti od nule (miksajui atome eljeza u aici od
Kinderlade); on e jednostavno otii kod susjednog dilera i kupiti gotovo kuite. Tako
je i u programiranju: mogue je kupiti gotove programske komponente koje se zatim
mogu iskoristiti u programu. Nije potrebno razumjeti kako komponenta radi potrebno
je jedino znati ju iskoristiti.
Takoer, kada projektanti u Renaultu ele izraditi novi model automobila, imaju
dva izbora: ili mogu poeti od nule i ponovo proraunavati svaki najmanji dio motora,
asije i ostalih dijelova, ili mogu jednostavno novi model bazirati na nekom starom
modelu. Kako je kompaniji vrlo vjerojatno u cilju to bre razviti novi model kako bi
pretekla konkurenciju, gotovo sigurno e jednostavno uzeti uspjean model automobila i
samo izmijeniti neka njegova svojstva: promijenit e mu liniju, pojaati motor, dodati
ABS konice. Slino je i s programskim komponentama: prilikom rjeavanja nekog
problema moemo uzdahnuti i poeti kopati, ili moemo uzeti neku ve gotovu
komponentu koja je blizu rjeenja i samo dodati nove mogunosti. To se zove ponovna
iskoristivost (engl. reusability) i vrlo je vano svojstvo. Za novu programsku

14

komponentu kae se da je naslijedila (engl. inherit) svojstva komponente iz koje je


izgraena.
Korisnik koji kupuje auto sigurno nee biti presretan ako se njegov novi model
razlikuje od starog po nainu koritenja: on jednostavno eli pritisnuti gas, a stvar je
nove verzije automobila primjerice krae vrijeme ubrzanja od 0 do 100 km/h. Slino je i
s programskim komponentama: korisnik se ne treba optereivati time koju verziju
komponente koristi on e jednostavno traiti od komponente uslugu, a na njoj je da to
obavi na adekvatan nain. To se zove polimorfizam (engl. polimorphism).
Gore navedena svojstva zajedno sainjavaju objektno orijentirani model
programiranja. Evo kako bi se postupak rotiranja trodimenzionalnih likova proveo
koristei objekte:
1. Listaj sve objekte redom.
2. Zatrai od svakog objekta da se zarotira za neki kut.
Sada glavni program vie ne mora voditi rauna o tome koji se objekt rotira on
jednostavno samo zatrai od objekta da se zarotira. Sam objekt zna to uiniti ovisno o
tome koji lik on predstavlja: kocka e se zarotirati na jedan nain, a kubini spline na
drugi. Takoer, ako se bilo kada kasnije program proiri novim likovima, nije potrebno
mijenjani program koji rotira sve objekte samo je za novi objekt potrebno definirati
operaciju rotacije.
Dakle, ono to C++ jezik ini vrlo pogodnim jezikom ope namjene za izradu
sloenih programa jest mogunost jednostavnog uvoenja novih tipova te naknadnog
dodavanja novih operacija.

2.3. Usporedba s C-om


Mnogi okorjeli C programeri, koji sanjaju strukture i dok se voze u tramvaju ili
razmiljaju o tome kako e svoju novu rutinu rijeiti pomou pokazivaa na funkcije,
dvoume se oko toga je li C++ doista dostojan njihovog kda: mnogi su u strahu od
nepoznatog jezika te se boje da e im se njihov supermunjeviti program za raunanje
nekog fraktalnog skupa na novom jeziku biti sporiji od programa za zbrajanje dvaju
jednoznamenkastih brojeva. Drugi se, pak, kunu da je C++ odgovor na sva njihova
ivotna pitanja, te u fanatinom zanosu umjesto Kristovog roenja slave roendan
gospodina Stroustrupa.
Moramo odmah razoarati obje frakcije: niti jedna nije u pravu te je njihovo
miljenje rezultat nerazumijevanja nove tehnologije. Kao i sve drugo, objektna
tehnologija ima svoje prednosti i mane, a takoer nije svemogua te ne moe rijeiti sve
probleme (na primjer, ona vam nee pomoi da opljakate banku i umaknete Interpolu).
Kao prvo, C++ programi nisu nuno sporiji od svojih C ekvivalenata. No u
pojedinim sluajevima oni doista mogu biti sporiji, a na programeru je da shvati kada je
to dodatno usporenje prevelika smetnja da bi se toleriralo.
Nadalje, koncept klase i enkapsulacija uope ne usporavaju dobiveni izvedbeni
program. Dobiveni strojni kd bi u mnogim sluajevima trebao biti potpuno istovjetan
onome koji e se dobiti iz analognog C programa. Funkcijski lanovi pristupaju

15

podatkovnim lanovima objekata preko pokazivaa, na slian nain na koji to korisnici


proceduralne paradigme ine runo. No C++ kd e biti itljiviji i jasniji te e ga biti
lake napisati i razumjeti. Ako pojedini prevoditelj i daje loiji izvedbeni kd, to je
posljedica loeg prevoditelja, a ne mana jezika.
Takoer, koritenje nasljeivanja ne usporava dobiveni kd ako se ne koriste
virtualni funkcijski lanovi i virtualne osnovne klase. Nasljeivanje samo uteuje
programeru viestruko pisanje kda te olakava ponovno koritenje ve napisanih
programskih odsjeaka.
Virtualne funkcije i virtualne osnovne klase, naprotiv, mogu unijeti znaajno
usporenje u program. Koritenje virtualnih funkcija se obavlja tako da se prije poziva
konzultira posebna tablica, pa je jasno da e poziv svake takve funkcije biti sporiji.
Takoer, pristup lanovima virtualnih osnovnih klasa se redovito obavlja preko jednog
pokazivaa vie. Na programeru je da odredi hoe li koristiti inkriminirana svojstva
jezika ili ne. Da bi se precizno ustanovilo kako djeluje pojedino svojstvo jezika na
izvedbeni kd, nije dobro nagaati i kriviti C++ za loe performanse, ve izmjeriti
vrijeme izvoenja te locirati problem.
No razmislimo o jo jednom problemu: asembler je jedini jezik u kojemu
programer tono zna to se deava u pojedinom trenutku prilikom izvoenja programa.
Kako je programiranje u asembleru bilo sloeno, razvijeni su vii programski jezici koji
to olakavaju. Prevedeni C kd takoer nije maksimalno brz posebno optimiran
asemblerski kd e sigurno dati bolje rezultate. No pisanje takvog kda danas
jednostavno nije mogue: problemi koji se rjeavaju su ipak previe sloeni da bi se
mogli rjeavati tako da se pazi na svaki ciklus procesora. Sloeni problemi zahtijevaju
nove pristupe njihovom rjeavanju: zbog toga imamo na raspolaganju nova raunala s
veim mogunostima koja su sposobna uiniti gubitak performansi beznaajnim u
odnosu na dobitak u brzini razvoja programa.
Slino je i s objektnom tehnologijom: moda e i dobiveni kd biti sporiji i vei od
ekvivalentnog C kda, no jednostavnost njegove izrade e sigurno omoguiti da
dobiveni program bude bolji po nizu drugih karakteristika: bit e jednostavnije izraditi
program koji e biti laki za koritenje, s vie mogunosti i slino. Uostalom, manje
posla vea zarada! (Raj zemaljski!) Tehnologija ide naprijed: dok se gubi neznatno na
brzini i memorijskim zahtjevima, dobici su viestruki.
Takoer, C++ nije svemoan. Koritenje objekata nee napisati pola programa
umjesto vas: ako elite provesti crtanje objekata u tri dimenzije i pri tome ih realistino
osjenati, namuit ete se poteno koristite li C ili C++. To niti ne znai da e
poznavanje objektne tehnologije jamiti da ete ju i ispravno primijeniti: ako se ne
potrudite prilikom izrade klase te posao ne obavite u duhu objektnog programiranja,
nee biti nita od ponovne iskoristivosti kda. ak i ako posao obavite ispravno, to ne
znai da jednog dana neete naii na problem u kojem e jednostavno biti lake
zaboraviti sve napisano i poeti od jajeta.
Ono to vam objektna tehnologija prua jest mogunost da manje panje obratite
jeziku i nainu na koji ete svoju misao izraziti, a da se usredotoite na ono to zapravo
elite uiniti. U gornjem sluaju trodimenzionalnog crtanja objekata to znai da ete
manje vremena provesti razmiljajui gdje ste pohranili podatak o poloaju kamere koji

16

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.4. Zato primjer iz knjige ne radi na mom raunalu?


Zahvaljujui velikoj popularnosti jezika C, programski jezik C++ se brzo proirio i vrlo
su se brzo pojavili mnogi komercijalno dostupni prevoditelji. Od svoje pojave, jezik
C++ se dosta mijenjao, a prevoditelji su kaskali za tim promjenama. Zbog toga se
redovito dogaalo da je program koji se dao korektno prevesti na nekom prevoditelju,
bio neprevodiv ve na sljedeoj inaici prevoditelja istog proizvoaa.
Pojavom zavrnog nacrta standarda je taj dinamiki proces zaustavljen. Stoga bi
bilo logino oekivati da se svi prevoditelji koji su se na tritu pojavili nakon travnja
1995. ponaaju u skladu sa standardom. Naalost, to nije sluaj, tako da vam se moe
lako dogoditi da i na najnovijem prevoditelju nekog proizvoaa (imena neemo
spominjati) ne moete prevesti kd iz knjige ili da vam prevedeni kd radi drukije od
onoga to smo mi napisali.
Naravno (unaprijed se posipamo pepelom), ne otklanjamo mogunost da smo i mi
napravili pogreku. Veinu primjera smo istestirali pomou prevoditelja koji je
uglavnom usklaen sa standardom, ... ali nikad se ne zna. Oznaku prevoditelja neemo
navoditi da nam netko ne bi prigovorio da objavljujemo (strane) plaene reklame (a ne
zato jer bi se radilo o piratskoj kopiji kopija je legalno kupljena i licencirana).

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, AddisonWesley, 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

[Hanly95]

[Horstmann96]

[Kernighan88]

[Kukrika89]
[Liberty96]
[Lippman91]
[Lippman96]
[Murray93]
[Schildt90]
[Stroustrup91]
[Stroustrup94]

Jeri R. Hanly, Elliot B. Koffman, Joan C. Horvath: C Program Design


for Engineers, Addison-Wesley, Reading, MA, 1994, ISBN 0-20159064-6
Cay S. Horstmann: Mastering C++ An Introduction to C++ and
Object-Oriented Programming for C and Pascal Programmers (2nd
Edition), John Wiley and Sons, New York, 1996, ISBN 0-471-10427-2
Brian Kernighan, Dennis Ritchie: The C Programming Language (2nd
Edition), Prentice-Hall, Englewood Cliffs, NJ, 1988, ISBN 0-13937699-2
Milan Kukrika: Programski jezik C, kolska knjiga, Zagreb, 1989,
ISBN 86-03-99627-X
Jesse Liberty, J. Mark Hord: Teach Yourself ANSI C++ in 21 Days,
Sams Publishing, Indianapolis, IN, 1996, ISBN 0-672-30887-6
Stanley B. Lippman: C++ Primer (2nd Edition), Addison-Wesley,
Reading, MA, 1991, ISBN 0-201-53992-6
Stanley B. Lippman: Inside the C++ Object Model, Addison-Wesley,
Reading, MA, 1996, ISBN 0-201-83454-5
Robert B. Murray: C++ Strategies and Tactics, Addison-Wesley,
Reading, MA, 1993, ISBN 0-201-56382-7
Herbert Schildt: Using Turbo C++, Osborne/McGraw-Hill, Berkeley,
CA, 1990, ISBN 0-07-881610-6
Bjarne Stroustrup: The C++ Programming Language (2nd Edition),
Addison-Wesley, Reading, MA, 1991, ISBN 0-201-53992-6
Bjarne Stroustrup: The Design and Evolution of C++, AddisonWesley, Reading, MA, 1994, ISBN 0-201-54330-3

Ponegdje se pozivamo na neku od knjiga, navodei pripadajuu, gore navedenu oznaku


u uglatoj zagradi. Takoer smo koristili lanke iz asopisa C++ Report u kojem se
moe nai mnotvo najnovijih informacija. asopis izlazi od 1989. godine, deset puta
godinje, a izdaje ga SIGS Publications Group, New York. Godita 19911995
objavljena su na CDROM-u (SIGS Books, New York, ISBN 1-884842-24-0). Uz to,
mnotvo odgovora te praktinih i korisnih rjeenja moe se nai na Usenix
konferencijama
(newsgroups):
comp.lang.c++,
comp.lang.c++.moderated
i
comp.std.c++.
Od svih navedenih knjiga, najreferentniji je nacrt ANSI C++ standarda. Moe se
nai na nekoliko Internet servera diljem svijeta i to u istom tekstovnom formatu, PDF
(Acrobat Reader) formatu ili u PostScript formatu. Broji oko 600 stranica formata A4
(oko 4MB komprimirane datoteke), a kao kuriozitet navedimo da je preko CARNet

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-80535340-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

3. Oluja kroz jezik C++


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

U uvodnom poglavlju proletjet emo kroz osnovne pojmove vezane uz programiranje i


upoznat emo se s strukturom najjednostavnijih programa pisanih u programskom
jeziku C++. Ovo poglavlje prvenstveno je namijenjeno itateljicama i itateljima koji
nisu nikada pisali programe u bilo kojem viem programskom jeziku i onima koji su to
moda radili, ali je od tada prola itava vjenost.

3.1. to je program i kako ga napisati


Elektronika raunala su danas postala pribor kojim se svakodnevno koristimo kako
bismo si olakali posao ili se zabavili. Istina, tonost prvog navoda e mnogi poricati,
istiui kao protuprimjer injenicu da im je za podizanje novca prije trebalo znatno
manje vremena nego otkad su alteri u banci kompjuterizirani. Ipak, injenica je da su
mnogi poslovi danas nezamislivi bez raunala; u krajnjoj liniji, dokaz za to je knjiga
koju upravo itate koja je u potpunosti napisana pomou raunala.
Samo raunalo, ak i kada se ukljui u struju, nije kadro uiniti nita korisno. Na
dananjim raunalima se ne moe ak ni zagrijati prosjean ruak, to je inae bilo
mogue na raunalima s elektronskim cijevima. Ono to vam nedostaje jest pamet
neophodna za koristan rad raunala: programi, programi, mnotvo programa. Pod
programom tu podrazumijevamo niz naredbi u strojnom jeziku koje procesor u vaem
raunalu izvodi i shodno njima obrauje podatke, provodi matematike proraune,
ispisuje tekstove, iscrtava krivulje na zaslonu ili gaa va avion F-16 raketama srednjeg
dometa. Pokretanjem programa s diska, diskete ili CD-ROM-a, program se uitava u
radnu memoriju raunala i procesor poinje s mukotrpnim postupkom njegova
izvoenja.
Programi koje pokreete na raunalu su u izvedbenom obliku (engl. executable),
razumljivom samo procesoru vaeg (i njemu slinih) raunala, sretnim procesorovim
roditeljima negdje u Silicijskoj dolini i nekolicini zadrtih hakera irom svijeta koji jo
uvijek programiraju u strojnom kdu. U sutini se strojni kd sastoji od nizova binarnih
znamenki: nula i jedinica. Budui da su dananji programi tipino duljine nekoliko
megabajta, nasluujete da ih autori nisu pisali izravno u strojnom kdu (kamo sree da
je tako ne bi Microsoft tek tako svake dvije godine izbacivao nove Windowse!).

20

Poetak

Upis i ispravke
izvornog kda

Prevoenje

Da

Pogreke tijekom
prevoenja?

Ne
Povezivanje

Da

Pogreka tijekom
povezivanja?

Gotovo svi dananji programi se piu u


nekom od viih programskih jezika
(FORTRAN, BASIC, Pascal, C) koji su
donekle razumljivi i ljudima (barem onima
koji imaju neto pojma o engleskom jeziku).
Naredbe u tim jezicima se sastoje od
mnemonika. Kombiniranjem tih naredbi
programer slae izvorni kd (engl. source
code) programa, koji se pomou posebnih
programa prevoditelja (engl. compiler) i
povezivaa (engl. linker) prevodi u izvedbeni
kd. Prema tome, pisanje programa u uem
smislu podrazumijeva pisanje izvornog kda.
Meutim, kako pisanje kda nije samo sebi
svrhom, pod pisanjem programa u irem
smislu podrazumijeva se i prevoenje,
odnosno povezivanje programa u izvedbeni
kd. Stoga moemo govoriti o etiri faze
izrade programa:
1. pisanje izvornog kda
2. prevoenje izvornog kda,
3. povezivanje u izvedbeni kd te
4. testiranje programa.

Da bi se za neki program moglo rei da je


uspjeno zgotovljen, treba uspjeno proi
kroz sve etiri faze.
Izvoenje programa
Kao i svaki drugi posao, i pisanje
programa iziskuje odreeno znanje i vjetinu.
Prilikom pisanja programera vrebaju Scile i
Haribde, danas poznatije pod nazivom
Da
Pogreke tijekom
pogreke ili bugovi (engl. bug - stjenica) u
izvoenja?
programu. Uoi li se pogreka u nekoj od faza
izrade programa, izvorni kd treba doraditi i
Ne
ponoviti sve prethodne faze. Zbog toga
postupak izrade programa nije pravocrtan, ve
Konac
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
Ne

21

nazivom integrirane razvojne okoline (engl. integrated development environment, IDE).


Nakon to je (prema obino nekritikom miljenju programera) pisanje izvornog kda
zavreno, on se pohrani u datoteku izvornog kda na disku. Toj datoteci se obino daje
nekakvo smisleno ime, pri emu se ono za kdove pisane u programskom jeziku C++
obino proiruje nastavkom .cpp, .cp ili samo .c, na primjer pero.cpp. Nastavak je
potreban samo zato da bismo izvorni kd kasnije mogli lake pronai.
Slijedi prevoenje izvornog kda. U integriranim razvojnim okolinama program za
prevoenje se pokree pritiskom na neku tipku na zaslonu, pritiskom odgovarajue tipke
na tipkovnici ili iz nekog od izbornika (engl. menu) ako prevoditelj nije integriran,
poziv je neto sloeniji. Prevoditelj tijekom prevoenja provjerava sintaksu napisanog
izvornog kda i u sluaju uoenih ili nasluenih pogreaka ispisuje odgovarajue poruke
o pogrekama, odnosno upozorenja. Pogreke koje prijavi prevoditelj nazivaju se
pogrekama pri prevoenju (engl. compile-time errors). Nakon toga programer e
pokuati ispraviti sve navedene pogreke i ponovo prevesti izvorni kd sve dok
prevoenje kda ne bude uspjeno okonano, nee se moi pristupiti povezivanju kda.
Prevoenjem izvornog dobiva se datoteka objektnog kda (engl. object code), koja se
lako moe prepoznata po tome to obino ima nastavak .o ili .obj (u naem primjeru
bi to bio pero.obj).
Nakon to su ispravljene sve pogreke uoene prilikom prevoenja i kd ispravno
preveden, pristupa se povezivanju objektnih kdova u izvedbeni. U veini sluajeva
objektni kd dobiven prevoenjem programerovog izvornog kda treba povezati s
postojeim bibliotekama (engl. libraries). Biblioteke su datoteke u kojima se nalaze ve
prevedene gotove funkcije ili podaci. One se isporuuju zajedno s prevoditeljem, mogu
se zasebno kupiti ili ih programer moe tijekom rada sam razvijati. Bibliotekama se
izbjegava opetovano pisanje vrlo esto koritenih operacija. Tipian primjer za to je
biblioteka matematikih funkcija koja se redovito isporuuje uz prevoditelje, a u kojoj
su definirane sve funkcije poput trigonometrijskih, hiperbolnih, eksponencijalnih i sl.
Prilikom povezivanja provjerava se mogu li se svi pozivi kdova realizirati u
izvedbenom kdu. Uoi li poveziva neku nepravilnost tijekom povezivanja, ispisat e
poruku o pogreki i onemoguiti generiranje izvedbenog kda. Ove pogreke nazivaju
se pogrekama pri povezivanju (engl. link-time errors) sada programer mora prionuti
ispravljanju pogreaka koje su nastale pri povezivanju. Nakon to se isprave sve
pogreke, kd treba ponovno prevesti i povezati.
Uspjenim povezivanjem dobiva se izvedbeni kd. Meutim, takav izvedbeni kd
jo uvijek ne jami da e program raditi ono to ste zamislili. Primjerice, moe se
dogoditi da program radi pravilno za neke podatke, ali da se za druge podatke ponaa
nepredvidivo. U tom se sluaju radi o pogrekama pri izvoenju (engl. run-time errors).
Da bi program bio potpuno korektan, programer treba istestirati program da bi uoio i
ispravio te pogreke, to znai ponavljanje cijelog postupka u lancu ispravljanje
izvornog kda-prevoenje-povezivanje-testiranje. Kod jednostavnijih programa broj
ponavljanja e biti manji i smanjivat e se proporcionalno s rastuim iskustvom
programera. Meutim, kako raste sloenost programa, tako se poveava broj moguih

Ovdje neemo opisivati konkretno kako se pokreu postupci prevoenja ili povezivanja, jer to
varira ovisno o prevoditelju, odnosno povezivau.

22

pogreaka i cijeli postupak izrade programa neiskusnom programeru moe postati


mukotrpan.
Za ispravljanje pogreaka pri izvoenju, programeru na raspolaganju stoje programi
za otkrivanje pogreaka (engl. debugger). Radi se o programima koji omoguavaju
prekid izvoenja izvedbenog kda programa koji testiramo na unaprijed zadanim
naredbama, izvoenje programa naredbu po naredbu, ispis i promjene trenutnih
vrijednosti pojedinih podataka u programu. Najjednostavniji programi za otkrivanje
pogreaka ispisuju izvedbeni kd u obliku strojnih naredbi. Meutim, veina dananjih
naprednih programa za otkrivanje pogreaka su simboliki (engl. symbolic debugger)
iako se izvodi prevedeni, strojni kd, izvoenje programa se prati preko izvornog kda
pisanog u viem programskom jeziku. To omoguava vrlo lagano lociranje i ispravljanje
pogreaka u programu.
Osim pogreaka, prevoditelj i poveziva redovito dojavljuju i upozorenja. Ona ne
onemoguavaju nastavak prevoenja, odnosno povezivanja kda, ali predstavljaju
potencijalnu opasnost. Upozorenja se mogu podijeliti u dvije grupe. Prvu grupu ine
upozorenja koja javljaju da kd nije potpuno korektan. Prevoditelj ili poveziva e
zanemariti nau pogreku i prema svom nahoenju izgenerirati kd. Drugu grupu ine
poruke koje upozoravaju da nisu sigurni je li ono to smo napisali upravo ono to smo
eljeli napisati, tj. radi se o dobronamjernim upozorenjima na zamke koje mogu
proizii iz naina na koji smo program napisali. Iako e, unato upozorenjima, program
biti preveden i povezan (moda ak i korektno), pedantan programer nee ta upozorenja
nikada zanemariti ona esto upuuju na uzrok pogreaka pri izvoenju gotovog
programa. Za precizno tumaenje poruka o pogrekama i upozorenja neophodna je
dokumentacija koja se isporuuje uz prevoditelj i poveziva.
Da zakljuimo: to nam dakle treba za pisanje programa u jeziku C++? Osim
raunala, programa za pisanje i ispravljanje teksta, prevoditelja i povezivaa trebaju
vam jo samo tri stvari: interes, puno slobodnih popodneva i dooobra knjiga. Interes
vjerojatno postoji, ako ste s itanjem stigli ak do ovdje. Slobodno vrijeme e vam
trebati da isprobate primjere i da se sami okuate u bespuima C++ zbiljnosti. Jer, kao
to stari junohrvatski izrijek kae: Nima dopisne kole iz plivanja. Stoga ne gubite
vrijeme i odmah otkaite sudar curi ili deku. A za dooobru knjigu(ta-ta-daaam
tu sada mi upadamo!).

23

3.2. Moj prvi i drugi C++ program


Bez mnogo okolianja i filozofiranja, napiimo najjednostavniji program u jeziku C++:
int main() {
return 0;
}

Utipkate li nadobudno ovaj program u svoje raunalo, pokrenete odgovarajui


prevoditelj, te nakon uspjenog prevoenja pokrenete program, na zaslonu raunala
sigurno neete dobiti nita! Nemojte se odmah hvatati za glavu, laati telefona i zvati
svoga dobavljaa raunala. Gornji program zaista nita ne radi, a ako neto i dobijete na
zaslonu vaeg raunala, znai da ste negdje pogrijeili prilikom utipkavanja. Unato
jalovosti gornjeg kda, promotrimo ga, redak po redak.
U prvom retku je napisano int main(). main je naziv za glavnu funkciju u
svakom C++ programu. Izvoenje svakog programa poinje naredbama koje se nalaze u
njoj.
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

Ovom naredbom se pokree program pkunzip i pritom mu se predaju dva parametra: t i mojzip. U naem C++ primjeru unutar zagrada nema nita, to znai da ne
prenosimo nikakve argumente. Tako e i ostati do daljnjega, tonije do poglavlja 5.11 u
kojem emo detaljnije obraditi funkcije, a posebice funkciju main().
Slijedi otvorena vitiasta zagrada. Ona oznaava poetak bloka u kojem e se
nalaziti naredbe glavne funkcije, dok zatvorena vitiasta zagrada u zadnjem retku
oznaava kraj tog bloka. U samom bloku prosjeni itatelj uoit e samo jednu naredbu,
return 0. Tom naredbom glavni program vraa pozivnom programu broj 0, a to je
poruka operacijskom sustavu da je program uspjeno okonan, togod on radio.
Uoimo znak ; (toka-zarez) iza naredbe return 0! On oznaava kraj naredbe te
slui kao poruka prevoditelju da sve znakove koji slijede interpretira kao novu naredbu.
Znak ; mora zakljuivati svaku naredbu u jeziku C++.
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;

Prilikom prevoenja prevoditelj e uoiti da funkcija main() nije pravilno zatvorena,


te e ispisati poruku o pogreki oblika Pogreka u prvi.cpp xx: sloenoj naredbi
nedostaje } u funkciji main(). U ovoj poruci je prvi.cpp ime datoteke u koju smo na
izvorni kd pohranili, a xx je broj retka u kojem se pronaena pogreka nalazi.
Zaboravimo li napisati naredbu return:
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

Programi poput gornjeg nemaju ba neku praktinu primjenu, te daljnju analizu


gornjeg kda preputamo poklonicima minimalizma. We need some action, man! Stoga
pogledajmo sljedei primjer:
#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:
Ja sam za C++!!! A vi?

Ona je u programu napisana unutar znakova navodnika, koji upuuju na to da se radi o


tekstu koji treba ispisati doslovce. Biblioteka iostream bit e detaljnije opisana
kasnije, u poglavlju 16.
Meutim, to jo nije sve! Iza znakovnog niza ponavlja se operator za ispis, kojeg
slijedi endl. endl je konstanta u biblioteci iostream koja prebacuje ispis u novi
redak, to jest vraa kurzor (engl. cursor) na poetak sljedeeg retka na zaslonu.
Dovitljiva itateljica ili itatelj e sami zakljuiti da bi se operatori za ispis mogli dalje
nadovezivati u istoj naredbi:
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!

3.3. Moj trei C++ program


Sljedei primjer je pravi mali dragulj interaktivnog programa:
#include <iostream.h>
int main() {
int a, b, c;
cout << "Upii prvi broj:";
cin >> a;
// oekuje prvi broj
cout << "Upii i drugi broj:";
cin >> b;
// oekuje drugi broj
c = a + b;
// rauna njihov zbroj
// ispisuje rezultat:
cout << "Njihov zbroj je: " << c << endl;
return 0;
}

U ovom primjeru uoavamo nekoliko novina. Prvo, to je redak


int a, b, c;

u kojem se deklariraju tri varijable a, b i c. Kljunom rijei (identifikatorom tipa) int


deklarirali smo ih kao cjelobrojne, tj. tipa int. Deklaracijom smo pridijelili simboliko,
nama razumljivo i lako pamtivo ime memorijskom prostoru u koji e se pohranjivati
vrijednosti tih varijabli. Naiavi na te deklaracije, prevoditelj e zapamtiti njihova
imena, te za njih rezervirati odgovarajui prostor u memoriji raunala. Kada tijekom
prevoenja ponovno sretne varijablu s nekim od tih imena, prevoditelj e znati da se
radi o cjelobrojnoj varijabli i znat e gdje bi se ona u memoriji trebala nalaziti. Osim
toga, tip varijable odreuje raspon dozvoljenih vrijednosti te varijable, te definira
operacije nad njima. Opirnije o deklaracijama varijabli i o tipovima podataka govorit
emo u sljedeem poglavlju.
Slijedi ispis poruke Upii prvi broj:, ije znaenje ne trebamo objanjavati.
Iza poruke nismo dodali ispis znaka endl, jer elimo da nam kurzor ostane iza poruke,
u istom retku, spreman za unos prvog broja.
Druga novina je ulazni tok (engl. input stream) cin koje je zajedno s izlaznim
tokom definiran u datoteci zaglavlja iostream.h. On slui za uitavanje podataka s
konzole, tj. tipkovnice. Operatorom >> (dvostruki znak vee od) podaci s konzole

27

upuuju se u memoriju varijabli a (prvi broj), odnosno b (drugi broj). Naglasimo da


uitavanje poinje tek kada se na tipkovnici pritisne tipka za novi redak, tj. tipka Enter.
To znai da kada pokrenete program, nakon ispisane poruke moete utipkavati bilo to i
to po potrebi brisati tek kada pritisnete tipku Enter, program e analizirati unos te
pohraniti broj koji je upisan u memoriju raunala. Napomenimo da taj broj u naem
primjeru ne smije biti vei od 32767 niti manji od 32768, jer je to dozvoljeni raspon
cjelobrojnih int varijabli. Unos veeg broja nee imati nikakve dramatine posljedice
na daljnji rad vaeg raunala, ali e dati krivi rezultat na kraju rauna.
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.
Radi saetosti kda, u veini primjera koji slijede pretprocesorska naredba za
ukljuivanje iostream biblioteke nee biti napisana, ali se ona podrazumijeva. Koriste
li se ulazno-izlazni tokovi, njeno izostavljanje prouzroit e pogreku kod prevoenja.
No, zavrimo s naim primjerom. Nakon unosa prvog broja, po istom principu se
unosi drugi broj b, a zatim slijedi naredba za raunanje zbroja
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.
Operatore << i >> moemo shvatiti kao da pokazuju smjer prijenosa
podataka [Lippman91].
Primjerice,
>> a

preslikava vrijednost u a, dok


<< 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.
*/

Stoga je ovakav nain komentiranja naroito pogodan za (privremeno) iskljuivanje


dijelova izvornog kda. Ispred naredbe u nizu koji elimo iskljuiti dodat emo oznaku
/* za poetak komentara, a iza zadnje naredbe u nizu nadodat emo oznaku */ za
zakljuenje komentara.
Iako komentiranje programa iziskuje dodatno vrijeme i napor, u kompleksnijim
programima ono se redovito isplati. Dogodi li se da netko drugi mora ispravljati va
kd, ili (jo gore) da nakon dugo vremena vi sami morate ispravljati svoj kd, komentari
e vam olakati da proniknete u ono to je autor njime htio rei. Svaki ozbiljniji
programer ili onaj tko to eli postati mora biti svjestan da e nakon desetak ili stotinjak
napisanih programa poeti zaboravljati emu pojedini program slui. Zato je vrlo
korisno na poetku datoteke izvornog programa u komentaru navesti osnovne
generalije, na primjer ime programa, ime datoteke izvornog kda, kratki opis onoga
to bi program trebao raditi, funkcije i klase definirane u datoteci te njihovo znaenje,
autor(i) kda, uvjeti pod kojima je program preveden (operacijski sustav, ime i oznaka
prevoditelja), zabiljeke, te naknadne izmjene:

29

/**********************************************************
Program:
Datoteka:
Funkcije:
Opis:
Autori:

Moj trei C++ program


Treci.cpp
main() - cijeli program je u jednoj datoteci
Uitava dva broja i ispisuje njihov zbroj
Boris (bm)
Julijan (j)
Okruenje: Prozori95
PHP C++ 4.5 prevoditelj
Zabiljeke: Znam Boris da si ti protiv ovakvih komentara,
ali sam ih morao staviti
Izmjene:
15.07.96.
(j) prva inaica
21.08.96.
(bm) svi podaci su iz float
promijenjeni u int
**********************************************************/

S druge strane, s koliinom komentara ne treba pretjerivati, jer e u protivnom izvorni


kd postati nepregledan. Naravno da ete izbjegavati komentare poput:
c = a + b;
i++;
y = sqrt(x)

// zbraja a i b
// uveava i za 1
// poziva funkciju sqrt

Nudimo vam sljedee naputke gdje i to komentirati:


Na poetku datoteke izvornog kda opisati sadraj datoteke.
Kod deklaracije varijabli, klasa i objekata obrazloiti njihovo znaenje i primjenu.
Ispred funkcije dati opis to radi, to su joj argumenti i to vraa kao rezultat.
Eventualno dati opis algoritma koji se primjenjuje.
Dati saeti opis na mjestima u programu gdje nije potpuno oito to kd radi.
Radi bolje razumljivosti, primjeri u knjizi bit e redovito prekomentirani, tako da e
komentari biti pisani i tamo gdje je iskusnom korisniku jasno znaenje kda. Osim toga,
redovito emo ubacivati komentare uz naredbe za koje bi prevoditelj javio pogreku.

3.5. Rastavljanje naredbi


Razmotrimo jo dva problema vana svakoj poetnici ili poetniku: praznine u
izvornom kdu i produljenje naredbi u vie redaka. U primjerima u knjizi intenzivno
emo koristiti praznine izmeu imena varijabli i operatora, iako ih iskusniji programeri
esto izostavljaju. Tako smo umjesto
c = a + b;

mogli pisati

30

c=a+b;

Umetanje praznina doprinosi preglednosti kda, a esto je i neophodno da bi prevoditelj


interpretirao djelovanje nekog operatora onako kako mi oekujemo od njega. Ako je
negdje dozvoljeno umetnuti prazninu, tada broj praznina koje se smiju umetnuti nije
ogranien, pa smo tako gornju naredbu mogli pisati i kao
c=

to je jo nepreglednije! Istaknimo da se pod prazninom ne podrazumijevaju iskljuivo


prazna mjesta dobivena pritiskom na razmaknicu (engl. blanks), ve i praznine
dobivene tabulatorom (engl. tabs) te znakove za pomak u novi red (engl. newlines).
Naredbe u izvornom kdu nisu ograniene na samo jedan redak, ve se mogu
protezati na nekoliko redaka zavretak svake naredbe jednoznano je odreen znakom
;. Stoga pisanje naredbe moemo prekinuti na bilo kojem mjestu gdje je dozvoljena
praznina, te nastaviti pisanje u sljedeem retku, na primjer
c =
a + b;

Naravno da je ovakvim potezom kd iz razmatranog primjera postao nepregledniji.


Razdvajati naredbe u vie redaka ima smisla samo kod vrlo dugakih naredbi, kada one
ne stanu u jedan redak. Veina klasinih tampaa i ekranskih editora podrava retke od
80 znakova, pa vam preporuujemo da sve naredbe koje zauzimaju vie od 80 znakova
razbijete u vie redaka. Zbog formata knjige duljine redaka u naim primjerima su
manje.
Posebnu panju treba obratiti na razdvajanje znakovnih nizova. Ako bismo pokuali
prevesti sljedei primjer, dobit emo pogreku prilikom prevoenja:
#include <iostream.h>
int main() {
// pogreka: nepravilno razdvojeni znakovni niz
cout << "Pojavom jezika C++ ostvaren je
tisuljetni san svih programera" << endl;
return 0;
}

Prevoditelj e javiti pogreku da znakovni niz Pojavom ... ostvaren je nije


zakljuen znakom dvostrukog navodnika, jer prevoditelj ne uoava da se niz nastavlja u
sljedeem retku. Da bismo mu to dali do znanja, zavretak prvog dijela niza treba
oznaiti lijevom kosom crtom \ (engl. backslash):
cout << "Pojavom jezika C++ ostvaren je \
tisuljetni san svih programera" << endl;

31

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:
cout << "Pojavom jezika C++ ostvaren je "
"tisuljetni san svih programera" << endl;

Pritom se ne smije zaboraviti staviti prazninu na kraju prvog ili poetak drugog niza.
Gornji niz mogli smo rastaviti na bilo kojem mjestu:
cout << "Pojavom jezika C++ ostvaren je tis"
"uljetni san svih programera" << endl;

ali je oito takav pristup manje itljiv.


Budui da znak ; oznaava kraj naredbe, mogue je vie naredbi napisati u jednom
retku. Tako smo Moj trei program mogli napisati i na sljedei nain:
#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

4. Osnovni tipovi podataka


Postoje dva tipa ljudi:
jedni koji nose pitolje, drugi koji kopaju...
Clint Eastwood, u filmu Dobar, lo, zao

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

Tablica 4.1. Kljune rijei jezika C++


asm
auto
bool
break
case
catch
char
class
const
const_cast
continue
default
delete
do
double
dynamic_cast

else
enum
explicit
extern
false
float
for
friend
goto
if
inline
int
long
mutable
namespace
new

operator
private
protected
public
register
reinterpret_cast
return
short
signed
sizeof
static
static_cast
struct
switch
template
this

throw
true
try
typedef
typeid
typename
union
unsigned
using
virtual
void
volatile
wchar_t
while

Tablica 4.2. Alternativne oznake operatora


and
and_eq

bitand
bitor

compl
not

not_eq
or

or_eq
xor

xor_eq

a izbjegavati imena poput


Snjeguljica_i_7_patuljaka
MojaPrivatnaVarijabla
FrankieGoesToHollywood
RockyXXVII

Unato injenici da e neki prevoditelji prihvatiti i nae dijakritike znakove u


identifikatorima, zbog prenosivosti kda preporuuje se njihovo izbjegavanje. U
protivnom se lako moe dogoditi da vam program s varijablama BeiJankec ili
TeksakiMasakrMotornjaom proe kod jednog prevoditelja, a na drugom prouzroi
pogreku prilikom prevoenja.
Valja uoiti da jezik C++ razlikuje velika i mala slova u imenima, tako da
identifikatori
maliNarodiVelikeIdeje
MaliNarodiVelikeIdeje
malinarodiVELIKEIDEJE

predstavljaju tri razliita naziva. Takoer, iako ne postoji teoretsko ogranienje na


duljinu imena, svi prevoditelji razlikuju imena samo po prvih nekoliko znakova (pojam

35

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

(praznina umjesto znaka podcrtavanja izmeu rijei oznaavala bi prekid imena, to bi


uzrokovalo pogreku prilikom prevoenja). U drugom pristupu rijei se piu spojeno, s
velikim poetnim slovom:
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.

4.2. Varijable, objekti i tipovi


Bez obzira na jezik u kojem je pisan, svaki program sastoji se od niza naredbi koje
mijenjaju vrijednosti objekata pohranjenih u memoriji raunala. Raunalo dijelove
memorije u kojima su smjeteni objekti razlikuje pomou pripadajue memorijske
adrese. Da programer tijekom pisanja programa ne bi morao pamtiti memorijske adrese,
svi programski jezici omoguavaju da se vrijednosti objekata dohvaaju preko
simbolikih naziva razumljivih inteligentnim biima poput ljudi-programera. Gledano
iz perspektive obinog raunala (lat. computer vulgaris ex terae Tai-wan), objekt je
samo dio memorije u koji je pohranjena njegova vrijednost u binarnom obliku. Tip
objekta, izmeu ostalog, odreuje raspored bitova prema kojem je taj objekt pohranjen u
memoriju.
U programskom jeziku C++ pod objektima u uem smislu rijei obino se
podrazumijevaju sloeni tipovi podataka opisani pomou posebnih receptura klasa,
to e biti opisano u kasnijim poglavljima. Jednostavni objekti koji pamte jedan cijeli ili
realni broj se esto nazivaju varijablama.

36

Da bi prevoditelj pravilno preveo na izvorni C++ kd u strojni jezik, svaku


varijablu treba prije njena koritenja u kdu deklarirati, odnosno jednoznano odrediti
njen tip. U Naem treem C++ programu varijable su deklarirane u prvom retku
glavne funkcije:
int a, b, c;

Tim deklaracijama je prevoditelju jasno dano do znanja da e varijable a, b i c biti


cjelobrojne prevoditelj e vrijednosti tih varijabli pohranjivati u memoriju prema
svom pravilu za cjelobrojne podatke. Takoer e znati kako treba provesti operacije na
njima. Prilikom deklaracije varijable treba takoer paziti da se u istom dijelu programa
(tonije bloku naredbi, vidi poglavlje 5.1) ne smiju deklarirati vie puta varijable s istim
imenom, ak i ako su one razliitih tipova. Zato e prilikom prevoenja sljedeeg kda
prevoditelj javiti pogreku o viekratnoj deklaraciji varijable a:
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;

Prevoditelj tekar tada pridruuje memorijski prostor u koji se pohranjuju podaci.


Postupak deklaracije varijable a i pridruivanja vrijednosti simboliki moemo prikazati
slikom 4.1. Lijevo od dvotoke je kuica koja simbolizira varijablu s njenim tipom i

int
int

aa

int
int
22

Slika 4.1. Deklaracija varijable i pridruivanje vrijednosti

imenom. Desno od dvotoke je kuica koja predstavlja objekt s njegovim tipom i


konkretnom vrijednou. Ako nakon toga istoj varijabli a pridruimo novu vrijednost
naredbom
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

Deklaracija varijable i pridruivanje vrijednosti mogu se obaviti u istom retku kda.

int
int

aa

int
int
55

Slika 4.2. Pridruivanje nove vrijednosti

Umjesto da piemo poseban redak s deklaracijama varijabli a, b i c i zatim im


pridruujemo vrijednosti, inicijalizaciju moemo provesti ovako:
int a = 2;
int b = a;
int c = a + b;

Za razliku od programskog jezika C u kojem sve deklaracije moraju biti na poetku


programa ili funkcije, prije prve naredbe, u C++ jeziku ne postoji takvo ogranienje, pa
deklaracija varijable moe biti bilo gdje unutar programa. tovie, mnogi autori
preporuuju da se varijable deklariraju neposredno prije njihove prve primjene.

4.3. Operator pridruivanja


Operatorom pridruivanja mijenja se vrijednost nekog objekta, pri emu tip objekta
ostaje nepromijenjen. Najei operator pridruivanja je znak jednakosti (=) kojim se
objektu na lijevoj strani pridruuje neka vrijednost s desne strane. Ako su objekt s lijeve
strane i vrijednost s desne strane razliitih tipova, vrijednost se svodi na tip objekta
prema definiranim pravilima konverzije, to e biti objanjeno u poglavlju 4.4 o
tipovima podataka. Oito je da s lijeve strane operatora pridruivanja mogu biti
iskljuivo promjenjivi objekti, pa se stoga ne moe pisati ono to je inae matematiki
korektno:
2 = 4 / 2
3 * 4 = 12
3.14159 = pi

// pogreka!!!
// pogreka!!!
// 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;

Naredbom u prvom retku deklarira se varijabla tipa int, te se njena vrijednost


inicijalizira na 5. Dosljedno gledano, operator = ovdje nema znaenje pridruivanja, jer
se inicijalizacija varijable i provodi ve tijekom prevoenja, a ne prilikom izvoenja
programa. To znai da je broj 5 ugraen u izvedbeni strojni kd dobiven prevoenjem i
povezivanjem programa. Obratimo, meutim, panju na drugu naredbu!
Matematiki gledano, drugi redak u ovom primjeru je besmislen: nema broja koji bi
bio jednak samom sebi uveanom za 3! Ako ga pak gledamo kao uputu raunalu to mu
je initi, onda taj redak treba poeti itati neposredno iza znaka pridruivanja: uzmi
vrijednost varijable i, dodaj joj broj 3..... Doli smo do kraja naredbe (znak ;), pa se
vraamo na operator pridruivanja: ...i dobiveni zbroj s desne strane pridrui varijabli i
koja se nalazi s lijeve strane znaka jednakosti. Nakon izvedene naredbe varijabla i
imat e vrijednost 8.
Jezik C++ dozvoljava vie operatora pridruivanja u istoj naredbi. Pritom
pridruivanje ide od krajnjeg desnog operatora prema lijevo:
a = b = c = 0;

te ih je tako najsigurnije i itati: broj 0 pridrui varijabli c, iju vrijednost pridrui


varijabli b, iju vrijednost pridrui varijabli a. Budui da se svakom od objekata lijevo
od znaka = pridruuje neka vrijednost, svi objekti izuzev onih koji se nalaze desno od
najdesnijeg znaka = moraju biti lvrijednosti:
a = b = c + d;
a = b + 1 = c;

// OK!
// pogreka: b + 1 nije lvrijednost

4.4. Tipovi podataka i operatori


U C++ jeziku ugraeni su neki osnovni tipovi podataka i definirane operacije na njima.
Za te su tipove precizno definirana pravila provjere i konverzije. Pravila provjere tipa
(engl. type-checking rules) uoavaju neispravne operacije na objektima, dok pravila
konverzije odreuju to e se dogoditi ako neka operacija oekuje jedan tip podataka, a
umjesto njega se pojavi drugi. U sljedeim poglavljima prvo emo se upoznati s
brojevima i operacijama na njima, da bismo kasnije preli na pobrojenja, logike
vrijednosti i znakove.
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. floatingpoint). Najjednostavniji tip brojeva su cijeli brojevi njih smo ve upoznali u Naem

39

treem C++ programu. Cjelobrojna varijabla deklarira se rijeju int i njena e


vrijednost u memoriji raunala obino zauzeti dva bajta (engl. byte), tj. 16 bitova. Prvi
bit je rezerviran za predznak, tako da preostaje 15 bitova za pohranu vrijednosti. Stoga
se varijablom tipa int mogu obuhvatiti svi brojevi od najmanjeg broja (najveeg
negativnog broja)
215 = 32768
do najveeg broja
215 1 = 32767
Za veinu praktinih primjena taj opseg vrijednosti je dostatan. Meutim, ukae li se
potreba za veim cijelim brojevima varijablu moemo deklarirati kao long int, ili
krae samo long:
long int HrvataUDijasporiZaVrijemeIzbora = 3263456;
long ZrnacaPrasineNaMomRacunalu = 548234581;
// mogao bih uzeti krpu za prainu i svesti to na int!

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

int SnjeguljicaIPatuljci = 010;

// odgovara dekadsko 8!

Heksadekadske konstante zapoinju s 0x ili 0X:


int TucetPatuljaka = 0x0C;

// dekadsko 12

Slova za heksadekadske znamenke mogu biti velika ili mala.


Vodea nula kod oktalnih brojeva uzrokom je estih poetnikih pogreaka, jer
korisnik obino smatra da e ju prevoditelj zanemariti i interpretirati broj kao obini
dekadski.
Sve konstante koje zapoinju s brojem 0 prevoditelj interpretira kao oktalne
brojeve. To znai da e nakon prevoenja 010 i 10 biti dva razliita broja!
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;
i = 040;
cout << i << endl;

// oktalni prikaz broja 32

i = 0x20;
cout << i << endl;

// heksadekadski prikaz broja 32

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;

Prvi primjer odgovara uobiajenom nainu prikaza brojeva s decimalnim zarezom. U


drugom i treem primjeru brojevi su prikazani u znanstvenoj notaciji, kao umnoak
mantise i potencije na bazi 10 (2,997925108, odnosno 1,61019). Kod prikaza u
znanstvenoj notaciji, slovo 'e' koje razdvaja mantisu od eksponenta moe biti veliko ili
malo.
Praznine unutar broja, na primjer iza predznaka, ili izmeu znamenki i slova
'e' nisu dozvoljene.

41

Prevoditelj e broj
float PlanckovaKonst = 6.626 e -34;

// pogreka

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
float pi_tocniji
= 3.141592654;
float pi_manjeTocan = 3.1415927;

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:
cout
cout
long
cout

<< "Veliina cijelih brojeva: " << sizeof(int);


<< "Veliina realnih brojeva: " << sizeof(float);
double masaSunca = 2e30;
<< "Veliina long double: " << sizeof(masaSunca);

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

tip konstante

bajtova

raspon vrijednosti

tonost

char

short int
int

2
2
(4)

long int

float

3,41038 do 3,41038
3,41038 do 3,41038

7 dec. znamenki

double

1,710308 do 1,710308
1,710308 do 1,710308

15 dec. znamenki

long double

10

1,1104932 do 3,4104932 i
3,4104932 do 1,1104932

18 dec. znamenki

32768 do 32767
32768
do
32767
2147483648 do 2147483647
2147483648 do 2147483647

Svi navedeni brojevni tipovi mogu biti deklarirani i bez predznaka, dodavanjem rijei
unsigned ispred njihove deklaracije:
unsigned int i = 40000;
unsigned long int li;
unsigned long double BrojZvijezdaUSvemiru;

U tom sluaju e prevoditelj bit za predznak upotrijebiti kao dodatnu binarnu


znamenku, pa se najvea mogua vrijednost udvostruuje u odnosu na varijable s
predznakom, ali se naravno ograniava samo na pozitivne vrijednosti. Pridruimo li
unsigned varijabli negativan broj, ona e poprimiti vrijednost nekog pozitivnog broja.
Na primjer, programski slijed
unsigned int = -1;
cout << i << endl;

e za varijablu i ispisati broj 65535 (uoimo da je on za 1 manji od najveeg


dozvoljenog unsigned int broja 65536). Zanimljivo da prevoditelj za gornji kd nee
javiti pogreku.
Prevoditelj ne prijavljuje pogreku ako se unsigned varijabli pridrui
negativna konstanta korisnik mora sam paziti da se to ne dogodi!

43

Stoga ni za ivu glavu nemojte u programu za evidenciju stanja na tekuem raunu


koristiti unsigned varijable. U protivnom se lako moe dogoditi da ostanete bez
ekovne iskaznice, a svoj bijes iskalite na raunalu, ni krivom ni dunom.
4.4.2. Aritmetiki operatori
Da bismo objekte u programu mogli mijenjati, na njih treba primijeniti odgovarajue
operacije. Za ugraene tipove podataka definirani su osnovni operatori, poput zbrajanja,
oduzimanja, mnoenja i dijeljenja (vidi tablicu 4.4). Ti operatori se mogu podijeliti na
unarne, koji djeluju samo na jedan objekt, te na binarne za koje su neophodna dva

Tablica 4.4. Aritmetiki operatori


+x
-x

unarni operatori

x++
++x
x---x
x + y
x - y

binarni operatori

x * y
x / y
x % y

unarni plus
unarni minus
uveaj nakon
uveaj prije
umanji nakon
umanji prije
zbrajanje
oduzimanje
mnoenje
dijeljenje
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;
cout << i << endl;
--i;
cout << i << endl;

//
//
//
//

uvea za 1
ispisuje 1
umanji za 1
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 =
cout <<
cout <<
cout <<
cout <<
cout <<

1;
i << endl;
(++i) << endl;
i << endl;
(i++) << endl;
i << endl;

//
//
//
//
//

ispii za svaki sluaj


prvo povea, pa ispisuje 2
ispisuje opet, za svaki sluaj
prvo ispisuje, a tek onda uvea
vidi, stvarno je uveao!

Na raspolaganju imamo pet binarnih aritmetikih operatora: za zbrajanje, oduzimanje,


mnoenje, dijeljenje i modulo operator:
float a
float b
cout <<
cout <<
cout <<
cout <<

= 2.;
= 3.;
(a + b)
(a - b)
(a * b)
(a / b)

<<
<<
<<
<<

endl;
endl;
endl;
endl;

//
//
//
//

ispisuje
ispisuje
ispisuje
ispisuje

5.
-1.
6.
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

-2

zbrajanje
oduzimanje
-3
Ako postoji mogunost
pojave numerikog 3preljeva, tada deklarirajte
varijablu s veim opsegom u gornjem primjeru umjesto int uporabite
long int.

int

32766

-32767
-32768

32767

Slika 4.3. Prikaz preljeva na brojevnoj krunici

na kojoj zbrajanje odgovara kretanju po krunici u smjeru kazaljke na satu, a


oduzimanje odgovara kretanju u suprotnom smjeru (slika 4.3).
Gornja situacija moe se poistovjetiti s kupovinom automobila na sajmu rabljenih
automobila. Naravno da auto star 10 godina nije preao samo 5000 kilometara koliko
pie na brojau kilometara (kilometer-cajgeru), ve je nakon 99999 kilometara broja
ponovno krenuo od 00000. Budui da broja ima mjesta za 5 znamenki najvia
znamenka se izgubila! Sutinska razlika je jedino u tome da je kod automobila do
preljeva dolo hardverskom intervencijom prethodnog vlasnika, dok u programu do
preljeva obino dolazi zbog nepanje programera pri izradi programa.
Drugo pitanje koje se namee jest kakvog e biti tipa rezultat binarne operacije na
dva broja. Za ugraene tipove tono su odreena pravila uobiajene aritmetike
konverzije. Ako su oba operanda istog tipa, tada je i rezultat tog tipa, a ako su operandi
razliitih tipova, tada se oni prije operacije svode na zajedniki tip (to je obino sloeniji
tip), prema sljedeim pravilima:
1. Ako je jedan od operanada tipa long double, tada se i drugi operand pretvara u
long double.
2. Inae, ako je jedan od operanada tipa double, tada se i drugi operand pretvara u
double.
3. Inae, ako je jedan od operanada tipa float, tada se i drugi operand pretvara u
float.
4. Inae, se provodi cjelobrojna promocija (engl. integral promotion) oba operanda
(ovo je bitno samo za operande tipa bool, wchar_t i pobrojenja, tako da emo o
cjelobrojnoj promociji govoriti u odgovarajuim poglavljima o tim tipovima).
5. Potom, ako je jedan od operanada unsigned long, tada se i drugi operand pretvara
u unsigned long.
6. U protivnom, ako je jedan od operanada tipa long int, a drugi operand tipa
unsigned int, ako long int moe obuhvatiti sve vrijednosti unsigned int,

46

unsigned int se pretvara u long int; inae se oba operanda pretvaraju u


unsigned long int.
7. Inae, ako je jedan od operanada tipa long, tada se i drugi operand pretvara u long.
8. Inae, ako je jedan od operanada unsigned, tada se i drugi operand pretvara u
unsigned.

Na primjer, izvoenje kda


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;

Suprotno svim pravilima zapadnoeuropske klasine matematike, na zaslonu e se kao


rezultat ispisati 0., tj. najobinija nula! Koristi li vae raunalo aborianski brojevni
sustav, koji poznaje samo tri broja (jedan, dva i puno)? Iako smo rezultat dijeljenja
pridruili float varijabli, pri izvoenju programa to pridruivanje slijedi tek nakon to
je operacija dijeljenja dva cijela broja bila zavrena (ili, u duhu junoslavenske narodne
poezije, Kasno float na Dijeljenje stie!). Budui da su obje varijable, Brojnik i
Nazivnik cjelobrojne, prevoditelj provodi cjelobrojno dijeljenje u kojem se
zanemaruju decimalna mjesta. Stoga je rezultat cjelobrojni dio kvocijenta varijabli
Brojnik i Nazivnik (0.25), a to je 0. Slina je situacija i kada dijelimo cjelobrojne
konstante:
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

Da bi se izbjegle sve nedoumice ovakve vrste, dobra je programerska navada


sve konstante koje trebaju biti s pominom decimalnom tokom (float i
double) pisati s decimalnom tokom, ak i kada nemaju decimalnih mjesta:
Evo pravilnog naina da se provede eljeno dijeljenje:
float TocniKvocijent = 3. / 2.;

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.;
cout << a / 3 << endl;
cout << b / 3 << endl;

48

Zadatak. Da li e postojati razlika pri ispisu u donjim naredbama (varijable a i b


deklarirane su u prethodnom zadatku):
float c
cout <<
c = a /
cout <<
c = b *
cout <<

= a
c *
3.;
c *
a;
c /

/ 3;
b << endl;
b << endl;
3 << endl;

4.4.3. Operator dodjele tipa

to uiniti elimo li podijeliti dvije cjelobrojne varijable, a da pritom ne izgubimo


decimalna mjesta? Dodavanje decimalne toke iza imena varijable nema smisla, jer e
prevoditelj javiti pogreku. Za eksplicitnu promjenu tipa varijable valja primijeniti
operator dodjele tipa (engl. type cast, krae samo cast):
int Brojnik = 1;
int Nazivnik = 3;
float TocniKvocijent = (float)Brojnik / (float)Nazivnik;

Navoenjem kljunih rijei float u zagradama ispred operanada njihove vrijednosti se


pretvaraju u decimalne brojeve prije operacije dijeljenja, tako da je rezultat korektan. I
ovdje bi bilo dovoljno operator dodjele tipa primijeniti samo na jedan operand prema
pravilima aritmetike konverzije i drugi bi operand bio sveden na float tip. Da ne bi
bilo zabune, same varijable Brojnik i Nazivnik i nadalje ostaju tipa int, tako da e
njihovo naknadno dijeljenje
float OpetKriviKvocijent = Brojnik / Nazivnik;

opet kao rezultat dati kvocijent cjelobrojnog dijeljenja.


Jezik C++ dozvoljava i funkcijski oblik dodjele tipa u kojem se tip navodi ispred
zagrade, a ime varijable u zagradi:
float TocniKvocijent2 = float(Brojnik) / float(Nazivnik);

Iako je ovakav oblik loginiji i za poetnika prihvatljiviji, on nije primjenjiv na


pokazivae (odjeljak 4.2), tako da ne preporuujemo njegovu uporabu.
Operator dodjele tipa moe se koristiti i u obrnutom sluaju, kada elimo iz
decimalnog broja izluiti samo cjelobrojne znamenke.
Zadatak. Odredite koje e od navedenih naredbi za ispis:
int a = 10000;
int b = 10;

49

long c = a * b;
cout
cout
cout
cout
cout

<<
<<
<<
<<
<<

c << endl;
(a * b) << endl;
((float)a * b) << endl;
(long)(a * b) << endl;
(a * (long)b) << endl;

dati ispravan umnoak, tj. 100000.


Zadatak. Odredite to e se ispisati na zaslonu po izvoenju sljedeeg kda:
float a = 2.71;
float b = (int)a;
cout << b << endl;

4.4.4. Dodjeljivanje tipa brojanim konstantama

Kada se u kdu pojavljuju brojane konstante, prevoditelj ih pohranjuje u formatu


nekog od osnovnih tipova. Tako s brojevima koji sadre decimalnu toku ili slovo e,
odnosno E, prevoditelj barata kao s podacima tipa double, dok sve ostale brojeve tretira
kao int. Operatore dodjele tipa mogue primijeniti i na konstante, na primjer:
cout << (long)10 << endl;
cout << (unsigned)60000 << endl;

// long int
// unsigned int

ee se za specificiranje konstanti koriste sufiksi, posebni znakovi kojima se


eksplicitno odreuje tip brojane konstante (vidi tablicu 4.5). Tako e sufiks l, odnosno
L cjelobrojnu konstantu pretvoriti u long, a konstantu s decimalnom tokom u double:
long double a = 1.602e-4583L / 645672L;
// (long double) / long

dok e sufiksi u, odnosno U cjelobrojne konstante pretvoriti u unsigned int:


Tablica 4.5. Djelovanje sufiksa na brojane konstante

broj ispred sufiksa

sufiks

rezultirajui tip

cijeli

L, l
U, u

int
long int
unsigned int

decimalni

F, f
L, l

double
float
long double

50

cout << 65000U << endl;

// unsigned int

Sufiks f, odnosno F e konstantu s decimalnom tokom pretvoriti u float:


float b = 1.234F;

Velika i mala slova sufiksa su potpuno ravnopravna. U svakom sluaju je preglednije


koristiti veliko slovo L, da bi se izbjegla zabuna zbog slinosti slova l i broja 1.
Takoer, sufiksi L (l) i U (u) za cjelobrojne podatke mogu se meusobno kombinirati u
bilo kojem redoslijedu (LU, UL, Ul, uL, ul, lu...) rezultat e uvijek biti
unsigned long int.
Sufiksi se rijetko koriste, budui da u veini sluajeva prevoditelj obavlja sam
neophodne konverzije, prema ve spomenutim pravilima. Izuzetak je, naravno pretvorba
double u float koju provodi sufiks F.
4.4.5. Simbolike konstante

U programima se redovito koriste simbolike veliine ija se vrijednost tijekom


izvoenja ne eli mijenjati. To mogu biti fizike ili matematike konstante, ali i
parametri poput maksimalnog broja prozora ili maksimalne duljine znakovnog niza, koji
se namjetaju prije prevoenja kda i ulaze u izvedbeni kd kao konstante.
Zamislimo da za neki zadani polumjer elimo izraunati i ispisati opseg krunice,
povrinu kruga, oploje i volumen kugle. Pri raunanju sve etiri veliine treba nam
Ludolfov broj =3,14159...:
float Opseg = 2.
float Povrsina =
double Oplosje =
double Volumen =

* r * 3.14159265359;
r * r * 3.14159265359;
4. * r * r * 3.14159265359;
4. / 3. * r * r * r * 3.14159265359;

Naravno da bi jednostavnije i pouzdanije bilo definirati zasebnu varijablu koja e


sadravati broj :
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;

Manja je vjerojatnost da emo pogrijeiti prilikom utipkavanja samo jednog broja, a


osim toga, ako se (kojim sluajem, jednog dana) promijeni vrijednost broja , bit e
lake ispraviti ga kada je definiran samo na jednom mjestu, nego raditi pretraivanja i
zamjene brojeva po cijelom izvornom kdu.

51

Pretvaranjem konstantnih veliina u varijable izlaemo ih pogibelji od nenamjerne


promjene vrijednosti. Nakon izvjesnog vremena jednostavno zaboravite da dotina
varijabla predstavlja konstantu, te negdje u kdu dodate naredbu
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:
const double pi = 3.14159265359;
pi = 2 * pi;

// sada je to pogreka!

Drugi esto koriteni pristup zasniva se na pretprocesorskoj naredbi #define:


#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
double Volumen = 4. / 3. * r * r * r * PI;
PI = 2 * PI;
// pogreka

interpretirati kao da se u njima umjesto simbola PI nalazi odgovarajui broj. U drugoj


naredbi e javiti pogreku, jer se taj broj naao s lijeve strane operatora pridruivanja.
Valja napomenuti da se ove zamjene nee odraziti u izvornom kdu i on e nakon
prevoenja ostati nepromijenjen.
Na prvi pogled nema razlike izmeu pristupa oba pristupa e osigurati prijavu
pogreke pri pokuaju promjene konstante. Razlika postaje oita tek kada pokuate
program ispraviti koristei program za simboliko lociranje pogreaka (engl. debugger,
doslovni prijevod bio bi istjeriva stjenica, odnosno stjeniji terminator). Ako ste
konstantu definirali pretprocesorskom naredbom, njeno ime nee postojati u simbolikoj
tablici koju prevoditelj generira, jer je prije prevoenja svaka pojava imena
nadomjetena brojem. Ne moete ak ni provjeriti da li ste moda pogrijeili prilikom
poziva imena.
Konstante definirane pomou deklaracije const dohvatljive su i iz programa
za simboliko lociranje pogreaka.

52

Napomenimo da vrijednost simbolike konstante mora biti inicijalizirana prilikom


deklaracije. U protivnom emo dobiti poruku o pogreki:
const float mojaMalaKonstanta;

// pogreka

Prilikom inicijalizacije vrijednosti nismo ogranieni na brojeve moemo pridruiti i


vrijednost neke prethodno definirane varijable. Vrijednost koju pridruujemo konstanti
ne mora biti ak ni zapisana u kdu:
float a;
cin >> a;
const float DvijeTrecine = a;

4.4.6. Kvalifikator volatile

U prethodnom odsjeku smo deklarirali konstantne objekte ime smo ih zatitili od


neovlatenih promjena. Svi ostali objekti su promjenjivi (engl. volatile). Promjenjivost
objekta se moe naglasiti tako da se ispred tipa umetne kljuna rije volatile:
volatile int promjenjivica;

Time se prevoditelju daje na znanje da se vrijednost varijable moe promijeniti njemu


nedokuivim nainima, te da zbog toga mora iskljuiti sve optimizacije kda prilikom
pristupa.
Valja razjasniti koji su to naini promjene koji su izvan prevoditeljevog znanja. Na
primjer, ako razvijamo sistemski program, vrijednost memorijske adrese se moe
promijeniti unutar obrade prekida (engl. interrupt) time prekidna rutina moe
signalizirati programu da je odreeni uvjet zadovoljen. U tom sluaju prevoditelj ne
smije optimizirati pristup navedenoj varijabli. Na primjer:
int izadjiVan = 0;
while (!izadjiVan) {
// ekaj dok se vrijednost izadjiVan ne postavi na 1
}

U gornjem primjeru prevoditelj analizom petlje moe zakljuiti da se varijabla


izadjiVan ne mijenja unutar petlje, te e optimizirati izvoenje tako da se vrijednost
varijable izadjiVan uope ne testira. Prekidna rutina koja e eventualno postaviti
vrijednost izadjiVan na 1 zbog toga nee okonati petlju. Da bismo to sprijeili,
izadjiVan moramo deklarirati kao volatile:
volatile int izadjiVan;

53

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):
enum dani {ponedjeljak, utorak, srijeda,
cetvrtak, petak, subota, nedjelja};

Ovom deklaracijom uvodi se novi tip podataka dani, te sedam nepromjenjivih


identifikatora (ponedjeljak, utorak,...) toga tipa. Prvom identifikatoru prevoditelj
pridjeljuje vrijednost 0, drugom 1, itd. Sada moemo definirati varijablu tipa dani, te
joj pridruiti neku od vrijednosti iz niza:
dani HvalaBoguDanasJe = petak;
dani ZakajJaNeVolim = ponedjeljak;

Naredbom
cout << HvalaBoguDanasJe << endl;

na zaslonu se ispisuje cjelobrojni ekvivalent za petak, tj. broj 4.


Varijable tipa dani mogli smo deklarirati i neposredno uz definiciju pobrojanog
tipa:
enum dani{ponedjeljak, utorak, srijeda, cetvrtak,
petak, subota, nedjelja} ThankGodItIs, SunnyDay;
ThankGodItIs = petak;
SunnyDay = nedjelja;

U pobrojanim nizovima podrazumijevana vrijednost prve varijable je 0. elimo li da niz


poinje nekom drugom vrijednou, treba eksplicitno pridijeliti eljenu vrijednost:
enum dani {ponedjeljak = 1, utorak, srijeda,
cetvrtak, petak, subota, nedjelja};

Identifikator utorak poprima vrijednost 2, srijeda 3, itd. U ovom sluaju, kd


dani ZecUvijekDolaziU = nedjelja;
cout << ZecUvijekDolaziU << endl;

na zaslonu ispisuje broj 7.


Eksplicitno pridjeljivanje moe se primijeniti i na bilo koji od ostalih lanova, s
time da slijedei lan, ako njegova vrijednost nije eksplicitno definirana, ima za 1 veu
vrijednost:

54

enum likovi {kruznica = 0,


trokut = 3,
pravokutnik = 4,
kvadrat = 4,
cetverokut = 4,
peterokut,
sedmerokut = cetverokut + 3};
cout
cout
cout
cout
cout

<<
<<
<<
<<
<<

kruznica << endl;


trokut << endl;
kvadrat << endl;
peterokut << endl;
sedmerokut << endl;

//
//
//
//
//

ispisuje
ispisuje
ispisuje
ispisuje
ispisuje

0
3
4
5
7

Ako nam ne treba vie varijabli tog tipa, ime tipa iza rijei enum moe se izostaviti:
enum {NE = 0, DA} YesMyBabyNo, ShouldIStayOrShouldIGo;
enum {TRUE = 1, FALSE = 0};

Pobrojane vrijednosti mogu se koristiti u aritmetikim izrazima. U takvim sluajevima


se prvo provodi cjelobrojna promocija (engl. integral promotion) pobrojane vrijednosti
ona se pretvara u prvi mogui cjelobrojni tip naveden u slijedu: int, unsigned int,
long ili unsigned long. Na primjer:
enum {DVOSTRUKI = 2, TROSTRUKI};
int i = 5;
float pi = 3.14159;
cout << DVOSTRUKI * i << endl;
cout << TROSTRUKI * pi << endl;

// ispisuje 10
// ispisuje 9.42477

Meutim, pobrojanim tipovima ne mogu se pridruivati cjelobrojne vrijednosti, pa e


prevoditelj za sljedei primjer javiti pogreku:
dani VelikiPetak = petak;
dani DolaziZec = VelikiPetak + 2;

// pogreka

Prilikom zbrajanja pobrojeni tip VelikiPetak svodi se na tip zajedniki sa cijelim


brojem 2 (a to je int). Dobiveni rezultat tipa int treba pridruiti pobrojenom tipu, to
rezultira pogrekom prilikom prevoenja.

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

4.4.8. Logiki tipovi i operatori

Logiki podaci su takvi podaci koji mogu poprimiti samo dvije vrijednosti, na primjer:
da/ne, istina/la, dan/no. Jezik C++ za prikaz podataka logikog tipa ima ugraen tip
bool, koji moe poprimiti vrijednosti true (engl. true tono) ili false (engl. false
pogreno):
bool JeLiDanasNedjelja = true;
bool SunceSije = false;

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
!x
logika negacija
tri operatora: ! (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:
enum Logicki{NEISTINA, ISTINA, DRUGAISTINA = 124};
Logicki a = NEISTINA;

Tablica 4.7. Stanja za logiki i

tono
(1)
tono
(1)
pogreno (0)
pogreno
(0)
Rije bool dolazi
od prezimena
utemeljitelja logike algebre.

tono
pogreno
tono
pogreno
engleskog

a .i. b

(1)
tono
(1)
(0)
pogreno (0)
(1)
pogreno (0)
(0)
pogreno
matematiara
Georgea(0)Boolea (18151864),

56

Tablica 4.8. Stanja za logiki ili

tono
tono
pogreno
pogreno

(1)
(1)
(0)
(0)

tono
pogreno
tono
pogreno

a .ili. b

(1)
(0)
(1)
(0)

tono
tono
tono
pogreno

(1)
(1)
(1)
(0)

Logicki b = ISTINA;
Logicki c = DRUGAISTINA;
cout << "a = " << a << ", b = " << b
<< ", c = " << c << endl;
cout << "Suprotno od a = " << !a << endl;
cout << "Suprotno od b = " << !b << endl;
cout << "Suprotno od c = " << !c << endl;
cout << "a .i. b = " << (a && b) << endl;
cout << "a .ili. c = " << (a || c) << endl;

Unato tome da smo varijabli c pridruili vrijednost DRUGAISTINA = 124, njenom


logikom negacijom dobiva se 0, tj. logika neistina. Operacija a-logiki i-b daje
neistinu (broj 0), jer operand a ima vrijednost pogreno, dok a-logiki ili-c daje istinu,
tj. 1, jer operand c ima logiku vrijednost tono.
Logiki operatori i operacije s njima uglavnom se koriste u naredbama za grananje
toka programa, pa emo ih tamo jo detaljnije upoznati.
Budui da su logiki podaci tipa bool dosta su kasno uvrteni u standard C++
jezika, stariji prevoditelji ne podravaju taj tip podataka. elite li na starijem
prevoditelju prevesti program koji je pisan u novoj verziji jezika, tip bool moete
simulirati tako da na poetak programa dodate sljedee pobrojenje:
enum bool {false, true};

4.4.9. Poredbeni operatori

Osim aritmetikih operacija, jezik C++ omoguava i usporedbe dva broja (vidi tablicu
4.9). Kao rezultat usporedbe dobiva se tip bool: ako je uvjet usporedbe zadovoljen,
rezultat je true, a ako nije rezultat je false. Tako e se izvoenjem kda
cout
cout
cout
cout

<<
<<
<<
<<

(5
(5
(5
(5

> 4) << endl;


>= 4) << endl;
< 4) << endl;
<= 4) << endl;

//
//
//
//

je
je
je
je

li
li
li
li

5
5
5
5

vee od 4?
vee ili jednako 4?
manje od 4?
manje ili jednako 4?

57

Tablica 4.9. Poredbeni operatori


x
x
x
x
x
x

< y
<= y
> y
>= y
== y
!= y

cout << (5 == 4) << endl;


cout << (5 != 4) << endl;

manje od
manje ili jednako
vee od
vee ili jednako
jednako
razliito

// je li 5 jednako 4?
// je li 5 razliito od 4?

na zaslonu ispisati brojevi 1, 1, 0, 0, 0 i 1.


Poredbeni operatori (ponekad nazvani i relacijski operatori od engl. relational
operators) se koriste preteito u naredbama za grananje toka programa, gdje se, ovisno
o tome je li neki uvjet zadovoljen, izvoenje programa nastavlja u razliitim
smjerovima. Stoga emo poredbene operatore podrobnije upoznati kod naredbi za
grananje, kasnije u ovom poglavlju.
Uoimo sutinsku razliku izmeu jednostrukog znaka jednakosti (=) koji je
simbol za pridruivanje, te dvostrukog znaka jednakosti (==) koji je operator
za usporedbu!
to e se dogoditi ako umjesto operatora pridruivanja =, pogrekom u nekom izrazu
napiemo operator usporedbe ==? Na primjer:
int a = 3;
a == 5;

U drugoj naredbi e se umjesto promjene vrijednosti varijable a, ona usporediti s


brojem 5. Rezultat te usporedbe je false, odnosno 0, ali to ionako nema nikakvog
znaaja, jer se rezultat usporedbe u ovom sluaju ne pridruuje niti jednoj varijabli
naredba nema nikakvog efekta. A to je najgore, prevoditelj nee prijaviti pogreku,
ve moda samo upozorenje! Kod sloenijeg izraza poput
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;

Bolji prevoditelji e u gornjim primjerima prilikom prevoenja dojaviti upozorenje o


tome da kd nema efekta.
4.4.10. Znakovi

Znakovne konstante tipa char piu se uglavnom kao samo jedan znak unutar
jednostrukih znakova navodnika:
char SlovoA = 'a';
cout << 'b' << endl;

Za znakove koji se ne mogu prikazati na zaslonu koriste se posebne sekvence (engl.


escape sequence) koje poinju lijevom kosom crtom (engl. backslash) (tablica 4.10). Na
primjer, kd
cout << '\n';

// znak za novi redak

uzrokovat e pomak kurzora na poetak sljedeeg retka, tj. ekvivalentan je ispisu


konstante endl.
Tablica 4.10. Posebni znakovi
\n
\t
\v
\b
\r
\f
\a
\\
\?
\'
\"
\0
\ddd
\xddd

novi redak
horizontalni tabulator
vertikalni tabulator
pomak za mjesto unazad (backspace)
povrat na poetak retka (carriage return)
nova stranica (form feed)
zvuni signal (alert)
kosa crta ulijevo (backslash)
upitnik
jednostruki navodnik
dvostruki navodnik
zavretak znakovnog niza
znak iji je kd zadan oktalno s 1, 2 ili 3 znamenke
znak iji je kd zadan heksadekadski

Znakovne konstante najee se koriste u znakovnim nizovima (engl. strings) za ispis


tekstova, te emo ih tamo detaljnije upoznati. Za sada samo spomenimo da se znakovni
nizovi sastoje od nekoliko znakova unutar dvostrukih navodnika. Zanimljivo je da se
char konstante i varijable mogu usporeivati, poput brojeva:

59

cout
cout
cout
cout

<<
<<
<<
<<

('a' < 'b') <<


('a' < 'B') <<
('A' > 'a') <<
('\'' != '\"')

endl;
endl;
endl;
<< endl; // usporedba jednostrukog
// i dvostrukog navodnika

pri emu se u biti usporeuju njihovi brojani ekvivalenti u nekom od standarda.


Najraireniji je ASCII niz (kratica od American Standard Code for Information
Interchange) u kojem su svim ispisivim znakovima, brojevima i slovima engleskog
alfabeta pridrueni brojevi od 32 do 127, dok specijalni znakovi imaju kdove od 0 do
31 ukljuivo. U prethodnom primjeru, prva naredba ispisuje 1, jer je ASCII kd malog
slova a (97) manji od kda za malo slovo b (98). Druga naredba ispisuje 0, jer je ASCII
kd slova B jednak 66 (nije vano to je B po abecedi iza a!). Trea naredba ispisuje 0,
jer za veliko slovo A kd iznosi 65. ASCII kdovi za jednostruki navodnik i dvostruki
navodnik su 39 odnosno 34, pa zakljuite sami to e etvrta naredba ispisati.
Znakovi se mogu usporeivati sa cijelim brojevima, pri emu se oni pretvaraju u
cjelobrojni kdni ekvivalent. Naredbom
cout << (32 == ' ') << endl;

// usporedba broja 32 i
// praznine

ispisat e se broj 1, jer je ASCII kd praznine upravo 32.


tovie, na znakove se mogu primjenjivati i svi aritmetiki operatori. Budui da se
pritom znakovi cjelobrojnom promocijom pretvaraju u cijele brojeve, za njih vrijede ista
pravila kao i za operacije sa cijelim brojevima. Ilustrirajmo to sljedeim primjerom:
char a = 'h';
char b;

// ASCII kd 104

cout << (a + 1) << endl;

// ispisuje 105

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

cout << 'b' << endl;


cout << asciiKod << endl;

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:
wchar_t podebljiZnak = L'ab';

Slovo L ispred pridruene eksplicitno odreuje da je znak koji slijedi tipa wchar_t.
4.4.11. Bitovni operatori

Velika prednost jezika C++ je to omoguava izravne operacije na pojedinim bitovima


podataka. Na raspolaganju je est operatora: bitovni komplement, bitovni i, bitovni ili,
bitovni iskljuivi ili, bitovni pomak udesno i bitovni pomak ulijevo (tablica 4.11). Valja
napomenuti da su bitovni operatori definirani
samo za cjelobrojne (int, long int)
Tablica 4.11. Bitovni operatori
operande.
Operator komplementiranja ~ (tilda) je
unarni operator koji mijenja stanja pojedinih
bitova, tj. sve bitove koji su jednaki nuli
postavlja u 1, a sve bitove koji su 1 postavlja u
0. Tako e binarni broj 001001012
komplementiranjem prijei u 110110102, kako
je prikazano na slici 4.4.
B7

B6

B5

B4

B3

B2

B1

~i
i & j
i | j
i ^ j
i << n
i >> n

komplement
binarni i
binarni ili
iskljuivi ili
pomakni ulijevo
pomakni udesno

B0

1
~00100101 = 11011010

B7

B6

B5

B4

B3

B2

B1

B0

Slika 4.4. Bitovni komplement

Zbog toga e se izvoenjem kda


unsigned char a = 0x25;
unsigned char b = ~a;

// 00100101 u heksadec.prikazu
// pridrui bitovni komplement

cout << hex << (int)a << endl;


// ispisuje a u hex-formatu
cout << (int)b << endl;
// ispisuje b u hex-formatu

61

ispisati heksadekadski broj 25 i njegov bitovni komplement, heksadekadski broj da (tj.


11011010 u binarnom prikazu). U gornjem kdu uoavamo hex manipulator za izlazni
tok, kojim smo osigurali da ispis svih brojeva koji slijede bude u heksadekadskom
formatu. Manipulator hex definiran je u iostream biblioteci. Operator dodjele tipa
(int) pri ispisu je neophodan, jer bi se bez njega umjesto heksadekadskih brojeva
ispisali pripadajui ASCII znakovi.
Poneki itatelj e se sigurno upitati zato smo u gornjem primjeru varijable a i b
deklarirali kao unsigned char. Varijabla tipa char ima duljinu samo jednog bajta, tj.
zauzima 8 bitova, tako da e varijabla a nakon pridruivanja vrijednosti 0x25 u
memoriji imati zaista oblik 00100101. Prilikom komplementiranja, ona se proiruje u
int, tj. dodaje joj se jo jedan bajt nula (ako int brojevi zauzimaju dva bajta), tako da
ona postaje 00000000 00100101. Komplement tog broja je 11111111 11011010
(FFDA16), ali kako se taj rezultat pridruuje unsigned char varijabli b, vii bajt
(lijevih 8 bitova) se gubi, a preostaje samo nii bajt (DA16). Da smo varijablu b
deklarirali kao int, lijevih 8 bitova bi ostalo, te bismo na zaslonu dobili ispis
heksadekadskog broja ffda. Paljivi itatelj e odmah primijetiti da je za varijablu a
potpuno svejedno da li je deklarirana kao char ili kao int ona se ionako prije
komplementiranja pretvara u int. Nju smo deklarirali kao unsigned char samo radi
dosljednosti s varijablom b.
Operator & (bitovni i) postavlja u 1 samo one bitove koji su kod oba operanda
jednaki 1 (slika 4.5); matematikom terminologijom reeno, trai se presjek bitova
dvaju brojeva.

B7

B6

B5

B4

B3

B2

B1

00100101 & 00001111 = 00000101

B0

1
Slika 4.5. Bitovni operator i

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;
cout << hex << (a & maska) << endl;

Izvoenjem se ispisuje broj 5.


Operator | (bitovni ili) postavlja u 1 sve one bitove koji su u bilo kojem od
operanada jednaki 1 (slika 4.6); matematikim rjenikom trai se unija bitova dvaju
brojeva.

B7

B6

B5

B4

B3

B2

B1

00100101 | 00001111 = 00101111

B0

Slika 4.6. Bitovni operator ili

Bitovni ili se najee koristi za postavljanje odreenih bitova u 1. Drugi operand je


pritom maska koja mora imati 1 na onim mjestima koja elimo u broju postaviti. U
gornjem primjeru postavili smo sva etiri najnia bita lijevog broja u 1. Stoga e niz
naredbi:
int a = 0x0025;
int maska = 0x000f;
cout << hex << (a | maska) << endl;

na zaslonu ispisati heksadekadski broj 2F.


Operator ^ (iskljuivi ili, ponekad nazvan i ex-ili, engl. exclusive or) postavlja u 1
samo one bitove koji su kod oba operanda meusobno razliiti (slika 4.7). On se
uglavnom koristi kada se ele promijeniti stanja pojedinih bitova. Odgovarajua maska
mora u tom sluaju imati 1 na mjestima bitova koje elimo promijeniti:

63

B7

B6

B5

B4

B3

B2

B1

00100101 ^ 00001111 = 00101010

B0

Slika 4.7. Bitovni operator iskljuivo ili

int a = 0x0025;
int maska = 0x000f;
cout << hex << (a ^ maska) << endl;

// ispisuje 0x2a

Zanimljivo je uoiti da ponovna primjena maske vraa broj u izvornu vrijednost:


cout << hex << (a ^ maska ^ maska) << endl;
// ispisuje 0x25

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;
cout << (a >> 2) << endl;

// pomak za 1
//
ispisuje
// pomak za 2
//
ispisuje

bit ulijevo 40
bita udesno 5

64

00100101 >> 2 = 00001001


00100101 << 1 = 01001010

B7

B6

0
B7

B6

B5

B4

B3

B2

B1

B5

B4

B3

B2

B1

B4

B3

B2

B1

B0

1
B6

0
B6

B0

B7

B7

B5

B5

B4

B3

B2

B1

B0

B0

0
B7

B6

B5

B4

B3

B2

B1

B0

Slika 4.8. Bitovni pomaci ulijevo, odnosno udesno

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:
enum {Baud110 = 0, Baud150, Baud300, Baud600,
Baud1200, Baud2400, Baud4800, Baud9600};

Budui da imamo brojeve od 0 do 7, moemo ih smjestiti u tri najnia bita B0 - B2


(vidi sliku 4.9). Podatak o broju bitova po podatku pohranit emo u B3; ako se prenosi
7 bitova po podatku bit B3 je jednak 0, a ako se prenosi 8 bitova po podatku onda je 1.
Binarni broj koji ima trei bit postavljen odgovara broju 08 u heksadekadskom prikazu
(0000 10002 = 0816):
enum {Bitova7 = 0x00, Bitova8 = 0x08};

65

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:
enum {StopB1 = 0x10, StopB2 = 0x20};

Konano, podatke o paritetu emo pohraniti u dva najvia bita (B6 - B7):
enum {ParitetNjet = 0x00, ParitetNeparni = 0x40,
ParitetParni = 0x80};

Uoimo da smo sva gornja pobrojenja mogli saeti u jedno.


elimo li sada definirati cjelobrojnu varijablu SerCom u kojoj e biti sadrani svi
parametri, primijenit emo bitovni operator ili:
int SerCom = Baud1200 | Bitova8 | StopB2 | ParitetNjet;

kojim se odreeni bitovi postavljaju u stanje 1. Rezultirajui raspored bitova prikazan je


na slici 4.9.
B7

0
B7

0
B7

0
B7

B6

B5

0
B6

0
B5

0
B6

0
B5

0
B6

1
B5

bez64748
pariteta
B7

B6

B4

0
B4

0
B4

0
B4

B3

0
B3

1
B3

0
B3

B2

1
B2

0
B2

0
B2

B1

B0

0
B1

0
B0

0
B1

Bitova8 |

B0

0
B1

Baud1200 |

StopB2 |

B0

ParitetNjet

8 bitova
podataka
678

B5

B4

B3

B2

B1

B0

14243

1442443

2 stop-bita

1200 bauda

Slika 4.9. Primjer rasporeda bitovnih parametara za serijsku komunikaciju

Kako iz nekog zadanog bajta s parametrima izluiti relevantne informacije? Za to


trebamo na cijeli bajt primijeniti masku kojom emo odstraniti sve nezanimljive bitove.
Na primjer, za brzinu prijenosa vani su nam samo bitovi B0 - B3, tako da emo
bitovnim i operatorom s brojem koji u binarnom prikazu ima 1 na tim mjestima (tj. s 07
heksadekadsko), izluiti podatak o brzini (vidi sliku 4.10):

66

B7

0
B7

0
B7

B6

0
B6

0
B6

B5

0
B5

1
B5

B4

B3

0
B4

B3

0
B4

B2

B2

1
B3

B1

B1

1
B2

B0

1
B0

0
B1

00000111 &

SerCom

B0

00000100

Slika 4.10. Primjena bitovne maske i operatora i

int Speed = SerCom & 0x07;

// dobiva se 4

Prenosi li se osam bitova podataka provjerit emo sljedeom bitovnom ili operacijom:
int JeLiOsamBita = SerCom & 0x08;

// dobiva se 8

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:
int BrojStopBita = SerCom & 0x30;

// dobiva se 32

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):
BrojStopBita = BrojStopBita >> 4;

dobit emo upravo broj stop-bitova, tj. broj 2.

1
B7

B6

B5

144424443

B4

B3

B2

B1

B0

00100000 >> 4

644474448
B7

B6

B5

B4

B3

B2

B1

B0

00000010

Slika 4.11. Pomak udesno za etiri bita

67

Zadatak. Napiite kd u kojem e se int varijabla nekiBroj mnoiti s 4 pomakom


bitova ulijevo. Prije pomaka pohranite bit predznaka u cjelobrojnu varijablu
predznak, a nakon pomaka vratite predznak rezultatu! Napravite to isto i za dijeljenje
s 2.
4.4.12. Operatori pridruivanja (2 )

Osim ve obraenog operatora =, jezik C++ za aritmetike i bitovne operatore podrava


i operatore obnavljajueg pridruivanja (engl. update assignment) koji se sastoje se od
znaka odgovarajueg aritmetikog ili bitovnog operatora i znaka jednakosti. Operatori
obnavljajueg pridruivanja omoguavaju krai zapis naredbi. Na primjer, naredba
a += 5;

ekvivalentna je naredbi
a = a + 5;

Tablica 4.12. Operatori pridruivanja

+=

-=

*=

/=

%=

>>=

<<=

^=

&=

|=

U tablici 4.12 dani su svi operatori pridruivanja. Primjenu nekolicine operatora


ilustrirat emo sljedeim kdom
int n = 10;
n +=
cout
n -=
cout
n *=
cout
n %=
cout

5;
<< n
20;
<< n
-2;
<< n
3;
<< n

<< endl;
<< endl;
<< endl;
<< endl;

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

isto kao: n =
ispisuje: 15
isto kao: n =
ispisuje: -5
isto kao: n =
ispisuje: 10
isto kao: n =
ispisuje 1

n + 5
n - 20
n * (-2)
n % 3

Pri koritenju operatora obnavljajueg pridruivanja valja znati da operator


pridruivanja ima nii prioritet od svih aritmetikih i bitovnih operatora. Stoga, elimo li
naredbu
a = a - b - c;

napisati krae, neemo napisati

68

a -= b - c;

// to je zapravo a = a - (b - c)

ve kao
a -= b + c;

// a = a - (b + c)

Operator -= ima nii prioritet od ostalih aritmetikih operatora. Izraz s desne


strane je zapravo izraz u zagradi ispred znaka oduzimanja.

4.4.13. Alternativne oznake operatora

U skladu sa standardima Meunarodne organizacija za standardizaciju (ISO), specijalni


znakovi poput [, ], {, }, |, \, ~ i ^ nadomjeteni su u pojedinim europskim jezicima
nacionalnim znakovima kojih nema u engleskom alfabetu. Tako su u hrvatskoj inaici
ISO standarda (udomaenoj pod nazivom CROSCII) ti znakovi nadomjeteni slovima
, , , , , , , odnosno . Oito je da e na raunalu koje podrava samo takav
sustav znakova biti nemogue pregledno napisati ak i najjednostavniji C++ program.
Na primjer, na zaslonu bi kd nekog trivijalnog programa mogao izgledati ovako (onaj
tko deifrira kd, zasluuje bronani Velered Bjarnea Stroustrupa):
int main()
int a = 5;
char b = '0';
int c = a b;
return 0;

Budui da prevoditelj interpretira kdove pojedinih znakova, a ne njihove grafike


prezentacije na zaslonu, program e biti preveden korektno. Meutim, za ovjeka je
kd neitljiv. Osim toga, ispis programa neete moi poslati svom prijatelju u
Njemakoj, koji ima svojih briga jer umjesto vitiastih zagrada ima znakove i .
Da bi se izbjegla ogranienja ovakve vrste, ANSI komitet je predloio alternativne
oznake za operatore koji sadre problematine znakove (tablica 4.13). Uz to, za istu

Tablica 4.13. Alternativne oznake operatora

osnovna

alternativa

osnovna

alternativa

osnovna

{
}
[
]
#
##

<%
%>
<:
:>
%:
%:%:

&&
||
!
&
|
^

and
or
not
bitand
bitor
xor

~
!=
&=
|=
^=

alternativa
compl
not_eq
and_eq
or_eq
xor_eq

69

Tablica 4.14. Trigraf nizovi

trigraf

zamjena za

trigraf

zamjena za

trigraf

zamjena za

??=
??/
??'

#
\
^

??(
??)
??!

[
]
|

??<
??>
??-

{
}
~

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 DOSom 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>
int main() <%
bool prva = true;
bool druga = false;
bool treca = prva and druga;
cout << treca << endl;
cout << not treca << endl;
cout << prva or druga << endl;
return 1;
%>

4.4.14. Korisniki definirani tipovi i operatori

Osim standardno ugraenih tipova podataka koje smo u prethodnim odjeljcima


upoznali, te operatora koji su za njih definirani, u programskom jeziku C++ na
raspolaganju su izvedeni tipovi podataka kao to su polja, pokazivai i reference (njih
emo upoznati u 4. poglavlju). Meutim, ono to programski jezik C++ ini naroito
monim su klase koje omoguavaju uvoenje potpuno novih, korisniki definirani
tipova podataka. Tako programer vie nije sputan osnovnim tipovima koji su ugraeni u
jezik, ve ih moe po volji dopunjavati i definirati operacije na novostvorenim
tipovima. Detaljnije o klasama bit e govora u narednim poglavljima.
4.4.15. Deklaracija typedef

Kljuna rije typedef omoguava uvoenje novog imena za ve postojei ugraeni


ili korisniki definirani tip podataka. Na primjer, deklaracijom:
typedef float broj;

70

identifikator broj postaje sinonimom za tip float. Nakon gornje deklaracije


novopeeni identifikator tipa broj moe se ravnopravno koristiti u deklaracijama
objekata:
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;

Osnovni razlog za primjenu deklaracije typedef jesu jednostavnije promjene kda.


Pretpostavimo da smo napisali program za neki numeriki proraun tako da su nam svi
brojani podaci tipa float. Nakon nekog vremena utvrdili smo da nam float ne
zadovoljava uvijek glede tonosti, te da ponekad neke brojeve moramo deklarirati kao
double. U najgorem sluaju to znai pretraivanje kda i runu zamjenu
odgovarajuih deklaracija float u deklaracije double ili obrnuto (ako se
predomislimo).
// prije
float a,
float k,
// ...
float x,

promjene:
b;
h;
y;

// nakon promjene:
double a, b;
float k, h;
// ...
double x, y;

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:
typedef float brojevi;
brojevi a, b;
float k, h;
// ...
brojevi x, y;

elimo li sada promijeniti tipove, dovoljno je samo gornju deklaraciju typedef


zamijeniti sljedeom:
typedef double brojevi;

Sada e svi podaci tipa brojevi biti prevedeni kao double podaci, bez potrebe
daljnjih izmjena u izvornom kdu.

71

Osim u ovakvim jednostavnim sluajevima, kljuna rije typedef se esto koristi


prilikom deklaracija pokazivaa i referenci na objekte, pokazivaa na funkcije i kod
deklaracija polja. Naime, sintaksa kojom se ti tipovi opisuju moe esto biti vrlo
sloena. Zbog toga, ako esto koristimo pokaziva na neki tip programski kd moe
postati vrlo neitljiv. U tom sluaju, jednostavnije je definirati pomou kljune rijei
typedef novi tip koji oznaava esto koriteni tip pokaziva na objekt. Takav tip
postaje ravnopravan svim ostalim ugraenim tipovima, te se moe pojaviti na svim
mjestima na kojima se moe pojaviti ugraeni tip: prilikom deklaracije objekata,
specificiranja parametara funkcije, kao parametar sizeof operatoru i sl. Primjena
typedef kljune rijei na takve tipove bit e objanjena u kasnijim poglavljima.

4.5. Operator sizeof


Operator sizeof je unarni operator koji kao rezultat daje broj bajtova to ih operand
zauzima u memoriji raunala:
cout << "Duljina podataka tipa int je " << sizeof(int)
<< " bajtova" << endl;

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;
cout << sizeof(i) << endl;

// duljina float
// duljina int

Operator sizeof se moe primijeniti i na izraze, koji se u tom sluaju ne izraunavaju


ve se odreuje duljina njegova rezultata. Zbog toga e sljedee naredbe ispisati
duljine float, odnosno int rezultata:
float f;
int i;
cout << sizeof(f * i) << endl;
cout << sizeof((int)(i * f)) << endl;

// duljina float
// duljina int

Operator sizeof se moe primijeniti i na pokazivae, reference, polja, korisniki


definirane klase, strukture, unije i objekte (s kojima emo se upoznati u sljedeim
poglavljima). Ne moe se primijeniti na funkcije (na primjer, da bi se odredilo njihovo
zauzee memorije), ali se moe primijeniti na pokazivae na funkcije. U svim
sluajevima on vraa ukupnu duljinu tih objekata izraenu u bajtovima.

72

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.

4.6. Operator razdvajanja


Operator razdvajanja , (zarez) koristi se za razdvajanje izraza u naredbama. Izrazi
razdvojeni zarezom se izvode postepeno, s lijeva na desno. Tako e nakon naredbe
i = 10, i + 5;

varijabli i biti pridruena vrijednost 15. Prilikom koritenja operatora razdvajanja u


sloenijim izrazima valja biti vrlo oprezan, jer on ima najnii prioritet (vidi sljedei
odjeljak o hijerarhiji operatora). To se posebice odnosi na pozive funkcija u kojima se
zarez koristi za razdvajanje argumenata. Ovaj operator se vrlo esto koristi u sprezi s
uvjetnim operatorom (poglavlje 5.3) te za razdvajanje vie izraza u parametrima for
petlje (poglavlje 5.5).

4.7. Hijerarhija i redoslijed izvoenja operatora


U matematici postoji utvrena hijerarhija operacija prema kojoj neke operacije imaju
prednost pred drugima. Podrazumijevani slijed operacija je slijeva nadesno, ali ako se
dvije operacije razliitog prioriteta nau jedna do druge, prvo se izvodi operacija s viim
prioritetom. Na primjer u matematikom izrazu
a + b c / d
mnoenje broja b s brojem c ima prednost pred zbrajanjem s brojem a, tako da se ono
izvodi prvo. Umnoak se zatim dijeli s d i tek se tada pribraja broj a.
I u programskom jeziku C++ definirana je hijerarhija operatora. Prvenstveni razlog
tome je kompatibilnost s matematikom hijerarhijom operacija, to omoguava pisanje
raunskih izraza na gotovo identian nain kao u matematici. Stoga gornji izraz u jeziku
C++ moemo pisati kao
y = a + b * c / d;

Redoslijed izvoenja operacija e odgovarati matematiki oekivanom. Operacije se


izvode prema hijerarhiji operacija, poevi s operatorima najvieg prioriteta. Ako dva
susjedna operatora imaju isti prioritet, tada se operacije izvode prema slijedu izvoenja
operatora. U tablici 4.15 na stranici 73 dani su svi operatori svrstani po hijerarhiji od
najvieg do najnieg. Operatori s istim prioritetom smjeteni su u zajednike blokove.

73

Tablica 4.15. Hijerarhija i pridruivanje operatora

operator

znaenje

pridruivanje

::
::

globalno podruje
podruje klase

s desna na lijevo
s lijeva na desno

-> .
[]
()
++
--

izbor lana
indeksiranje
poziv funkcije
uveaj nakon
umanji nakon

s lijeva na desno
s lijeva na desno
s lijeva na desno
s lijeva na desno
s lijeva na desno

sizeof
++
-* & + - ! ~
new
delete
typeid
()

veliina objekta
uveaj prije
umanji prije
unarni operatori
stvori objekt
izbrii objekt
tip objekta
dodjela tipa
pokazivai na lan
mnoenja
zbrajanja
bitovni pomaci
poredbeni operatori
operatori jednakosti
bitovni i
bitovno iskljuivo ili
bitovni ili
logiki i
logiki ili
uvjetni izraz
pridruivanja

s desna na lijevo
s desna na lijevo
s desna na lijevo
s desna na lijevo
s desna na lijevo
s desna na lijevo
s desna na lijevo
s desna na lijevo
s lijeva na desno
s lijeva na desno
s lijeva na desno
s lijeva na desno
s lijeva na desno
s lijeva na desno
s lijeva na desno
s lijeva na desno
s lijeva na desno
s lijeva na desno
s lijeva na desno
s desna na lijevo
s desna na lijevo

razdvajanje

s lijeva na desno

->* .*
* / %
+ << >>
< > <= >=
== !=
&
^
|
&&
||
?:
= *= /= += -= &=
^= |= %= >>= <<=
,

prioritet
najvii

najnii

Striktno definiranje hijerarhije i slijeda izvoenja je neophodno i zato jer se neki


znakovi koriste za vie namjena. Tipian primjer je znak - (minus) koji se koristi kao
binarni operator za oduzimanje, kao unarni operator za promjenu predznaka, te u
operatoru za umanjivanje (--). Takva vieznanost moe biti uzrokom estih pogreaka.

74

Ilustrirajmo to sljedeim primjerom. Neka su zadana dva broja a i b; elimo od broja a


oduzeti broj b prethodno umanjen za 1. Neopreznom programeru moe se dogoditi da
umjesto
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;
}

Izvoenjem ovog programa na zaslonu e se ispisati


a = 1, b = 5, c = -3

to znai da je prevoditelj naredbu interpretirao kao


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);

prvo zbrojiti varijable b i c, a tek potom e se njihov zbroj mnoiti s varijablom a.


esto je zgodno zagrade koristiti i radi itljivosti kda kada one nisu neophodne. Na
primjer, u gore razmatranom primjeru mogli smo za svaki sluaj pisati
c = a - (--b);

75

Dobra je navada stavljati zagrade i praznine svugdje gdje postoji dvojba o


hijerarhiji operatora i njihovom pridruivanju. Time kd postaje pregledniji i
razumljiviji. Pritom valja paziti da broj lijevih zagrada u naredbi mora biti
jednak broju desnih zagrada u protivnom e prevoditelj javiti pogreku.

77

5. Naredbe za kontrolu toka programa


Tko kontrolira prolost, glasio je slogan
Stranke, kontrolira i budunost: tko kontrolira
sadanjost kontrolira prolost.
George Orwell (19031950), 1984

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 (endof-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.

5.1. Blokovi naredbi


Dijelovi programa koji se uvjetno izvode ili ije se izvoenje ponavlja grupiraju se u
blokove naredbi jednu ili vie naredbi koje su omeene parom otvorena-zatvorena
vitiasta zagrada {}. Izvana se taj blok ponaa kao jedinstvena cjelina, kao da se radi
samo o jednoj naredbi. S blokom naredbi susreli smo se ve u prvom poglavlju, kada
smo opisivali strukturu glavne funkcije main(). U prvim primjerima na stranicama 23 i
26 cijeli program sastojao se od po jednog bloka naredbi. Blokovi naredbi se redovito
piu uvueno. To uvlaenje radi se iskljuivo radi preglednosti, to je naroito vano
ako imamo blokove ugnijeene jedan unutar drugog.
Vano svojstvo blokova jest da su varijable deklarirane u bloku vidljive samo
unutar njega. Zbog toga e prevoenje kda
#include <iostream.h>
int main() {
{

// poetak bloka naredbi


77

78

int a = 1;
cout << a << endl;
}
return 0;

// lokalna varijabla u bloku


// kraj bloka naredbi

proi uredno, ali ako naredbu za ispis lokalne varijable a prebacimo izvan bloka
#include <iostream.h>
int main() {
{
int a = 1;
}
cout << a << endl;
return 0;
}

// greka, jer a vie ne postoji!

dobit emo poruku o pogreki da varijabla a koju pokuavamo ispisati ne postoji.


Ovakvo ogranienje podruja lokalne varijable (engl. scope domet, doseg)
omoguava da se unutar blokova deklariraju varijable s istim imenom kakvo je ve
upotrijebljeno izvan bloka. Lokalna varijabla e unutar bloka zakloniti istoimenu
varijablu prethodno deklariranu izvan bloka, u to se najbolje moemo uvjeriti
sljedeim primjerom
#include <iostream.h>
int main() {
int a = 5;
{
int a = 1;
cout << a << endl;
}
cout << a << endl;
return 0;
}

// ispisuje 1
// ispisuje 5

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

5.2. Grananje toka naredbom if


Naredba if omoguava uvjetno grananje toka programa ovisno o tome da li je ili nije
zadovoljen uvjet naveden iza kljune rijei if. Najjednostavniji oblik naredbe za
uvjetno grananje je:
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;

Zbog preglednosti kda i nedoumica koje mogu nastati prepravkama,


poetniku preporuujemo redovitu uporabu vitiastih zagrada i pisanje
naredbi u novom retku.
U protivnom se moe dogoditi da nakon dodavanja naredbe u blok programer zaboravi
omeiti blok zagradama i time dobije nepredviene rezultate:
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

elimo li da se ovisno u rezultatu izraza u if uvjetu izvode dva nezavisna


programska odsjeka, primijenit emo sljedei oblik uvjetnog grananja:
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
cout << "Presjecite s apscisom: ";
if (a != 0)
cout << -c / a << ", ";
else
cout << "nema, ";
// pravac je horizontalan
cout << "presjecite s ordinatom: ";
if (b != 0)
cout << -c / b << endl;
else
cout << "nema" << endl; // pravac je vertikalan
return 0;
}

Blokovi if naredbi se mogu nadovezivati:


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

Ako je logiki_izraz1 toan, izvest e se prvi blok naredbi, a zatim se izvoenje


nastavlja od prve naredbe iza zadnjeg else bloka u nizu, tj. iza bloka
zadnji_blok_naredbi. Ako logiki_izraz1 nije toan, izraunava se
logiki_izraz2 i ovisno o njegovoj vrijednosti izvodi se drugi_blok_naredbi, ili
se program nastavlja iza njega. Ilustrirajmo to primjerom u kojem traimo realne
korijene kvadratne jednadbe:
#include <iostream.h>
int main() {
float a, b, c;
cout << "Unesi koeficijente kvadratne jednadbe:"
<< endl;
cout << "a = ";
cin >> a;
cout << "b = ";
cin >> b;
cout << "c = ";
cin >> c;
float diskr = b * b - 4. * a * c;

// diskriminanta

cout << "Jednadba ima ";


if (diskr == 0)
cout << "dvostruki realni korijen." << endl;
else if (diskr > 0)
cout << "dva realna korijena." << endl;
else
cout << "dva kompleksna korijena." << endl;
return 0;
}

Blokovi if naredbi mogu se ugnjeivati jedan unutar drugoga. Ilustrirajmo to


primjerom u kojem gornji kd poopujemo i na sluajeve kada je koeficijent a
kvadratne jednadbe jednak nuli, tj. kada se kvadratna jednadba svodi na linearnu
jednadbu:
#include <iostream.h>
int main() {
float a, b, c;
cout << "Unesi koeficijente kvadratne jednadzbe:"
<< endl;

82

cout << "a = ";


cin >> a;
cout << "b = ";
cin >> b;
cout << "c = ";
cin >> c;
if (a) {
float diskr = b * b - 4. * a * c;
cout << "Jednadba ima ";
if (diskr == 0)
cout << "dvostruki realni korijen." << endl;
else if (diskr > 0)
cout << "dva realna korijena." << endl;
else
cout << "kompleksne korijene." << endl;
}
else
cout << "Jednadba je linearna." << endl;
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
if (-10 < b < 0) //...

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.
if ((-10 < b) < 0) //...

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:
if (-10 < b && b < 0) //...

83

Druga nezgoda koja se moe dogoditi jest da se umjesto operatora za usporedbu ==, u
logikom izrazu napie operator pridruivanja =. Na primjer:
if (k = 0)
k++;
else
k = 0;

// pridruivanje, a ne usporedba!!!

Umjesto da se varijabla k usporeuje s nulom, njoj se u logikom izrazu pridjeljuje


vrijednost 0. Rezultat logikog izraza jednak je vrijednosti varijable k (0 odnosno
false), tako da se prvi blok naredbi nikada ne izvodi. Bolji prevoditelj e na takvom
mjestu korisniku prilikom prevoenja dojaviti upozorenje.

5.3. Uvjetni operator ? :


Iako ne spada meu naredbe za kontrolu toka programa, uvjetni operator po strukturi je
slian if-else bloku, tako da ga je zgodno upravo na ovom mjestu predstaviti.
Sintaksa operatora uvjetnog pridruivanja je:
uvjet ? izraz1 : izraz2 ;

Ako izraz uvjet daje logiku istinu, izraunava se izraz1, a u protivnom izraz2. U
primjeru
x = (x < 0) ? -x : x;

// x = abs(x)

ako je x negativan, izraunava se prvi izraz, te se varijabli x na lijevoj strani znaka


jednakosti pridruuje njegova pozitivna vrijednost, tj. varijabla x mijenja svoj predznak.
Naprotiv, ako je x pozitivan, tada se izraunava drugi izraz i varijabli x na lijevoj strani
pridruuje njegova nepromijenjena vrijednost.
Izraz u uvjetu mora kao rezultat vraati aritmetiki tip ili pokaziva. Alternativni
izrazi desno od znaka upitnika moraju davati rezultat istog tipa ili se moraju dati svesti
na isti tip preko ugraenih pravila konverzije.
Uvjetni operator koristite samo za jednostavna ispitivanja kada naredba stane
u jednu liniju. U protivnom kd postaje nepregledan.
Koliko itatelja odmah shvaa da u sljedeem primjeru zapravo raunamo korijene
kvadratne jednadbe?
((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

5.4. Grananje toka naredbom switch


Kada izraz uvjeta daje vie razliitih rezultata, a za svaki od njih treba provesti razliite
odsjeke programa, tada je umjesto if grananja esto preglednije koristiti switch
grananje. Kod tog grananja se prvo izraunava neki izraz koji daje cjelobrojni rezultat.
Ovisno o tom rezultatu, tok programa se preusmjerava na neku od grana unutar switch
bloka naredbi. Openita sintaksa switch grananja izgleda ovako:
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
}

Prvo se izraunava cjelobrojni_izraz, koji mora davati cjelobrojni rezultat. Ako je


rezultat tog izraza jednak nekom od konstantnih izraza u case uvjetima, tada se izvode
sve naredbe koje slijede pripadajui case uvjet sve do prve break naredbe. Nailaskom
na break naredbu, izvoenje kda u switch bloku se prekida i nastavlja se od prve
naredbe iza switch bloka. Ako izraz daje rezultat koji nije naveden niti u jednom od
case uvjeta, tada se izvodi blok naredbi iza kljune rijei default. Razmotrimo
tokove programa za sve mogue sluajeve u gornjem primjeru. Ako
cjelobrojni_izraz kao rezultat daje:
konstantan_izraz1, tada e se prvo izvesti prvi_blok_naredbi, a zatim
drugi_blok_naredbi. Nailaskom na naredbu break prekida se izvoenje naredbi
u switch bloku. Program iskae iz bloka i nastavlja od prve naredbe iza bloka.
konstantan_izraz2, izvodi se drugi_blok_naredbi. Nailaenjem na naredbu
break prekida se izvoenje naredbi u switch bloku i program nastavlja od prve
naredbe iza bloka.
konstantan_izraz3 ili konstantan_izraz4 izvodi se trei_blok_naredbi.
Naredbom break prekida se izvoenje naredbi u switch bloku i program nastavlja
od prve naredbe iza bloka.
Ako rezultat nije niti jedan od navedenih konstantnih_izraza, izvodi se
etvrti_blok_naredbi iza default naredbe.

Evo i konkretnog primjera switch grananja (algoritam je prepisan iz prirunika za


jedan stari programirljivi kalkulator):

85

#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;
}

Algoritam se zasniva na cjelobrojnim dijeljenjima, tako da sve varijable treba deklarirati


kao cjelobrojne. tovie, godina se mora definirati kao long int, jer se ona u raunu

86

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)
/*...*/

(int)(0.4 * mjesec + 2.3)

/*...*/

kojim se rezultat mnoenja i zbrajanja brojeva s pominim zarezom pretvara u cijeli


broj, tj. odbacuju decimalna mjesta. U switch naredbi se rezultat prethodnih rauna
normira na neki od sedam dana u tjednu pomou operatora % (modulo).
Blok default se smije izostaviti, ali ga je redovito zgodno imati da bi kroz njega
program proao za vrijednosti koje nisu obuhvaene case blokovima. Ovo je naroito
vano tijekom razvijanja programa, kada se u default blok moe staviti naredba koja
e ispisivati upozorenje da je cjelobrojni_izraz u switch poprimio neku
nepredvienu vrijednost.

5.5. Petlja for


esto u programima treba ponavljati dijelove kda. Ako je broj ponavljanja poznat prije
ulaska u petlju, najprikladnije je koristiti for petlju. To je najopenitija vrsta petlje i
ima sljedei oblik:
for ( poetni_izraz ; uvjet_izvoenja ; izraz_prirasta )
// blok_naredbi

Postupak izvoenja for-bloka je sljedei:


1. Izraunava se poetni_izraz. Najee je to pridruivanje poetne vrijednosti
brojau kojim e se kontrolirati ponavljanje petlje.
2. Izraunava se uvjet_izvoenja, izraz iji rezultat mora biti tipa bool. Ako je
rezultat jednak logikoj neistini, preskae se blok_naredbi i program se nastavlja
prvom naredbom iza bloka.
3. Ako je uvjet_izvoenja jednak logikoj istini, izvodi se blok_naredbi.
4. Na kraju se izraunava izraz_prirasta (npr. poveavanje brojaa petlje).
Program se vraa na poetak petlje, te se ona ponavlja od toke 2.
Programski odsjeak se ponavlja sve dok uvjet_izvoenja na poetku petlje daje
logiku istinu; kada rezultat tog izraza postane logika neistina, programska petlja se
prekida.
Kao primjer za for petlju, napiimo program za raunanje faktorijela (faktorijela
od n je umnoak svih brojeva od 1 do n):
n! = 1 2 (n 2) (n 1) n

Pogledajmo kd:

87

#include <iostream.h>
int main() {
int n;
cout << "Upii prirodni broj: ";
cin >> n;

// manji od 13!

long int fjel = 1;


for (int i = 2; i <= n; i++)
fjel *= i;
cout << n << "! = " << fjel;
return 0;
}

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:
for (int i = n; i > 1; i--)
fjel *= i;

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:
// ovaj primjer pokreete na vlastitu odgovornost!
cout << "Beskonana petlja";
for (int i = 5; i > 1; )
cout << "a";

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

Uoimo da smo u gornjem kdu izostavili izraz_prirasta. Openito, moe se


izostaviti bilo koji od tri izraza u for naredbi jedino su oba znaka ; obavezna.
Izostavi li se uvjet_izvoenja, podrazumijevana vrijednost e biti true
i petlja e biti beskonana!
tovie, mogu se izostaviti i sva tri izraza:
for ( ; ; )

// opet beskonana petlja!

ali itatelju preputamo da sam zakljui koliko je to smisleno.


Koristan savjet (ali ne apsolutno pravilo) je izbjegavati mijenjanje vrijednosti
kontrolne varijable unutar bloka naredbi for petlje. Sve njene promjene
bolje je definirati iskljuivo u izrazu prirasta.
U protivnom se lako moe dogoditi da petlja postane beskonana, poput sljedeeg
primjera:
for (int i = 0; i < 5; i++)
i--;
// opet beskonana petlja!

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++) ;
cout << n << "! = " << fjel;
return 0;
}

U poetnom izrazu postavljaju se broja i varijabla fjel na svoje inicijalne vrijednosti.


Naredba za mnoenje s brojaem prebaena je iz bloka naredbi u izraz prirasta, tako da

89

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:
for (int i = 2, long fjel = 1; i <= n; fjel *= i, i++) ;

prevoditelj bi javio da je deklaracija varijable i okonana nepravilno, jer bi iza zareza,


umjesto imena varijable naiao na kljunu rije long.
Ako for naredba sadri deklaraciju varijable, tada se podruje te varijable prostire
samo do kraja petlje . Na primjer:
#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;
}

Nakon prevoenja i pokretanja programa, na zaslonu e se ispisati ve pomalo


zaboravljena, ali generacijama pukokolaca omraena tablica mnoenja do 10:
1
2
3
4
5

2
4
6
8
10

3
6
9
12
15

4
8
12
16
20

5
10
15
20
25

6
12
18
24
30

7
14
21
28
35

8
16
24
32
40

9
18
27
36
45

10
20
30
40
50

Tijekom razvoja standarda to pravilo se mijenjalo. Tako e neki stariji prevoditelji ostaviti
varijablu i ivom i iza for petlje.

90

6
7
8
9
10

12
14
16
18
20

18
21
24
27
30

24
28
32
36
40

30
35
40
45
50

36
42
48
54
60

42
49
56
63
70

48
56
64
72
80

54
63
72
81
90

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.

5.6. Naredba while


Druga od tri petlje kojima jezik C++ raspolae jest while petlja. Ona se koristi
uglavnom za ponavljanje segmenta kda kod kojeg broj ponavljanja nije unaprijed
poznat. Sintaksa while bloka je
while ( uvjet_izvoenja )
// blok_naredbi
uvjet_izvoenja je izraz iji je rezultat tipa bool. Tok izvoenja for-bloka je

sljedei:
1. Izraunava se logiki izraz uvjet_izvoenja.
2. Ako je rezultat jednak logikoj neistini, preskae se blok_naredbi i program se
nastavlja od prve naredbe iz bloka.
3. Ako je uvjet_izvoenja jednak logikoj istini izvodi se blok_naredbi. Potom
se program vraa na while naredbu i izvodi od toke 1.
Konkretnu primjenu while bloka dat emo programom kojim se ispisuje sadraj
datoteke s brojevima. U donjem kdu je to datoteka brojevi.dat, ali uz promjene
odgovarajueg imena, to moe biti i neka druga datoteka.
#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;
}

Brojevi u datoteci brojevi.dat moraju biti upisani u tekstovnom obliku, razdvojeni


prazninama, tabulatorima ili napisani u zasebnim recima (moemo ih upisati pomou
najjednostavnijeg programa za upis teksta te ih pohraniti na disk).
Na poetku programa se stvara objekt ulTok tipa (klase) ifstream. Iako e klase
biti detaljnije objanjene kasnije, za sada je dovoljno rei da je objekt ulTok slian
ulaznom toku cin kojeg smo do sada koristili za unos podataka s tipkovnice. Osnovna
razlika je u tome to je cin povezan na tipkovnicu, dok se ulTok vee za datoteku iji
je naziv zadan u dvostrukim navodnicima u zagradama. elimo li koristiti tokove u
naem programu, potrebno je ukljuiti datoteku zaglavlja fstream.h pomou
pretprocesorske naredbe #include.
Usredotoimo se na blok while naredbe. Na poetku while-petlje, u uvjetu
izvoenja se naredbom
ulTok >> broj

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

cout << "Upii prirodni broj: ";


cin >> n;
long int fjel = 1;
int i = 2;
while (i <= n) {
fjel *= i;
i++;
}
cout << n << "! = " << fjel;
return 0;
}

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.

5.7. Blok do-while


Zajedniko for i while naredbi jest ispitivanje uvjeta izvoenja prije izvoenja naredbi
bloka. Zbog toga se moe dogoditi da se blok naredbi ne izvede niti jednom. Meutim,
esto je neophodno da se prvo izvede neka operacija te da se, ovisno o njenom ishodu,
ta operacija eventualno ponavlja. Za ovakve sluajeve svrsishodnija je do-while petlja:
do
// blok_naredbi
while ( uvjet_ponavljanja );

Primjenu do-while bloka ilustrirat emo programom-igricom u kojem treba pogoditi


sluajno generirani trazeniBroj. Nakon svakog naeg pokuaja ispisuje se samo
poruka da li je broj vei ili manji od traenog. Petlja se ponavlja sve dok je pokuaj
(mojBroj) razliit od traenog broja.
#include <iostream.h>
#include <stdlib.h>
int main() {
int raspon = 100;
randomize();// inicijalizira generator slu. br.
// generira sluajni broj izmeu 1 i raspon

93

int trazeniBroj = (float)rand() / RAND_MAX


* (raspon - 1) + 1;
cout << "Treba pogoditi broj izmeu 1 i "
<< raspon << endl;
int mojBroj;
int brojPokusa = 0;
do {
cout << ++brojPokusa << ". pokuaj: ";
cin >> mojBroj;
if (mojBroj > trazeniBroj)
cout << "MANJE!" << endl;
else if (mojBroj < trazeniBroj)
cout << "VIE!" << endl;
} while(mojBroj != trazeniBroj);
cout << "BINGO!!!" << endl;
return 0;
}

Za generiranje sluajnih brojeva upotrijebljena je funkcija rand() iz zaglavlja


stdlib.h. Ona kao rezultat vraa sluajni broj izmeu 0 i RAND_MAX, pri emu je
RAND_MAX broj takoer definiran u stdlib biblioteci. Da bismo generirali broj u
rasponu izmeu 1 i raspon, broj koji vraa funkcija rand() podijelili smo s
RAND_MAX (ime smo normirali broj na interval od 0 do 1), pomnoili s raspon - 1 i
uveali za 1. Prilikom dijeljenja primijenili smo operator dodjele tipa (float), jer bi
se u protivnom izgubila decimalna mjesta i rezultat bi bili samo brojevi 0 ili 1.
Uzastopni pozivi funkcije rand() uvijek generiraju isti slijed sluajnih brojeva, a
prvi poziv te funkcije daje uvijek isti rezultat. Da bi se izbjegle katastrofalne posljedice
takvog svojstva na neizvjesnost igre, prije poziva funkcije rand() valja pozvati
funkciju randomize() koja inicijalizira klicu generatora sluajnog broja.
Zadatak. Napiite program u kojem se rauna priblina vrijednost funkcije sinus
pomou reda potencija

x 2i +1

x3

x5

x7

x9

(1)i (2i + 1)! = x 3! + 5! 7! + 9!


i =0

Petlju za raunanje lanova reda treba ponavljati sve dok je apsolutna vrijednost
zadnjeg izraunatog lana reda vea od nekog zadanog broja (npr. 107).

5.8. Naredbe break i continue


Naredba break moe se koristiti samo u petljama te u switch grananjima. Njenu
funkciju u switch grananjima upoznali smo u poglavlju 0, dok se u petljama njome
prekida izvoenje okolne for, while ili do-while petlje. Na primjer, elimo da se

94

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;
}
// ...

Nailaskom na znak broj 0 program iskae iz while-petlje te se prekida daljnje itanje


datoteke i ispis njena sadraja.
Naredba continue takoer uzrokuje skok programa na kraj petlje, ali se potom
njeno ponavljanje nastavlja. Na primjer, elimo li da na program za ispis sadraja
datoteke ispisuje samo one znakove koji se mogu prikazati na zaslonu, jedna mogua
varijanta bila bi:
// ...
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.
Valja izbjegavati esto koritenje break i continue naredbi u petljama, jer
one naruavaju strukturiranost programa.
Koristite ih samo za izvanredna stanja, kada ne postoji drugi prikladan nain da se
izvoenje naredbi u petlji prekine. Naredba continue redovito se moe izbjei ifblokom.

5.9. Ostale naredbe za skok


Naredba goto omoguava bezuvjetni skok na neku drugu naredbu unutar iste funkcije.
Opi oblik je:

95

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.
U pravilno strukturiranom programu naredba goto uope nije potrebna, te ju
velika veina programera uope ne koristi.
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.

5.10. O strukturiranju izvornog kda


Uvlaenje blokova naredbi i pravilan raspored vitiastih zagrada doprinose preglednosti
i itljivosti izvornog kda. Programeri koji tek zapoinju pisati u programskim jezicima
C ili C++ esto se nau u nedoumici koji stil strukturiranja koristiti. Obino preuzimaju
stil knjige iz koje pretipkavaju svoje prve programe ili od kolege preko ijeg ramena
kriomice stjeu prve programerske vjetine, ne sagledavajui nedostatke i prednosti tog
ili nekog drugog pristupa. Nakon to im taj stil ue u krv, teko e prijei na drugi
bez obzira koliko je on bolji (u to su se uvjerili i sami autori knjige tijekom njena
pisanja!).
Prvi problem jest raspored vitiastih zagrada koje oznaavaju poetak i kraj
blokova naredbi. Navedimo nekoliko najeih pristupa (redoslijed navoenja je
sluajan):
1. vitiaste zagrade uvuene i meusobno poravnate:
for ( /*...*/ )
{
// blok naredbi
// ...
}

96

2. poetna zagrada izvuena, zavrna uvuena:


for ( /*...*/ )
{
// blok naredbi
// ...
}

3. zagrade izvuene, meusobno poravnate:


for ( /*...*/ )
{
// blok naredbi
// ...
}

4. poetna zagrada na kraju naredbe za kontrolu, zavrna izvuena:


for ( /*...*/ ) {
// blok naredbi
// ...
}

Pristupi 2. i 3. imaju jo podvarijante u kojima se redak s poetnom zagradom ostavlja


prazan, bez naredbe. Meutim, taj prazan redak ne doprinosi bitno preglednosti i
nepotrebno zauzima prostor. Ista zamjerka moe se uputiti prvom pristupu.
Ve letiminim pogledom na etiri gornja pristupa itatelj e uoiti da izvuena
zavrna zgrada u 3. odnosno 4. pristupu bolje istie kraj bloka. U 3. pristupu zagrade u
paru otvorena-zatvorena vitiasta zagrada meusobno su poravnate, tako da je svakoj
zagradi lako uoiti njenog partnera, to moe biti vrlo korisno prilikom ispravljanja
programa. Meutim, u gotovo svim knjigama koristi se pristup 4 kod kojeg je poetna
vitiasta zagrada smjetena na kraj naredbe za kontrolu toka. Budui da ona zapoinje
blok te je vezana uz njega, ovaj pristup je logian. U ostalim pristupima povezanost
bloka naredbi s naredbom za kontrolu toka nije vizualno tako oita i moe se stei
dojam da blok naredbi predstavlja potpuno samostalnu cjelinu. Zbog navedenih razloga
(Kud svi Turci, tuda i mali Mi), u knjizi koristimo 4. pristup. Uostalom, pronalaenja
para, odnosno provjera uparenosti vitiastih zagrada u urednicima teksta (editorima)
koji se koriste za pisanje i ispravljanje izvornog kda ne predstavlja problem, jer veina
njih ima za to ugraene funkcije.
Druga nedoumica jest koliko duboko uvlaiti blokove. Za uvlaenje blokova
najprikladnije je koristiti tabulatore. Obino editori imaju poetno ugraeni pomak
tabulatora od po 8 znakova, meutim za iole sloeniji program s vie blokova
ugnijeenih jedan unutar drugoga, to je previe. Najee se za izvorni kd koristi
uvlaenje po 4 ili samo po 2 znaka. Uvlaenje po 4 znaka je dovoljno duboko da bi se i
poetnik mogao lagano snalaziti u kdu, pa smo ga zato i mi koristimo u knjizi.
Iskusnijem korisniku dovoljno je uvlaenje i po samo 2 znaka, to ostavlja dovoljno
prostora za dugake naredbe. Naravno da kod definiranja tabulatora treba voditi rauna
o tipu znakova (fontu) koje se koristi u editoru. Za pravilnu strukturiranost kda

97

neophodno je koristiti neproporcionalno pismo kod kojeg je irina svih znakova


jednaka.
Trei estetski detalj vezan je uz praznine oko operatora. Poetniku u svakom
sluaju preporuujemo umetanje praznina oko binarnih operatora, ispred prefiksoperatora i iza postfiks operatora, te umetanje zagrada kada je god u nedoumici oko
hijerarhije operatora. U protivnom osim estetskih, moete imati i sintaktikih problema.
Uostalom, neka sam itatelj procijeni koji je kd je itljiviji:
a = b * c - d / (2.31 + e) + e / 8.21e-12 * 2.43;

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;

5.11. Kutak za budue C++ gurue


Jedna od odlika programskog jezika C++ jest mogunost saetog pisanja naredbi.
Podsjetimo se samo naredbe za inkrementiranje koja umjesto
i = i + 1;

omoguava jednostavno napisati


i++;

Takoer, mogunost viekratne uporabe operatora pridruivanja u istoj naredbi,


dozvoljava da se primjerice umjesto dviju naredbi
a = b + 25.6;
c = e * a - 12;

napie samo jedna


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

izvedbenog kda. tovie, mnoge naredbe jezika C++ (poput naredbe za


inkrementiranje) vrlo su bliske strojnim instrukcijama mikroprocesora, pa se njihovim
prevoenjem dobiva maksimalno efikasan kd. Pri saimanju kda posebno valja paziti
na hijerarhiju operatora (vidi tablicu 4.15). esto se zaboravlja da logiki operatori
imaju nii prioritet od poredbenih operatora, a da operatori pridruivanja imaju najnii
prioritet. Zbog toga nakon naredbe
c = 4 * (a = b - 5);

varijabla a nee imati istu vrijednost kao nakon naredbe


c = 4 * ((a = b) - 5);

ili nakon naredbi


a = b;
c = 4 * (b - 5);

U prvom primjeru e se prvo izraunati b - 5 te e rezultat dodijeliti varijabli a, to je


oito razliito od drugog, odnosno treeg primjera gdje se prvo varijabli a dodijeli
vrijednost od b, a zatim se provede oduzimanje.
Kod C-gurua su uobiajena saimanja u kojima se umjesto eksplicitne usporedbe
s nulom, kao na primjer
if (a != 0) {
// ...
}

pie implicitna usporedba


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

dok bi nerijetki gurui to krae napisali kao


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;
}

Odmah uoavamo da naredbu za zbrajanje unutar petlje moemo napisati krae:


// ver. 2.0
//...
for(int i = 1; i <= 100; i++) {
zbroj += i;
//...

tovie, zbrajanje moemo ubaciti u uvjet za poveavanje kontrolne varijable for


petlje:
// 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) ;
//...

Efekt je potpuno isti u izrazu prirasta for-naredbe izrazi odvojeni znakom ,


(zarezom) se izraunavaju postupno, slijeva na desno. Naravno, da su izrazi napisani
obrnutim redoslijedom
for (int i = 1; i <= 100; i++, zbroj += 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

for (int i = 1; i <= 100; zbroj += i++) ;


//...

vie nije svejedno koristi li se postfiks- ili prefiks-operator inkrementiranja, tj. da li se


prvo dohvaa vrijednost brojaa, dodaje zbroju i zatim poveava broja, ili se prvo
poveava broja, a tek potom dohvaa njegova vrijednost:
for (int i = 1; i <= 100; zbroj += ++i)

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:
Ne troite previe energije na ovakva saimanja, jer e najvjerojatnije
rezultat biti neproporcionalan uloenom trudu, a izvorni kd postati
neitljiv(iji).

6. Polja, pokazivai, reference


Let me take you down,
cos Im going to Strawberry Fields.
Nothing is real and nothing to get hungabout.
Strawberry Fields forever.
John Lennon, Paul McCartney,
Strawberry Fields forever (1967)

U ovom poglavlju upoznat emo se s izvedenim oblicima podataka. Prvo e biti


opisana polja, koja predstavljaju skupove istovjetnih podataka. Ona su u informatiku
preuzeta iz matematike i omoguavaju pristupanje veem broju podataka preko istom
simbolikog imena i indeksa koji pokazuje redni broj elementa.
U drugom dijelu ovog poglavlja bit e analizirani pokazivai i reference. To su
tipovi koji omoguavaju posredno pristupanje podacima pomou posebnog mehanizma.
Takvi tipovi nadovezuju se na adresiranje u strojnom jeziku, gdje oni ine vaan
mehanizam za pristup podacima u memoriji raunala.

Strawberry Fields je ime psihijatrijske ustanove u Velikoj Britaniji.

102

6.1. Polja podataka


esto se u programima koriste skupovi istovjetnih podataka. Za oznaavanje i rukovanje
takvim istovjetnim podacima bilo bi vrlo nespretno koristiti za svaki pojedini podatak
zasebnu varijablu, primjerice x1, x2, x3, itd. Zamislimo samo da trebamo rezultate
nekog mjerenja koji sadre 100 podataka statistiki obraditi, pri emu svaki podatak
treba proi kroz isti niz raunskih operacija. Ako bismo koristili zasebna imena za svaki
podatak, program bi se morao sastojati od 100 istovjetnih blokova naredbi koji bi se
razlikovali samo u imenu varijable koja se u dotinom bloku obrauje. Neefikasnost
ovakvog pristupa je vie nego oita. Daleko efikasnije je sve podatke smjestiti pod isto
nazivlje, a pojedini rezultat mjerenja dohvaati pomou brojanog indeksa. Ovakvu
obradu podataka omoguavaju polja podataka (krae polja, engl. arrays).
Polje podataka je niz konanog broja istovrsnih podataka lanova polja. Ti podaci
mogu biti bilo kojeg tipa, ugraenog (primjerice float ili int) ili korisniki
definiranog. Pojedini lanovi polja mogu se dohvaati pomou cjelobrojnih indeksa te
mijenjati neovisno o ostalim lanovima polja.
6.1.1. Jednodimenzionalna polja

Najjednostavniji oblik polja su jednodimenzionalna polja kod kojih se lanovi


dohvaaju preko samo jednog indeksa. lanovi polja sloeni su u linearnom slijedu, a
indeks pojedinog lana odgovara njegovoj udaljenosti od poetnog lana.
elimo li deklarirati jednodimenzionalno polje x koje e sadravati pet decimalnih
brojeva tipa float, napisat emo
float x[5];

Prevoditelj e ovom deklaracijom osigurati kontinuirani prostor u memoriji za pet


podataka tipa float (slika 6.1). Ovakvom deklaracijom lanovi polja nisu

float[5]

x[0]:

x[1]:

x[2]:

x[3]:

x[4]:

float

float

float

float

float

x
14243

4 bajta

144444444424444444443
20 bajtova

Slika 6.1. Deklaracija polja

inicijalizirani, tako da imaju sluajne vrijednosti, ovisno o tome to se nalazilo u dijelu


memorije koji je dodijeljen (alociran) polju. lanovi polja se mogu inicijalizirati
prilikom deklaracije:

103

float x[] = {1342.5, 4.7, 23.4, 12., -7.2e12};

Ovom naredbom se deklarira jednodimenzionalno polje


pridjeljuju poetne vrijednosti. Vrijednosti se navode
odvajaju zarezima. Iako duljina polja nije eksplicitno
inicijalizacijske liste sam zakljuiti da je polje duljine
memorijski prostor (slika 6.2).

float[5]

x, te se lanovima tog polja

unutar vitiastih zagrada i


navedena, prevoditelj e iz
5 i rezervirati odgovarajui

x[0]:

x[1]:

x[2]:

x[3]:

x[4]:

float

float

float

float

float

1342.5

4.7

23.4

12.

-7.2e12

Slika 6.2. Jednodimenzionalno polje s inicijaliziranim lanovima

Navede li se ipak kod inicijalizacije duljina polja, treba paziti da ona bude vea ili
jednaka broju inicijalizirajuih lanova:
float x[5] = {1342.5, 4.7, 23.4, 12., -7.2e12};

U protivnom e prevoditelj javiti pogreku:


int a[2] = {10, 20, 30, 40};

// pogreka: previe
// inicijalizatora!

Broj inicijalizatora u listi smije biti manji od duljine polja:


int b[10] = {1, 2};

Tada lanovi brojanog polja kojima nedostaju inicijalizatori postaju jednaki nuli. Nije
dozvoljeno pridruiti praznu inicijalizacijsku listu polju koje nema definiranu duljinu:
float z[] = {};

// pogreka: kolika je duljina polja?

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.
Ako se deklarira polje s imenom koje je ve iskoriteno za neku drugu varijablu,
prevoditelj e dojaviti pogreku da je varijabla s dotinim imenom deklarirana
viekratno. Na primjer:
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:
float a = x[0];
//pridruuje vrijednost prvog lana
x[2] = x[0] + x[1]; //trei lan postaje jednak zbroju
// prvog i drugog lana
x[3]++;
//uvea etvrti lan za 1

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

pri emu je Li(x) Lagrangeov koeficijent:


n

Li ( x ) =
j =0
j i

x xj
xi x j

( x x0 )( x x1 ) ( x xi 1 )( x xi +1 ) ( x xn )
( xi x0 )( xi x1 ) ( xi xi 1 )( xi xi +1 ) ( xi xn )

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[]:

xn , fn
x3 , f3

x1 , f1

x0 , f0

x 2 , f2
x

Slika 6.3. Lagrangeova interpolacija

105

#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...

Na samom poetku glavne funkcije deklarira se simbolika konstanta nmax kojom se


dimenzioniraju polja x[] i y[] za pohranu uitanih parova podataka.
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.

Valja primijetiti da je ulazniTok objekt, a ne cijeli broj te se on ne moe direktno


usporeivati s nulom. No postoji operator konverzije koji to omoguava i koji vraa nulu ako
je datoteka proitana do kraja. Operatori konverzije e biti kasnije objanjeni.

106

Slijedi unos broja x0 za koji se trai vrijednost funkcije y0, te raunanje


koeficijenata i traene vrijednosti:
// nastavak prethodnog kda:
int n = i;
float x0, y0 = 0.;
cout << "Upii broj: ";
cin >> x0;
for (i = 0; i < n; i++) {
float Li = 1.;
for (int j = 0; j < n; j++) {
if (i != j) {
Li *= (x0 - x[j]);
Li /= (x[i] - x[j]);
}
}
y0 += (Li * y[i]);
}
cout << "f(" << x0 << ") = " << y0 << endl;
return 0;
}

Petlja za raunanje koeficijenata sastoji se od dvije for-petlje, jedne ugnijeene unutar


druge. Vanjska petlja prolazi po indeksu i od prvog do zadnjeg uitanog podatka
raunajui sumu u prvoj formuli, a unutarnja petlja za svaki i prolazi po indeksu j i
rauna produkt iz druge formule. Pri tome varijabla Li sadri vrijednost i-tog
Lagrangeovog koeficijenta.
Za provjeru, pretpostavimo da datoteka podaci.dat sadri tablicu kvadratnih
korijena nekoliko brojeva od 0 do 9:
0.00
0.49
1.00
1.44
2.25
2.89
4.00
6.25
9.00

0.0
0.7
1.0
1.2
1.5
1.7
2.0
2.5
3.0

Pokretanjem programa, utipkamo li broj 2, dobit emo ispis pribline vrijednosti


kvadratnog korijena broja 2:
f(2) = 1.41701

Uoimo da se prilikom uitavanja podataka iz datoteke unutar while-petlje provjerava


da li je broj uitanih podataka nadmaio duljinu polja u koje se podaci pohranjuju. To je

107

vana mjera zatite, jer za razliku od mnogih programskih jezika, u jeziku C++ nema
provjere granica polja prilikom pristupa elementima.
Navede li se preveliki ili negativni indeks, prevoditelj nee javiti pogreku i
pristupit e se memorijskoj adresi koja je izvan podruja rezerviranog za
polje!
Kod dohvaanja lana s nedozvoljenim indeksom, uitana vrijednost bit e openito
neki sluajni broj, to i ne mora biti pogibeljno. Meutim, kod pridruivanja lanu s
indeksom izvan dozvoljenog opsega, vrijednost e se pohraniti negdje u memoriju u
podruje predvieno za neku drugu varijablu ili ak izvrni kd. U prvom sluaju
dodjela e uzrokovati promjenu vrijednosti varijable pohranjene na tom mjestu, to e
vjerojatno na kraju dati pogreku u konanom rezultatu. U drugom sluaju, ako
vrijednost odbjeglog lana dospije u dio memorije gdje se nalazi izvrni kd,
posljedice su nesagledive i vjerojatno e rezultirati blokiranjem rada programa ili
cijelog raunala. Da bismo se osvjedoili u gornje tvrdnje, pogledajmo sljedei
(bezopasan) primjer:
int main() {
float a[] = {10, 20, 30};
float b[] = {40, 50, 60};
float c[] = {70, 80};
cout << b[-1] << endl;
cout << b[3] << endl;
return 0;
}

to e se ispisati izvravanjem ovog programa prvenstveno ovisi o tome kako koriteni


prevoditelj raspodjeljuje memorijski prostor za polja a[], b[] i c[]. Prevoditelj koji
smo mi koristili sloio je polja u memoriju jedno za drugim prema slici 6.4.
b[-1]

b[3]

c[0]: c[1]: b[0]: b[1]: b[2]: a[0]: a[1]: a[2]:


70.

80.

14243

int c[2]

40.

50.

1442443
int b[3]

60.

10.

20.

1442443

30.

int a[3]

Slika 6.4. Dohvaanje lanova polja s indeksom izvan deklariranog opsega

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];
b = a[i % j];
b = a[2 * i - j];

// a[7]
// a[2]
// a[-1]

Meutim, budui da indeks ne smije biti nita osim cjelobrojnog tipa, naredbe poput:
b = a[2.3];
float c = 1.23;
b = a[c / 2];

// pogreka
// pogreka

uzrokovat e pogreku prilikom prevoenja.


6.1.2. Dvodimenzionalna polja

U dosadanjim primjerima koristili smo jednodimenzionalna polja u kojima su lanovi


sloeni u jednom kontinuiranom nizu i dohvaaju se samo jednim indeksom. esto se
pak javlja potreba za pohranjivanjem podataka u dvodimenzionalne ili
viedimenzionalne strukture. elimo li pohraniti podatke iz neke tablice s 3 retka i 5
stupaca (slika 6.5), najprikladnije ih je pohraniti u dvodimenzionalno polje
1. stupac

2. stupac

3. stupac

4. stupac

5. stupac

1. redak

214

...

...

...

...

2. redak

...

...

...

...

...

3. redak

...

101

...

...

...

Slika 6.5. Primjer tablice s tri retka i pet stupaca

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:
Tablica[0][0] = 214;
Tablica[2][1] = 101;

// 1.redak, 1.stupac
// 3.redak, 2.stupac

U sutini se ovo dvodimenzionalno polje moe shvatiti kao


jednodimenzionalna polja, od kojih je svako duljine 5 (vidi sliku 6.6).

tri jednaka

109

Indeksi za pojedine dimenzije moraju biti odvojeni u zasebne parove uglatih


zagrada.
U nekim jezicima se indeksi za sve dimenzije smjetaju unutar zajednikog para uglatih
zagrada, odvojeni zarezom:
Tablica[2, 1];

// pogreno

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:
int Tablica[3][5] = { {11, 12, 13, 14, 15},
{21, 22, 23, 24, 25} };

Ovime se inicijaliziraju lanovi prvih dvaju redaka. Unutranje vitiaste zagrade


omeuju lanove u pojedinim recima. Budui da je inicijalizacijska lista kraa od
duljine polja (nedostaju lanovi treeg retka), lanovi treeg retka se inicijaliziraju na
nulu, kako je prikazano slikom 6.6. Ako su liste unutar zagrada za pojedine retke
prekratke, inicijalizirat e se lanovi prvih stupaca dotinih redaka. Na primjer:

110
Tablica Tablica Tablica Tablica Tablica
[0][0]: [0][1]: [0][2]: [0][3]: [0][4]:
int[3][5]
Tablica

int

int

int

int

int

11

12

13

14

15

Tablica Tablica Tablica Tablica Tablica


[1][0]: [1][1]: [1][2]: [1][3]: [1][4]:
int

int

int

int

int

21

22

23

24

25

Tablica Tablica Tablica Tablica Tablica


[2][0]: [2][1]: [2][2]: [2][3]: [2][4]:
int

int

int

int

int

Slika 6.6. Prikaz dvodimenzionalnog polja

int Tablica[3][5] = { {11}, {21, 22}, {31} };

Ovime e se inicijalizirati samo pojedini lanovi: Tablica[0][0], Tablica[1][0],


Tablica[1][1] i Tablica[2][0]. Ako se vitiaste zagrade redaka izostave, tada se
inicijalizacija provodi postupno od prvog retka, bez preskakanja u sljedei redak sve
dok se ne popune svi lanovi tog retka. Naredbom
int Tablica[3][5] = { 11, 12, 13, 14, 15, 21, 22 };

deklarirat e se polje i inicijalizirati sve lanove prvog retka (od Tablica[0][0] do


Tablica[0][4]), te samo prva dva lana drugog retka (Tablica[1][0] i
Tablica[1][1]).
Primjenu dvodimenzionalnih polja prikazat emo u programu za rjeavanje sustava
linearnih jednadbi Gaussovim postupkom. Gaussov postupak sastoji se iz dva dijela. U
prvom se dijelu postupno iz jednadbi uklanjaju nepoznanice tako da se matrica
koeficijenata sustava svodi na gornju trokutastu matricu. U drugom dijelu se iz tako
dobivenog trokutastog sustava povratnim supstitucijama izraunavaju nepoznanice.
Pretpostavimo da je zadan sustav 4 jednadbe s 4 nepoznanice:

a11x1 + a12 x2 + a13 x3 + a14 x4 = a15


a21 x1 + a22 x2 + a23 x3 + a24 x4 = a25
a31 x1 + a32 x2 + a33 x3 + a34 x4 = a35
a41 x1 + a42 x2 + a43 x3 + a44 x4 = a45
Dijeljenjem prve jednadbe s a11 ona prelazi u

111

x1 + b12 x2 + b13 x3 + b14 x4 = b15


Od preostale tri jednadbe treba sada oduzeti prvu jednadbu pomnoenu s
koeficijentima a21, a31, odnosno a41, da bismo iz njih uklonili nepoznanicu x1. Time
dobivamo sustav jednadbi

x1 + b12 x2 + b13 x3 + b14 x4 = b15


(1)
(1)
(1)
(1)
a22
x2 + a23
x3 + a24
x4 = a25
(1)
(1)
(1)
(1)
a32
x2 + a33
x3 + a34
x4 = a35
(1)
(1)
(1)
(1)
a42
x2 + a43
x3 + a44
x4 = a45

(1)
Podijelimo li sada drugu jednadbu s a22
, ona prelazi u

x2 + b23 x3 + b24 x4 = b25


Od zadnje dvije jednadbe treba sada oduzeti gornju jednadbu pomnoenu
(1)
(1)
, odnosno a42
, da bismo iz njih uklonili nepoznanicu x2.
koeficijentima a32
Ponavljanjem postupka do zadnje jednadbe, na kraju dobivamo sustav jednadbi

x1 + b12 x2 + b13 x3 + b14 x4 = b15


x2 + b23 x3 + b24 x4 = b25
x3 + b34 x4 = b35
x4 = b35
Nakon to smo matricu sustava sveli na gornju trokutastu, povratnim supstitucijama
dobivamo traene nepoznanice: nepoznanicu x4 dobivamo izravno iz zadnje jednadbe,
uvrtavanjem x4 u treu jednadbu izraunava se x3 itd.
U programu emo prvo uitati broj jednadbi, odnosno nepoznanica, a zatim
uitavamo koeficijente jednadbi:
#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++)
for (int s = 0; s <= n; s++)
ulazniTok >> a[r][s];
// nastavak slijedi...

// po recima
// po stupcima

Koeficijenti se uitavaju u dvodimenzionalno polje a[][], pri emu prvi indeks


odgovara recima (tj. rednom broju jednadbe), a drugi indeks polja odgovara stupcima
matrice koeficijenata. Koeficijenti s desne strane znaka jednakosti (konstantni lanovi)
dodani su kao zadnji stupac matrice, tako da je broj stupaca za 1 vei od broja redaka.
Za sustav jednadbi:

2 x1 7 x2 + 4 x3 = 9
x1 + 9 x2 6 x3 = 1
3x1 + 8 x2 + 5x3 = 6
datoteka s ulaznim podacima jest:
3
2
1
-3

-7
9
8

4
-6
5

9
1
6

Nakon uitavanja, podaci e biti pohranjeni u polje a[][] na sljedei nain:


a[0][0]=2.
a[1][0]=1.
a[2][0]=-3.

a[0][1]=-7.
a[1][1]=9.
a[2][1]=8.

a[0][2]=4.
a[1][2]=-6.
a[2][2]=5.

a[0][3]=9.
a[1][3]=1.
a[2][3]=6.

Ostali lanovi polja ostaju nedefinirani, ali kako ih neemo dohvaati, to nema
nikakvog utjecaja na raun. Slijedi ranije opisani Gaussov postupak:
// nastavak: svoenje na trokutastu matricu...
for (r = 0; r < n; r++) {
for (s = r + 1; s <= n; s++)
a[r][s] /= a[r][r];
for (int rr = r + 1; rr < n; rr++)
for (int ss = r + 1; ss <= n; ss++)
a[rr][ss] -= a[rr][r] * a[r][ss];
}
// ...te povratna supstitucija
for (r = n - 1; r >= 0; r--)
for (s = n - 1; s > r; s--)
a[r][n] -= a[r][s] * a[s][n];
// ispis rezultata
for (r = 0; r < n; r++)

113

cout << "x" << (r + 1) << " = "


<< a[r][n] << endl;
return 0;
}

Za navedene ulazne podatke, program treba na zaslonu ispisati


x1 = 4.
x2 = 1.
x3 = 2.

Napomenimo da e ovaj program pasti za sustav jednadbi koji nema jednoznano


rjeenje, jer e tijekom svoenja na trokutastu matricu doi do dijeljenja s nulom.
Zadatak: Ubacite u kd provjeru da li sustav ima jednoznano rjeenje; ako ga nema,
neka se ispie poruka o pogreki i prekine izvoenje programa.
Ne ulazei dublje u analizu algoritma i njegove implementacije, uoimo jo
nekoliko detalja vezanih uz programski kd. Kod deklaracije polja a[][] moemo
uoiti da je maksimalna veliina drugog indeksa definirana aritmetikim izrazom
(nmax + 1). To je dozvoljeno, uz uvjet da je rezultat tog izraza poznat u trenutku
prevoenja kda.

Uoimo takoer kako su u gornjem kdu izostavljene vitiaste zagrade kod fornaredbi koje su ugnijeene jedna unutar druge:
for (r = 0; r < n; r++)
for (int s = 0; s <= n; s++)
ulazniTok >> a[r][s];

Unato injenici da se unutar vanjske for-petlje nalaze dvije naredbe (unutarnja fornaredba 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

slae uzastopno, jednu za drugom. Tako su lanovi dvodimenzionalnog polja


deklariranog kao a[3][2] rasporeeni u memoriju prema predloku na slici 6.7.
1. redak
644474448
a[0][0]

2. redak
644474448

a[0][1]

a[1][0]

a[1][1]

3. redak
644474448
a[2][0]

a[2][1]

Slika 6.7. Raspored dvodimenzionalnog polja u memoriji raunala

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:
int *kazalo;
int n = 5;
kazalo = &n;

// pokaziva na int
// usmjeri pokaziva na n

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*

kazalo
kazalo :

int*
int*
??

a)

int
int

nn

int
int
55

b)

int*
int*

kazalo
kazalo :

int*
int*

int
int

nn

int
int
55

c)
Slika 6.8. Deklaracija pokazivaa i pridruivanje vrijednosti

cout << "n = " << n << endl;


cout << "*kazalo = " << *kazalo << endl;

// ispisuje 5...
// ...i opet 5

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.
Operator * naziva se operatorom dereferenciranja ili indirekcije
(dereferencing, indirection operator) dok se operator & naziva operatorom
adrese (address-of operator).
Izostavimo li znak * ispred imena pokazivaa, dohvatit emo sadraj pokazivaa, tj.
memorijsku adresu na kojoj je pohranjena varijabla n:
cout << "kazalo = " << kazalo << endl;
// ispisuje memorijsku adresu varijable n

to e se ispisati izvoenjem ove naredbe nije jednoznano odreeno, jer to ovisi o


organizaciji memorije u pojedinom raunalu te o memorijskoj lokaciji u koju e se
program smjestiti prilikom izvravanja. Stoga je vrlo vjerojatno da e viekratno
izvoenje programa na istom raunalu dati ispis razliitih memorijskih adresa, jer se
program svaki puta smjeta u drugi dio memorije. Ova nejednoznanost ne treba
zabrinjavati, jer se vrlo rijetko barata izravno s memorijskim adresama veinu tih
operacija obavlja prevoditelj. U svakom sluaju e gornji ispis dati neki heksadecimalni
broj, na primjer 0x2190.
Sve operacije koje su dozvoljene izravno na cjelobrojnoj varijabli, mogu se provesti
i preko pokazivaa. Tako gornjem kdu moemo pridodati sljedee naredbe:

116

*kazalo += 5;
n = *kazalo - 2;

// isto kao: n += 5
// sada je n = 10
// isto kao: n = n - 2;
// sada je n = 8

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 *
kazalo += 5;
// preusmjerava pokaziva!
cout << "kazalo = " << kazalo << endl;
cout << "*kazalo = " << *kazalo << endl;

prevoditelj nee imati uputu da elimo pristupiti vrijednosti na koju on pokazuje.


Naprotiv, on e baratati adresom pohranjenom u pokazivau. Tako e prva naredba
poveati tu adresu za 5 memorijskih blokova, pri emu je veliina tih memorijskih
blokova definirana prostorom koji zauzima int varijabla (da je pokaziva bio definiran
kao float *, prirast bi bio jednak peterostrukoj duljini float varijable u memoriji).
Prvi e ispis dati adresu koja je za 5 duljina cjelobrojne varijable vea od prvobitno
ispisane adrese. Drugi ispis e dati nepredvidljivi rezultat, jer ne znamo to se moe
nai na toj adresi to ovisi o ostalim varijablama, te o nainu na koji prevoditelj
organizira pohranjivanje varijabli.
Definiramo li novu cjelobrojnu varijablu, pokaziva moemo preusmjeriti na nju
(slika 6.9) :
int m = 3;
kazalo = &m;

Ovime se oito raskida veza izmeu pokazivaa kazalo i varijable n, a uspostavlja


identina veza izmeu kazalo i m.
Valja uoiti da se pokaziva prilikom deklaracije moe inicijalizirati, ali samo na
objekt koji ve postoji u memoriji. Tako smo poetne deklaracije iz gornjeg primjera
mogli pisati kao:
int n = 5;
int *kazalo = &n;

Postoji jo jedan mogui oblik inicijalizacije pokazivaa prilikom deklaracije pomou


operatora new, o emu emo govoriti kasnije u odjeljku 6.2.5.
Budui da brojane konstante nemaju svoj prostor u memoriji, sljedea naredba nema
smisla:
float *pi = &3.14;

// pogreka: pokaziva na to?

117

int*
int*

int*
int*

kazalo
kazalo :

kazalo
kazalo :

int*
int*

int*
int*

int
int

nn

int
int
88

int
int

mm

int
int

nn

int
int

mm

a)

int
int
33

int
int
88
int
int
33

b)

Slika 6.9. Preusmjeravanje pokazivaa

Zadatak. Odredite to e se ispisati izvravanjem sljedeih naredbi:


int i = 1;
int j = 10;
int *p = &j;
*p *= *p;
i = i + j;
p = &i;
cout << i << endl << j << endl << *p << endl;

Meusobne operacije s pokazivaima na razliite tipove podataka nisu preporuljive i


redovito su nedozvoljene. Razmotrimo to na primjeru:
int n = 5;
int *pokn = &n;
float x = 10.27;
float *pokx = &x;
*pokn = *pokx;
pokx = pokn;
pokx = &n;

// n = 10
// pogreka
// pogreka

Poetnim deklaracijama i inicijalizacijama stvorene su cjelobrojna varijabla n = 5, te


pokaziva na nju i decimalna varijabla x = 10.27 s pripadajuim pokazivaem.
Naredbom
*pokn = *pokx;

118

se varijabli na koju je pokazuje pokn pridruuje vrijednost varijable na koju pokazuje


pokx. Kako su te varijable razliitih aritmetikih tipova, prilikom pridruivanja
primjenjuju se uobiajena pravila konverzije, pa e nakon obavljene naredbe varijabla n
poprimiti vrijednost 10.
Slijede dvije meusobno jednake naredbe kojima pokuavamo pokaziva na
decimalnu varijablu preusmjeriti na adresu na kojoj se nalazi cjelobrojna varijabla. Iako
adrese na razliite tipove podataka u nekom raunalu uvijek (manje-vie) imaju isti
oblik, naini na koje su te varijable pohranjene na dotinim lokacijama su razliiti, te bi
pridruivanja ovakvog tipa davala nepredvidive rezultate. Zato prevoditelj ne
dozvoljava ovakva pridruivanja i javlja pogreku.
Zadatak: Prije nego to proitate objanjenje koje slijedi, razmislite zato e izvoenje
sljedeeg kda prouzroiti pogreku:
int *pokn;
float x = 10.27;
float *pokx = &x;
*pokn = *pokx;

// pogreka pri izvoenju

Kd se razlikuje od prethodnoga po tome to nije inicijalizirana vrijednost pokazivaa


pokn. To znai da e biti alociran prostor za taj pokaziva, no njegova e vrijednost
biti sluajna (ovisno o sadraju memorije prije alokacije). Pokuaj pridruivanja u
zadnjem retku vrlo e vjerojatno prouzroiti prekid programa ako pokn pokazuje na
neku nedozvoljenu memorijsku lokaciju, jer e se vrijednost 10.27 pokuati prepisati u
zabranjeno memorijsko podruje.
Izuzetak od gornjih pravila o meusobnom pridruivanju pokazivaa ine
pokazivai tipa void * (engl. void - prazan, ispranjen). Oni pokazuju na neodreeni tip
podataka, tj. na openitu memorijsku lokaciju. Stoga pokaziva na void moemo
preusmjeriti na objekt bilo kojeg tipa:
int n = 5;
int *pokn = &n;
float x = 10.27;
float *pokx = &x;
void *nesvrstan;

// pokaziva na void

nesvrstan = &n;
nesvrstan = pokx;

// preusmjeri na n
// preusmjeri na x

Meutim, pokazivau na void ne moemo izravno pridruiti vrijednost:

Veina dananjih procesora posjeduje mehanizme kojima se pojedini segmenti memorije


mogu zatititi od neovlatene promjene. Zbog toga e se gornji program prekinuti samo ako
pokx pokazuje izvan dozvoljenog podruja.

119

*nesvrstan = x;

// pogreka: void se ne moe


//
dereferencirati

Budui da pokaziva na void ne sadri informaciju o tipu podatka na koji pokazuje,


prilikom dohvaanja vrijednosti moramo mu priloiti odgovarajue natuknice. Na
primjer, elimo li u prethodnom primjeru ispisati sadraj varijable n preko pokazivaa
nesvrstan, tada emo morati napisati:
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
// duljine pokazivaa na:
cout << sizeof(int*) << endl;
//
int,
cout << sizeof(float*) << endl; //
float,
double *xyz;
cout << sizeof(xyz) << endl;
//
double

e dati isti ishod. Preputamo itatelju da sam provjeri ovu tvrdnju.


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 nulpokaziva tako da mu se jednostavno dodijeli nula:
int *x = 0;

U gornjem primjeru se pokaziva na cijeli broj ne inicijalizira tako da ga se odmah


usmjeri na neki broj, ve se eksplicitno naznaava da vrijednost pokazivaa nije
postavljena. Umjesto nule, moemo koristiti konstantu NULL koja je definirana u

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

standardnim bibliotekama. Tako se eksplicitnije naznaava da se radi ba o nulpokazivau. Na primjer:


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.
6.2.2. Kamo sa zvijezdom (petokrakom)

Osvrnimo se na trenutak na nain deklaracije pokazivaa. U gornjim deklaracijama smo


simbol za pokaziva * pisali uz ime pokazivaa. Openito su praznine oko znaka *
proizvoljne: smiju se staviti na jednu i/ili drugu stranu ili se ne moraju uope staviti.
Tako je dozvoljeno pisati:
float *ZvjezdicaUzIme;
float* ZvjezdicaUzTip;
float * ZvjezdicaOdmaknutaOdImenaITipa;
float*ZvjezdicaUzImeITip;

Vie no oito je zadnji nain nepregledan, pa se ne koristi. Autori mnogih knjiga


preferiraju pisanje zvjezdice uz tip:
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, nekazaljka;

Znak za pokaziva u gornjoj deklaraciji je pridruen samo varijabli kazaljka, dok je


nekazaljka obini int (lat. int vulgaris domesticus). elimo li ostaviti operator za
pokaziva uz tip na koji varijabla pokazuje, zbog dosljednosti bismo morali gornju
deklaraciju razbiti na dvije:
int* kazaljka;
int* kazaljka2;

Pristupom koji emo i mi koristiti u knjizi, izbjegnute su takve zamke:


int *PeterPointer, *WhereAreYou;

121

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
int
int
int

a = 5;
b = 10;
*poka = &a;
*pokb = &b;

int c = *poka * *pokb;

Druga rjeenja su manje pregledna:


int c = *poka * * pokb;

ili, jo gore:
int c = *poka **pokb;

Jednaka razmatranja vrijede iz za & (operator adrese): kao i simbol za pokaziva, on se


moe pisati uz oznaku tipa ili uz identifikator.
Koji od pristupa e itatelj koristiti preputamo njegovom izboru. Mi smo se samo
osjetili dunima navesti razloge za ili protiv nekog pristupa, da bi se poetnik lake
opredijelio. Jer, tee je mijenjati navike kada vam neto ue u krv. U svakom sluaju,
kada jednom odaberete neki pristup, drite ga se dosljedno do kraja.
6.2.3. Tajna veza izmeu pokazivaa i polja

U programskom jeziku C++ pokazivai i polja su meusobno vrsto povezani. Iako to


kod dohvaanja preko indeksa nije oito, lanovi polja dohvaaju se u biti preko
pokazivaa. Naredbom
float x[5];

deklarira se jednodimenzionalno polje objekata koje se sastoji od pet lanova tipa


float. Pri tome samo ime x ima smisao pokazivaa na prvi lan polja x[0] (slika
6.10). Prilikom dohvaanja lanova polja, prevoditelj e vrijednost indeksa pribrojiti
pokazivau na prvi lan; tako e naredba
float a = x[2];

biti zapravo prevedena kao


float a = *(x + 2);

122

float[5]
float[5]

xx

x[0]:

x[1]:

x[2]:

x[3]:

x[4]:

float
float

float
float

float
float

float
float

float
float

float
float **

Slika 6.10. Veza izmeu polja i pokazivaa

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:
int b[] = {10, 20, 30};
cout << b << endl;
cout << *b << endl;

// adresa poetnog lana


// njegova vrijednost

Isto tako e obje naredbe


cout << &(b[1]) << endl;
cout << (b + 1) << endl;

// adresa lana b[1]


// ista stvar, druga forma

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];

se ne stvara pokaziva x koji pokazuje na polje; x je jednostavno samo sinonim za


pokaziva na prvi lan polja. Njega moemo koristiti isti nain na koji moemo koristiti

123

i ostale pokazivae, ali mu ne moemo promijeniti vrijednost. Sljedei kd e ispisati


iste vrijednosti:
int x[5];
cout << "x: " << x << endl;
cout << "&x: " << &x << endl;

To nam svjedoi da se u memoriji raunala nigdje ne alocira prostor za pokaziva x, pa


x i &x imaju isti smisao. Poneki prevoditelji ak imaju upozorenje da se znak & ispred
imena polja zanemaruje.
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.
Neposredna posljedica ovakvog rukovanja poljima jest nemogunost izravnog
pridruivanja sadraja cijelog polja. elimo li sve lanove polja a preslikati u polje b, to
neemo moi ostvariti jednostavnom operacijom pridruivanja:
float a[] = {10, 20, 30, 40};
float b[4];
b = a;
// pogreka prilikom prevoenja

Potrebno je napisati petlju koja e pojedinano dohvaati lanove izvornog polja i


preslikavati ih u odredino polje. Pokuaj pridruivanja pokazivaa:
*b = *a;

// b[0] = a[0]

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:
float a[3][4] = {{1.1, 1.2, 1.3, 1.4},
{2.1, 2.2, 2.3, 2.4}};
cout << *a[0] << "\t" << *a[1] << endl;

124

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
1.1
1.1

float
float (*a)[0]
(*a)[0]
float
float (*a)[1]
(*a)[1]

float
float (*a)[2]
(*a)[2]

float
float
1.2
1.2

float
float
1.3
1.3

float
float
1.4
1.4

a[1][0]: a[1][1]: a[1][2]: a[1][3]:


float
float
2.1
2.1

float
float
2.2
2.2

float
float
2.3
2.3

float
float
2.4
2.4

a[2][0]: a[2][1]: a[2][2]: a[2][3]:


float
float

float
float

float
float

float
float

Slika 6.11. Prikaz dvodimenzionalnog polja preko polja pokazivaa

Vrijedi ista ograda kao u sluaju pokazivaa na jednodimenzionalno polje: a[1] je


samo sinonim za pokaziva na lan a[1][0], no on nije nigdje u memoriji posebno
alociran. Zbog toga mu se ne moe uzeti adresa (a[1] i &a[1] su iste vrijednosti), te
mu se ne moe mijenjati vrijednost.
6.2.4. Aritmetike operacije s pokazivaima

Na pokazivaima su definirane neke aritmetike operacije, kao i usporedbe pokazivaa.


Na primjer, mogue je pokazivau pribrojiti ili oduzeti cijeli broj, raunati razliku
izmeu dva pokazivaa, te usporeivati pokazivae.
Ako se pokazivau pribroji ili oduzme cijeli broj (konstanta, varijabla ili izraz),
rezultat takve operacije bit e pokaziva koji pokazuje na memorijsku lokaciju udaljenu
od poetne za navedeni broj objekata dotinog tipa. Na primjer, ako se od pokazivaa na
objekt tipa int oduzme broj 3, dobit e se pokaziva koji pokazuje na memorijsku
lokaciju koja je za 3 * sizeof(int) manja od poetne:
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;

moe se interpretirati kao


pok2 = (T *)(((char *)pok1) + i * sizeof(T));

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
float
float
float

x[10];
*px = &x[3];
x2 = *(px - 1);
x5 = *(px + 1);

// x[2] prethodni lan


// x[4] sljedei lan

Dozvoljeno je meusobno oduzimanje pokazivaa samo na objekte istog tipa. Rezultat


takvog izraza broj objekata tog tipa koji bi se mogli smjestiti izmeu ta dva pokazivaa.
Oduzimanje pokazivaa se esto koristi kada pokazivai koje oduzimamo pokazuju na
lanove istog polja: rezultat je tada jednak razlici indeksa lanova na koje ti pokazivai
pokazuju:
float x[10];
float *prviClanUNizu = x;
float *zadnjiClanUNizu = &x[9];
int razmak = zadnjiClanUNizu - prviClanUNizu;
razmak = prviClanUNizu - zadnjiClanUNizu;

// = 9
// = -9

Pokazivae je mogue usporeivati. Pri tome se usporeuju memorijske adrese na koje


pokazivai pokazuju. U nastavku gornjeg primjera
if (prviClanUNizu >= zadnjiClanUNizu)
cout << "Frka!";

Poseban smisao ima operator jednakosti, kojim se utvruje da li dva pokazivaa


pokazuju na istu memorijsku lokaciju. Takoer, esto se pokaziva usporeuje s nulvrijednosti:

126

#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)

// ...

Ilustrirajmo primjenu pokazivake aritmetike programom kojim se u polje brojeva


sortiranih po veliini umee novi lan. Tada je potrebno sve lanove polja vee od
novoga lana pomaknuti prema kraju polja, tako da se napravi prazno mjesto. Radi
preglednosti, kd je maksimalno pojednostavljen, te nema provjere da li je broj lanova
u polju nadmaio njegovu duljinu (itatelju preputamo da nadopuni kd
odgovarajuom provjerom).
// 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

Aritmetika s pokazivaima ima prednosti kod dohvaanja uzastopnih


lanova polja.
Varijable i lanovi polja inicijalizirani su proizvoljnim vrijednostima na poetku kda
da bi se lake shvatio njegov smisao. Ako bi polje inicijalno bilo prazno, tada bismo
uzastopnim pozivanjem gornjeg kda dobili program koji uitava podatke te ih odmah
slae po veliini.
Usredotoimo se na operacije s pokazivaima unutar while petlje. Osim usporedbe
u uvjetu izvoenja petlje, zanimljivo je uoiti usporedbu pokazivaa odrediste i
polje u if naredbi, gdje se koristi usporedba pokazivaa za odreivanje uvjeta prekida
petlje. Ako odrediste pokazuje na adresu manju od one na koju pokazuje pokaziva
polje, petlja se prekida, jer je program tijekom pretraivanja stigao na poetak polja,
ne naavi lan u nizu manji od novog broja te noviBroj treba umetnuti na poetak
polja. Takoer uoimo pridruivanje:
*(odrediste + 1) = *odrediste;

Na prvi pogled se ini da bi prevoditelj trebao javiti pogreku da se s lijeve strane


operatora = ne nalazi lvrijednost. Meutim, razmotrimo li stvarno znaenje gornje
naredbe, nedoumice e nestati: vrijednost pohranjenu u memoriji na adresi na koju
pokazuje odrediste ne pridruujemo varijabli odrediste + 1, ve pohranjujemo u
memoriju na adresu koja je za jedan memorijski blok vea od adrese odrediste.
Gornju petlju mogli smo napisati i krae, tako da pomake brojeva ubacimo u uvjet
izvoenja while petlje:
while ((*(odrediste--) = *(odrediste - 1)) > noviClan)
if (odrediste < polje)
break;

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.
6.2.5. Dinamiko alociranje memorije operatorom

new

Do sada smo pokaziva uvijek usmjeravali na prethodno deklariranu i (eventualno)


inicijaliziranu varijablu:
float nekiBroj;
float *pokazivacNaTajBroj = &nekiBroj;
*pokazivacNaTajBroj = 34.234;

128

Deklaracija varijable nekiBroj u ovom primjeru treba nam samo da bi se osigurao


memorijski prostor na koji e pokazivacNaTajBroj biti usmjeren dalje sve
moemo napraviti preko pokazivaa. Je li deklaracija varijable nekiBroj ba
neophodna ili ju moemo nekako izbjei? Drugim rijeima, je li mogue istodobno s
deklaracijom pokazivaa rezervirati memorijski prostor za objekt ija e vrijednost biti
tamo pohranjena? Odgovor na to pitanje prua sljedei kod:
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:
float *pokaZivac = new float();
if (pokaZivac == 0)
cout << "Pun sam kao ipak!" << endl;
//...

Unutar okruglih zagrada iza oznake tipa moe se definirati poetna vrijednost objekta,
to znai da smo prethodni primjer mogli pisati jo krae:
float *pokaZivac = new float(10.5);
if (!pokaZivac)
cout << "Pun sam kao ipak!" << endl;
//...

Alocirani prostor zauzet operatorom new se ne oslobaa automatski prilikom izlaska iz


bloka naredbi. Do sada smo koristili iskljuivo automatske objekte (engl. automatic
objects) kojima je prevoditelj sam alocirao memorijski prostor pa se sam i brinuo o
oslobaanju tog prostora pri izlasku iz bloka unutar kojeg je objekt bio deklariran. Za
pohranjivanje takvih objekata koristi se stog (engl. stack), zasebni dio memorije u koji
se pohranjuju privremeni podaci. Izlaskom iz bloka naredbi objekti koji su bili
deklarirani u bloku proglaavaju se nepotrebnima i preko njih se prepisuju novi podaci.
Istina, ako nema novih podataka, ti podaci jo mogu postojati zapisani u memoriji, ali
prevoditelj vie ne garantira njihov integritet, pa ih stoga proglaava nedohvatljivima.
Za razliku od automatskih objekata, prostor za dinamike objekte (engl. dynamic
objects) alocira programer operatorom new. Dinamiki objekti se smjetaju u javni dio
memorije, tzv. hrpu (engl. heap). Budui da prevoditelj ne kontrolira ienje tog
dijela memorije, sam programer mora unititi dinamike objekte kada mu vie ne

129

trebaju, ali svakako prije zavretka programa u protivnom e se izvoenjem programa


bespovratno smanjivati raspoloiva memorija.
Unitavanje dinamikih objekata obavlja se operatorom delete; kada nam vie
prostor na koji pokaZivac pokazuje nije potreban, napisat emo naredbu:
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:
int *varijablaKojaNestaje = new int(123);
delete varijablaKojaNestaje;
cout << *varijablaKojaNestaje << endl;

ispisati broj 123, unato tome da je prethodno osloboen prostor koji je


varijablaKojaNestaje zauzimala. Sadraj objekta nakon primjene operatora
delete je neodreen.
6.2.6. Dinamika alokacija polja

Operatorom new se moe alocirati prostor i za polje objekata. To omoguava da se


duljina polja definira tijekom izvoenja programa, umjesto da ju moramo definirati prije
prevoenja kao to je to sluaj kod automatskih polja.
Kod alokacije polja operatorom new treba u uglatim zagradama nakon oznake tipa
navesti duljinu polja. Tako e naredba
int *sati = new int[12];

alocirati prostor za polje od 12 cjelobrojnih lanova. Operator new e u sluaju


uspjenog osiguranja memorijskog prostora kao rezultat vratiti pokaziva na poetak
polja. U sluaju neuspjeha, vraa se nul-pokaziva.
Kao to se za oslobaanje prostora dinamikih varijabli koristi operator delete, za
oslobaanje alociranih polja se koristi operator delete [], tako da se iza uglate
zagrade navede ime polja:
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.
Operatori delete i delete [] nisu isti operatori. delete slui za
oslobaanje jednog objekta, dok delete [] oslobaa 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.

Tablica 6.1. Ulazni podaci za primjer dinamikog alociranja polja.

s
t

0
0

100 m
13 s

200 m
31 s

300 m
46 s

400 m
63 s

500 m
76 s

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
n1
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

50
t /s

100

Slika 6.12. Linearna interpolacija mjerenih toaka.

1 n-1
x = x i ,
n i =0

1 n-1
y = yi
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;
float *x = new float[n];
float *y = new float[n];

// alocira se prostor za
// polja s koordinatama

double srednjiX = 0.0;


// srednja vrijednost od x
double srednjiY = 0.0;
// srednja vrijednost od y
cout << "Upii koordinate!" << endl;
for (int i = 0; i < n; i++) {
cout << "t(" << i+1 << ") = ";
cin >> x[i];
cout << "s(" << i+1 << ") = ";
cin >> y[i];
srednjiX += x[i];
srednjiY += y[i];
}
srednjiX /= n;

132

srednjiY /= n;
// nastavak slijedi...

Koordinate toaka pohranjuju se u dva jednodimenzionalna polja (x i y) za koja je


alociran prostor operatorom new:
float *x = new float[n];
float *y = new float[n];

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...

Budui da nam polja x i y vie ne trebaju, operatorom delete []emo osloboditi


prostor kojeg oni zauzimaju, a potom ispisati rezultate (za podatke iz tablice dobiva se
prosjena brzina trkaa od 6,41 m/s, te 124 sekunde potrebne za stazu od 800 m):
// ...konac djelo krasi:
delete [] x;
// oslobaamo prostor
delete [] y;
cout << "Prosjena brzina trkaa je "

Pozdrav kojim su spartanski vojnici otpravljani u ratove. Znai: Vratite se kao pobjednici ili
nam se ne vraajte ivi pred oi!

133

<< a << " m/s" << endl;


cout << "Ako bude jednako uporan, za 800 metara "
<< "trebat e mu oko " << (800 - b) / a
<< " sekundi." << endl;
return 0;
}

Dealokacija polja x i y provedena je tako da je izmeu operatora delete i imena polja


umetnut prazan par uglatih zagrada:
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:
float *prvoPolje = new float[n];
// ...
delete [] prvoPolje;
float *novoPolje = new float[m];

u vjeri da e se novoPolje smjestiti na mjesto nastalo dealokacijom polja prvoPolje,


tako da e se postojei lanovi iz prvoPolje jednostavno pojaviti u novom polju.
Meutim, ne postoji nikakva garancija da e se takvo to i dogoditi (tovie, to je
gotovo nevjerojatno). Prema tome, kada alocirano polje postane pretijesno, jedini
pouzdan nain da se osigura dodatni prostor jest alociranje potpuno novog polja vee
duljine, kopiranje svih postojeih lanova u novo polje, te unitenje starog polja.
Daleko fleksibilnije promjene duljine niza podataka omoguavaju vezane liste (engl.
linked lists) s kojima emo se upoznati kasnije.
6.2.7. Dinamika alokacija viedimenzionalnih polja

Nakon to smo savladali dinamiku alokaciju jednodimenzionalnih polja, upoznat emo


se i s alokacijom viedimenzionalnih polja. Kao to smo u odsjeku 6.2.3 vidjeli,
dvodimenzionalno se polje sastoji od niza jednodimenzionalnih polja jednake duljine.
Stoga prilikom alokacije primjerice dvodimenzionalnog polja 34, u sutini treba
alocirati prostor za 3 jednodimenzionalna polja duljine 4:

134

float (*a)[4] = new float[3][4];

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:
float *a[4] = new float[3][4];

// pogreka

u stvari se a deklarira kao pokaziva na polje od etiri lana, te prevoditelj prijavljuje


pogreku. Oslobaanje prostora se obavlja identino kao i kod jednodimenzionalnih
polja:
delete [] a;

Treba naglasiti da kod alokacije prostora za viedimenzionalna polja duljine svih


dimenzija osim prve moraju poznate prilikom prevoenja. Zbog toga e naredba
double (*b)[n] = new double[10][n];

// pogreka

prouzroiti pogreku pri prevoenju, dok je naredba


double (*c)[5] = new double[n][5];

// OK

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.
6.2.8. Pokazivai na pokazivae

Kao to je mogue definirati pokazivae na objekte bilo kojeg tipa, mogue je


definirati i pokazivae na pokazivae. Netko e se priupitati emu je to potrebno; zar
nije dovoljna fleksibilnost postignuta ve samim pokazivaem, koji moe pokazivati na
bilo koje mjesto u memoriji? Da bismo odagnali sve nedoumice, itatelju emo
podastrijeti vrlo praktian primjer dinamikog alociranja prostora za dvodimenzionalno
polje. Za razliku od gornjeg primjera u kojem sve dimenzije polja moraju biti poznate u
trenutku alociranja memorijskog prostora, ovaj primjer (preuzet iz [Borland94]) prua
daleko veu fleksibilnost, jer broj lanova ne mora biti jednak u svakom retku.
Pogledajmo prvi dio kda u kojem se alocira neophodan prostor za polje, te se
lanovima pridruuju vrijednosti. Budui da reci mogu imati proizvoljan broj lanova,
valja pohraniti podatke o broju lanova za svaki redak. Taj broj emo pohraniti kao prvi
podatak u svakom retku, prije vrijednosti samih lanova.

135

#include <iostream.h>
int main() {
float **dvaDpolje;
int i, redaka, stupaca;
cout << "Broj redaka: ";
cin >> redaka;
dvaDpolje = new float*[redaka];
for (i = 0; i < redaka; i++) {
cout << "Broj lanova u " << (i+1) << ". retku:";
cin >> stupaca;
dvaDpolje[i] = new float[stupaca + 1];
dvaDpolje[i][0] = stupaca;
for (int j = 1; j <= stupaca; j++)
dvaDpolje[i][j] = i + 1 + j / 100.;
}
cout << "Ispis lanova polja:" << endl;
for (i = 0; i < redaka; i++)
for (int j = 1; j <= dvaDpolje[i][0]; j++)
cout << "[" << i << "][" << j << "] = "
<< dvaDpolje[i][j] << endl;
// nastavak slijedi nakon raslanbe...

Deklaracijom
float **dvaDpolje;

varijablu dvaDpolje smo proglasili pokazivaem na pokaziva na varijablu tipa


float. Naredbom
dvaDpolje = new float*[redaka];

136

alociran je prvo prostor za polje pokazivaa na float. Ti e pokazivai biti usmjereni


na poetke pojedinih redaka. Slijedi petlja po recima unutar koje se alociraju prostori za
lanove svakog pojedinog retka (slika 6.13):
dvaDpolje[i] = new float[stupaca + 1];

Duljine polja za podatke po recima su vee od broja lanova za podatak o duljini


svakog retka.
Nakon to polje postane nepotrebno, prostor treba osloboditi:
// nastavak: oslobaanje prostora
for (i = 0; i < redaka; i++)
delete [] dvaDpolje[i];
delete [] dvaDpolje;
return 0;
}

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.

6.3. Nepromjenjivi pokazivai i pokazivai na nepromjenjive


objekte
Kljunom rijei const moemo deklarirati nepromjenjivi objekt (simboliku
float**
float**

dvaDpolje
dvaDpolje

float*[0]

float**
float**
:

float*[1]

float[0][0] float[0][1]

float*[redaka]
float*[redaka]

float[0][stupaca]
float[0][stupaca]

Slika 6.13. Pokaziva na pokaziva

137

konstantu). Vidjeli smo da e na pokuaj promjene takvog objekta prevoditelj javiti


pogreku:
const float pi = 3.14;
pi = 3.1415926;

// pogreka

Pokuajmo nadmudriti prevoditelja tako da na pi usmjerimo pokaziva, te pi


promijenimo posredno preko pokazivaa:
float *pipok = &pi;
*pipok = 3.1415926;

// pogreka

Ni prevoditelj nije veslo sisao! Prepoznat e na podmukli pokuaj da na simboliku


konstantu usmjerimo pokaziva i preduhitriti nas javljajui pogreku pri pridruivanju
adrese nepromjenjivog objekta. Da bi se sprijeile nehajne promjene simbolikih
konstanti, na njih se smiju usmjeravati samo pokazivai koji su eksplicitno deklarirani
tako da pokazuju na nepromjenjive objekte:
float const *pipok;
pipok = &pi;

// pokaziva na nepromjenjivi objekt

Kao to smo vidjeli, u protivnom prevoditelj prijavljuje pogreku. Tako deklarirani


pokaziva smije se preusmjerivati:
const float e = 2.71828;
pipok = &e;

// OK

tovie, moe se usmjeriti i na promjenjivi objekt:


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;
*pipok = 9.23

// OK
// pogreka

Prema tome, usmjeravanjem pokazivaa, koji je deklariran kao pokaziva na konstantu,


ne osigurava se nepromjenjivost tog objekta deklaracijom pokazivaa kao pokazivaa
na konstantni objekt dozvoljava se samo njegovo preusmjeravanje na nepromjenjive
objekte te se spreavaju promjene takvih objekata posredno preko pokazivaa.
Takoer je mogue deklarirati nepromjenjivi pokaziva koji moe pokazivati samo
na jedan objekt. Takav pokaziva se mora inicijalizirati, te se kasnije ne moe
preusmjerivati:

138

double nekiBroj = 1989.1005;


double* const neMrdaj = &nekiBroj;

// nepromjenjivi
// pokaziva

Objekt na koji pokaziva pokazuje moe se po volji mijenjati, izravno ili preko
pokazivaa:
nekiBroj = 1993.1009;
*neMrdaj = 1992.3001;

// OK
// OK

Meutim, pokuaj preusmjeravanja nepromjenjivog pokazivaa na neki drugi objekt


sprijeit e prevoditelj, javljajui pogreku:
double nekiDrugiBroj = 1205;
neMrdaj = &nekiDrugiBroj;

// pogreka

Konano, mogue je deklarirati i nepromjenjivi pokaziva na nepromjenjivi objekt:


const float q = 1.602e-19;
const float* const nabojElektrona = &q;

Ovako sloena sintaksa ak i iskusnim programerima zadaje glavobolje. Evo


vrlo jednostavnog naputka za prepoznavanje tipa podataka [Barton94]:
itajte deklaraciju tipa s desna na lijevo, tj. od identifikatora prema naprijed.
Na svaki pokuaj preusmjeravanja pokazivaa nabojElektrona, te na svaki pokuaj
promjene vrijednosti varijable na koju on pokazuje, prevoditelj e javiti pogreku.
Na primjer, deklaracija int * se, prema gornjem naputku, moe itati kao pokaziva na
int, deklaracija const double * kao pokaziva na double koji je nepromjenjiv,
deklaracija double * const kao nepromjenjivi pokaziva na podatak tipa double, a
const float * const se moe itati kao nepromjenjivi pokaziva na float koji je
nepromjenjiv.
Promjenjivost ili nepromjenjivost pokazivaa je dio tipa: tipovi const char * i
char * su razliiti tipovi te ih nije mogue koristiti jedan umjesto drugoga. No postoji
standardna konverzija pokazivaa koja automatski pretvara pokaziva na promjenjivi
objekt u pokaziva na konstantan objekt. Da bismo to ilustrirali, deklarirat emo
sljedee pokazivae:
const char *pokKonst;
char *pokPromj;

Vrijednost pokazivaa pokPromj dozvoljeno je dodijeliti pokazivai pokKonst:


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:
pokPromj = pokKonst;

// pogreka

U gornjem sluaju se pokaziva na konstantni objekt pokuava dodijeliti pokazivau na


promjenjivi objekt. Ako bi to bilo dozvoljeno, tada bi postojala opasnost da se preko
pokazivaa pokPromj promijeni vrijednost konstantnog objekta, to bi naruilo njegov
integritet. U sluaju da ipak bezuvjetno (glavom kroz zid) elimo provesti pridruivanje,
to moramo uiniti pomou eksplicitne dodjele tipa kojom se odbacuje (engl. cast away)
konstantnost pokazivaa pokKonst:
pokPromj = (char *)pokKonst;

// OK

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:
volatile int *pok1;
int volatile *pok2;

// volatile pokaziva na int


// pokaziva na volatile int

Takoer, prilikom dodjele pokazivaa, osim po tipu pokazivai se moraju slagati i po


volatile kvalifikatoru.

6.4. Znakovni nizovi


Za pohranjivanje tekstova koriste se znakovni nizovi (engl. character strings, krae
strings). U sutini su to jednodimenzionalna polja ije lanove ine znakovi (char).
Prilikom inicijalizacije znakovnog niza, njegov sadraj se navodi unutar para znakova "
(dvostruki navodnici):
char minijature[] = "Julije Klovi";
char padobran[] = "Faust Vrani";

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

Slika 6.14. Pohranjivanje znakovnog niza u memoriji

140

Na slici 6.14 prikazan je predloak po kojem se pohranjuje znakovni niz u memoriji


raunala. Kao to se vidi, znakovni niz se sastoji od lanova tipa char iza kojih slijedi
znak '\0'.
Svaki pravilno inicijalizirani znakovni niz sadri i zakljuni znak '\0' (nulznak, engl. null-character). Njega kod inicijalizacije nije potrebno eksplicitno
navesti, ali treba voditi rauna da on zauzima jedno znakovno mjesto.
Za provjeru, napiimo petlju koja e ispisati pojedine lanove niza padobran:
for (int i = 0; i < 20; i++)
cout << padobran[i] << "\t"
<< (int)(unsigned char)padobran[i] << endl;

Dodjela tipa (int) neophodna je da bi se ispisao ASCII kd pojedinog znaka, dok je


dodjela tipa (unsigned char) dodana radi pravilnog ispisa kdova hrvatskih
dijakritikih znakova. Izvoenjem, dobit emo sljedei ispis (kdovi za hrvatska slova
mogu se razlikovati, ovisno o koritenom skupu znakova):
F
a
u
s
t
V
r
a
n

J
u
l
i
j
e

70
97
117
115
116
32
86
114
97
110
232
105
230
0
74
117
108
105
106
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:
cout << padobran << endl;

tono znati da je slovo '' ispred nul-znaka zadnje u nizu, te e pravilno ispisati sadraj
niza padobran samo do tog slova:
Faust Vrani

Prepiemo li nul-znak nekim drugim znakom, na primjer naredbom:


padobran[13] = '&';

gornja naredba za ispis niza padobran dat e:


Faust Vrani&Julije Klovi

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:
char padobran[] = { 'F', 'a', 'u', 's', 't', ' ',
'V', 'r', 'a', 'n', '', 'i', '',
'\0'
};

Vjerujemo da itatelja ne treba posebno upuivati u nespretnost ovakvog naina


inicijalizacije.
injenica da znakovnim nizovima pripada i zakljuni nul-znak nalae dodatni oprez
pri inicijalizaciji i rukovanju. Primjerice, rizino je eksplicitno definirati duljinu
znakovnog niza:
char Stroustrup[3] = "C++";

// nezakljuen niz!

Ovakvom inicijalizacijom rezerviran je prostor od 3 bajta za znakove. Budui da sam


niz sadri 3 znaka, nul-znak e biti pohranjen u memoriju na prvu lokaciju iza polja
Stroustrup, te nee biti zatien od mogueg prepisivanja pri inicijalizaciji varijable
ili polja koje e prevoditelj smjestiti iza polja Stroustrup.

142

Prilikom alociranja prostora za znakovni niz, neophodno je predvidjeti i


prostor za zakljuni nul-znak.
Takoer, prilikom preslikavanja znakovnih nizova, neophodno je preslikati i nul-znak,
jer e u protivnom preslikani niz ostati nezakljuen. Sreom, za veinu operacija s
nizovima postoje gotove funkcije u datoteci string.h koje vode rauna o nul-znaku,
pa je najsigurnije njih koristiti.
Budui da se dohvaanje lanova polja u sutini obavlja preko pokazivaa, zato ne
bismo deklarirali i inicijalizirali pokaziva:
char *Stroustrup = "C++";

Naizgled, ova inicijalizacija nee proi, jer kada smo slinu stvar pokuali s brojevima:
int *n = 10;

// pogreka

prevoditelj je javljao pogreku. Meutim, kod pokazivaa na niz situacija je drukija.


Prevoditelj e osigurati neophodan memorijski prostor za pohranu cijelog znakovnog
niza zajedno sa zakljunim nul-znakom, te e usmjeriti pokaziva Stroustrup na
njegovu poetnu adresu. Sljedea naredba e prouzroiti pogreku prilikom
prevoenja:
char *Kernighan = 'C';

// pogreka

Naime, ovom naredbom deklarira se pokaziva na znak (a ne znakovni niz) i pokuava


ga se usmjeriti na znakovnu konstantu 'C' (za koju nije predvien stalan memorijski
prostor).
Vjerujemo da je paljiviji itatelj ve uoio sutinsku razliku izmeu znaka i
znakovnog niza, ali ipak nije na odmet jo jednom ponoviti:
Znak (char) je samostalna cjelina, a znakovne konstante se piu omeene
znakom ' (jednostruki navodnik). Znakovni nizovi sastoje se od niza znakova
i zakljunog nul-znaka, a nizovne konstante piu se omeene znakom "
(dvostruki navodnik).
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

Oito je da znakovne nizove nije mogue preslikavati jednostavnim operatorom


pridruivanja kao i kod polja brojeva, preslikavanje se mora obavljati znak po znak.
Uz prevoditelje se isporuuje biblioteka string.h koja sadri osnovne funkcije
neophodne za rukovanje znakovnim nizovima. Budui da su te funkcije standardizirane
i vrlo korisne, opisat emo ih u poglavlju posveenom funkcijama.
Kao to se mogu definirati znakovni nizovi sloeni od znakova tipa char, mogu se
definirati i nizovi sloeni od znakova tipa wchar_t (vidi odjeljak 2.4.10)
wchar_t *duplerica = L"abc";

6.4.1. Polja znakovnih nizova

esto se u programu javljaju srodni znakovni nizovi koje je praktino pohraniti u jedno
polje. Budui da je znakovni niz sam po sebi jednodimenzionalno polje znakova, polje
znakovnih nizova u stvari e biti dvodimenzionalno polje. Evo primjera:
#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;
}

Paljiviji itatelj e odmah uoiti nedostatke ovakvog pristupa. Prilikom deklaracije


dvodimenzionalnog polja potrebno je zadati obje dimenzije: broj dana u tjednu i duljinu
naziva dana (uveanu za zakljuni nul-znak). Budui da imena dana nisu jednake
duljine, trebamo se ravnati prema najduljem imenu, dok e kraa imena ostavljati
neiskoriteni prostor u dvodimenzionalnom polju znakova. (Koliko bi samo ivot
programera bio jednostavniji da su svi nazivi jednakih duljina!)
itatelj koji je ve dobro upuen u vezu izmeu pokazivaa i polja e sam naslutiti
pravo rjeenje za gornji problem:
char *dani[] = {
"nedjelja",
"ponedjeljak",
"utorak",
"srijeda",
"etvrtak",
"petak",
"subota" };

144

Umjesto dvodimenzionalnog polja, deklarira se polje pokazivaa, koji se usmjeravaju na


poetke znakovnih nizova s imenima dana. Prevoditelj e naslagati te nizove jedan za
drugim tako da ne ostaju neiskoritene rupe.
Potekoe nastaju ele li se neki od lanova ili svi lanovi niza promijeniti.
Najjednostavnije je deklarirati i definirati drugo polje znakovnih nizova, te preusmjeriti
pokazivae:
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;
// promjenom reference mijenja se izvorna varijabla
iref = 75;
cout << "i = " << i << " iref = " << iref << endl;
return 0;
}

Na poetku se deklarira i inicijalizira cjelobrojna varijabla i, a potom se deklarira


cjelobrojna referenca iref na varijablu i:
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

Nema bitne razlike izmeu pokazivaa i


:
int
ii
reference: oboje omoguavaju posredan pristup
int
int
int
nekom objektu. Reference se i implementiraju kao

55
int&
pokazivai. Meutim, pokaziva se moe
int& iref
iref :

preusmjeriti na neki drugi objekt. Pri dohvaanju


preko pokazivaa neophodan je operator *, a sam
Slika 6.15. Referenca
pokaziva iziskuje odreeni memorijski prostor u
koji e biti pohranjena adresa na koju on pokazuje.
Naprotiv, referenca se inicijalizira prilikom deklaracije i pokazuje na dani objekt dok
ona postoji, te nije potreban nikakav operator za dohvaanje sadraja.
Oito je da referenca prilikom deklaracije mora biti inicijalizirana mora ve
postojati objekt na koji se referenca poziva. Stoga e deklaracija reference bez
inicijalizacije izazvati pogreku prilikom prevoenja:
int &nref;

// pogreka

Budui da postoje konstantni objekti, postoje i reference na nepromjenjive objekte:


const float korijen2 = 1.41;
const float &refKor2 = korijen2;
refKor2 je referenca na nepromjenjivi korijen2. Zanimljivo je primijetiti da se
referenca na const objekt moe vezati i na promjenjivi objekt:
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.
6.5.1. Reference za hakere

Reference se u principu implementiraju kao pokazivai koji ne trae operator * za


pristup sadraju. Mogli bismo pomisliti da emo pomou sljedee naredbe dosei
adresu reference, te direktnim upisivanjem nove adrese promijeniti objekt na koji ona
pokazuje:
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.

6.6. Nevolje s pokazivaima


Pokaziva se moe usmjeriti na bilo koji objekt u memoriji. Meutim, mnogi objekti u
programu ima svoj ogranieni ivotni vijek, od trenutka kada su deklarirani, do trenutka
kada se oni unitavaju. Lokalni objekti se automatski unitavaju na kraju bloka naredbi
u kojem su deklarirani. Unitenjem objekta, na mjesto u memoriji koje je on zauzimao
prevoditelj e pohraniti neki drugi objekt. Potekoe nastaju ako je pokaziva ostao
usmjeren na tu memorijsku lokaciju, unato injenici da dotinog objekta tamo vie
nema. Ilustrirajmo to sljedeim primjerom u kojem u ugnijeenom bloku definiramo
lokalnu varijablu j na koju usmjeravamo pokaziva pok:
int main() {
int *pok;
int i = 10;

// pokaziva

{
int j = 100;
pok = &j;

// lokalna varijabla u bloku


// 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

unitene ili nepostojee objekte esto se nazivaju viseim


pokazivaima (engl. dangling pointers) .
to uiniti s pokazivaem nakon to je objekt na koji je on
pokazivao uniten? Ne elimo li unititi pokaziva (jer nam
moda treba kasnije za neki drugi objekt), najjednostavnije je
pridruiti mu vrijednost nul-pokazivaa:
pok = 0;

Prilikom dohvaanja preko pokazivaa u tom sluaju emo


prethodno provjeriti je li pokaziva razliit od nul-pokazivaa:
//...
if (pok)
cout << *pok << endl;
else
cout << "nul-pokaziva!" << endl;
//...
Slika 6.16. Visei
pokaziva

6.7. Skraeno oznaavanje izvedenih


tipova
Kako je u poglavlju o tipovima ve reeno, pomou kljune rijei typedef moe se
uvesti sinonim za esto koriteni tip kako bi se programski kd uinio itljivijim. To
posebno dolazi do izraaja prilikom definiranja pokazivaa i referenci na objekte, te
prilikom deklaracije polja.
Ako esto koristimo tip pokaziva na znak - char *, moemo uvesti sinonim
pokZnak koji e oznaavati gornji tip:
typedef char *pokZnak;

Znakovne nizove sada moemo deklarirati na sljedei nain:


pokZnak uPotrazi = "gDJE mI jE tA pROKLETA cAPSlOCK tIPKA?"
pokZnak indeks;

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

Jednom deklarirani sinonim moe se kasnije iskoristiti u drugim deklaracijama


sinonima. Recimo, ako radimo s poljem od sto znakovnih nizova, pogodno je uvesti
novi tip poljeZnakovnihNizova koji e oznaavati taj tip:
typedef pokZnak poljeZnakovnihNizova[100];

Ako elimo deklarirati varijablu telefonskiImenik, umjesto deklaracije polja od sto


pokazivaa na znak
char *telefonskiImenik[100];

razumljivije emo napisati


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
Test prvoklasne inteligencije sastoji se u
sposobnosti istovremenog dranja dviju
suprotnih ideja u glavi, a da se pritom zadri
sposobnost funkcioniranja.
Scott Fitzgerald (1896-1940)

U ovom poglavlju upoznat emo se s funkcijama: njihovim znaenjem i namjenom,


razlikom izmeu deklaracije i definicije, tipom funkcija i argumentima. Prouit emo
to se moe funkciji prenijeti kao argument, a to ona moe vratiti kao rezultat te kako
funkcije djeluju na objekte koji su deklarirani izvan njena tijela. Upoznat emo
znaenje kljune rijei inline, preoptereenja funkcije i predloka funkcije. Na kraju
emo posebnu panju posvetiti funkciji main() i standardnim funkcijama.

7.1. to su i zato koristiti funkcije


U svakom sloenijem programu postoje nizovi naredbi koji opisuju neki postupak
zajedniki nizu ulaznih vrijednosti. Do sada, ako smo htjeli ponoviti isti postupak s
drugim parametrima, morali smo preslikati sve naredbe na sva mjesta gdje se taj
postupak koristi. Mane takvog pristupa su oite: osim to programski kd postaje
glomazan, oteava se i ispravak pogreaka: primijeti li se pogreka u postupku, ispravak
je potrebno unijeti na sva mjesta gdje smo postupak koristili.
Kako bi se takvo ponavljanje izbjeglo, C++ nudi rjeenje tako da se ponavljani
postupak smjesti u zasebnu cjelinu funkciju, te se pozove na mjestima gdje je to
potrebno. Funkcije se piu kao zasebni blokovi naredbi prije ili poslije glavne funkcije
main(). Poziv funkcije se obavlja tako da se izvoenje glavnog kda trenutno prekine
te se nit izvoenja prenese u funkciju. Nakon to se kda u funkciji izvede, glavni
program se nastavlja od sljedee naredbe iza poziva funkcije (slika 7.1). Gledano
izvana, funkcije se ponaaju poput zasebnih cjelina ije unutarnje ustrojstvo ne mora biti
poznato korisniku funkcije. Pozivatelj mora znati to neka funkcija ini, no nije mu
bitno kako ona to ini. Funkcije imaju parametre ili argumente (engl. parameters,
arguments) koje pozivatelj mora zadati prilikom poziva, te vraaju povratnu vrijednost
(engl. return value) pozivatelju ime ga informiraju o rezultatu svoga rada.
Funkcije je mogue pozvati i iz neke druge funkcije (primjerice poziv funkcije
funkcijaTreca() iz funkcije funkcijaDruga() na slici 7.1), pa ak i iz sebe same.
Svaka se funkcija moe pozivati neogranieni broj puta.
Razbijanjem kda u funkcije, doprinosi se modularnosti programa program se
cijepa na manje cjeline koje je lake koristiti. Kd ujedno postaje itljiviji i razumljiviji.

151

int main(){
// ...
funkcijaPrva();
// ...
// ...
funkcijaDruga();
// ...
// ...
funkcijaDruga();
// ...
funkcijaCetvrta();
// ...
return 1;
}

funkcijaPrva(){
// ...
return;
}

funkcijaDruga(){
// ...
funkcijaTreca();
// ...
return;
}

funkcijaTreca(){
// ...
return;
}

funkcijaCetvrta(){
// ...
return;
}

Slika 7.1. Izvoenje programa s funkcijama

Ilustrirajmo to vrlo jednostavnim primjerom programom za raunanje binomnih


koeficijenata po formuli

p!
p
.
=
r r !( p r )!
Bez koritenja funkcija program bi izgledao ovako:
#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--)
brojnik *= i;
long nazivnik = 1;
for (i = r; i > 1; i--)
nazivnik *= i;
for (i = p - r; i > 1; i--)
nazivnik *= i;

// rauna p!
// rauna r!
// rauna (p-r)!

cout << brojnik / nazivnik << endl;


return 0;
}

152

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;
cout << faktorijel(p) / faktorijel(r) /
faktorijel(p - r) << endl;
return 0;
}

Ovaj kd je vie nego oito pregledniji i razumljiviji od prethodnoga. tovie, funkcije


koje napiemo mogu se pohraniti u biblioteke te se zatim ukljuivati i u programe koje
kasnije piemo, bez potrebe da se kd tih funkcija ponovo pie ili kopira. Nau funkciju
faktorijel() moemo tako spremiti u biblioteku matem. Poelimo li kasnije napisati
program koji treba izraunavati faktorijele (na primjer za rjeavanje zadataka iz
vjerojatnosti), jednostavno emo ukljuiti biblioteku matem u na projekt. Uz
prevoditelje se redovito isporuuju biblioteke s nizom gotovih funkcija koje obavljaju
tipine zadatke kao to su raunanje kvadratnog korijena, pristup datotekama ili
manipulacija znakovnim nizovima. Veina tih funkcija je standardizirana, te e neke
od njih biti opisane kasnije u ovom poglavlju.

7.2. Deklaracija i definicija funkcije


Poput varijabli, i funkcije treba prije prvog poziva deklarirati. Deklaracija funkcije
obznanjuje naziv funkcije, broj i tip parametara te tip njene povratne vrijednosti.
Deklaracija ne sadrava opis to i kako funkcija radi, ona daje tzv. prototip funkcije
(engl. function prototype). Deklaracija funkcije ima oblik:
<povratni_tip> ime_funkcije ( <tip> arg1, <tip> arg2
);

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:
double kvadrat(float x);

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.
Ako se definicija i deklaracija razlikuju, prevoditelj e javiti pogreku. Moraju se
poklapati tip funkcije te broj, redoslijed i tip argumenata.
Imena argumenata u deklaraciji i definiciji funkcije mogu se razlikovati. tovie, u
deklaraciji funkcije imena argumenata mogu se izostaviti, to znai da smo gornju
deklaraciju mogli napisati i kao:
double kvadrat(float);

154

Nakon to je funkcija deklarirana (i eventualno definirana), moemo ju pozivati iz bilo


kojeg dijela programa tako da navedemo naziv funkcije te unutar zagrada ()
specificiramo parametre moemo rei da na naziv funkcije primjenjujemo operator
(). Pogledajmo kako bi izgledao program za ispis tablice kvadrata brojeva u ijem se
glavnom dijelu poziva funkcija kvadrat():
#include <iostream.h>
#include <iomanip.h>
double kvadrat(float);

// deklaracija funkcije

int main() {
for(int i = 1; i <= 10; i++)
cout << setw(5) << i
<< setw(10) << kvadrat(i) << endl;
return 0;
}
double kvadrat(float x) {
return x * x;
}

// definicija funkcije

Uoimo u gornjem kdu razliku izmeu argumenta u pozivu funkcije i argumenta u


definiciji funkcije. Argument x u definiciji je formalni argument (engl. formal
argument), simboliko ime kojim prevoditelj barata tijekom prevoenja tijela funkcije.
Taj identifikator je dohvatljiv samo unutar funkcije, dok za kd izvan nje nije vidljiv.
Kada se program izvodi, pri pozivu funkcije se formalni argument inicijalizira stvarnim
argumentom (engl. actual argument), tj. konkretnom vrijednou u gornjem primjeru
je to vrijednost varijable i. Imena formalnog i stvarnog argumenta ne moraju biti
jednaka. ak, kao to je i sluaj u primjeru, ne moraju biti jednakog tipa uobiajenim
pravilima konverzije stvarni argument e biti sveden na tip formalnog argumenta.
Naravno da u pozivu funkcije stvarni argument moe biti i brojana konstanta.
Broj argumenata u pozivu funkcije mora biti jednak broju argumenata u
definiciji funkcije, a tipovi stvarnih argumenata u pozivu moraju se
podudarati s tipovima odgovarajuih formalnih argumenata u definiciji, ili se
moraju (ugraenim ili korisniki definiranim pravilima konverzije) dati svesti
na tipove formalnih argumenata.
U protivnom e prevoditelj javiti pogreku. Na primjer, na pokuaj poziva gore
definirane funkcije kvadrat() naredbom:
kvadrat(4, 3);

// pogreka: argument vika

prevoditelj e javiti pogreku: Suvini parametar u pozivu funkcije kvadrat(float) .


Ovakva stroga sintaksna provjera poziva funkcija moe zasmetati poetnika. Meutim,
ona osigurava ispravno prevoenje i rad programa: prevoditelj e uoiti pogreku u

155

kdu, ispisati poruku o pogrenom pozivu funkcije i prekinuti postupak generiranja


izvrnog programa. Ako bi prevoditelj, unato pogreki dozvolio generiranje izvrnog
kda, nepravilan poziv funkcije bi izazvao nepredvidiv prekid programa tijekom
njegovog izvoenja.
Stroga sintaksna provjera izvornog kda prilikom prevoenja svodi broj
pogreaka pri izvoenju (engl. run-time errors) na najmanju moguu mjeru.
Upravo da bi se omoguila potpuna sintaksna provjera, deklaracija funkcije mora
prethoditi prvom pozivu funkcije.
Obratimo stoga na trenutak panju strukturi prethodnog programa: nakon
pretprocesorske naredbe #include slijedi deklaracija funkcije kvadrat(). Zatim
slijedi glavna (main()) funkcija unutar koje se poziva funkcija kvadrat(), a na kraju
se nalazi definicija funkcije kvadrat(). Ako bi deklaracija funkcije bila izostavljena,
prevoditelj bi tijekom prevoenja izvornog kda u funkciji main() naletio na njemu
nepoznatu funkciju kvadrat(), te bi prijavio pogreku.
Druga je mogunost da se definicija funkcije kvadrat() prebaci na poetak koda.
U definiciji funkcije je implicitno sadrana i njena deklaracija, te e ona tada biti
deklarirana prije poziva u funkciji main(). Shodno tome, u jednostavnijim programima
je dovoljno sve definicije funkcija staviti ispred funkcije main(), tj. funkciju main()
staviti kao zadnju funkciju u kdu. Meutim, situacija postaje zamrena ako se funkcije
meusobno pozivaju, jedna iz druge.
Svaka definicija je ujedno i deklaracija. Nakon definicije nije dozvoljeno
ponavljati deklaracije.

7.2.1. Deklaracije funkcija u datotekama zaglavlja

U sloenijim programima se izvorni kd obino razbija u vie razliitih datoteka


modula. Osnovni motiv za to je bre prevoenje i povezivanje programa: promijenimo li
kd neke funkcije, bit e potrebno ponovno prevesti samo modul u kojem je dotina
funkcija definirana. Prevoenjem se stvara novi objektni kd promijenjenog modula,
koji se potom povezuje s ve postojeim objektnim kdovima ostalih modula.
Budui da se u tako ralanjenim programima lako moe dogoditi da neka funkcija
bude pozvana iz razliitih modula, neophodno je deklaracije pozivanih funkcija uiniti
dostupnima iz bilo kojeg modula. Stoga se deklaracije funkcija stavljaju u zasebne
datoteke zaglavlja (engl. header files), koje se zatim pretprocesorskom naredbom

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

#include ukljuuju u sve datoteke izvornog kda u kojima se neka od deklariranih


funkcija poziva.
Prikaimo ovakvu organizaciju kda na primjeru apstraktnog programa s dvije
funkcije (nazvat emo ih funkcijaPrva() i funkcijaDruga()) koje se pozivaju iz
glavne (main()) funkcije, a uz to se jedna funkcija poziva iz druge funkcije. Prvo emo
napisati kd kako bi izgledao smjeten u jednu datoteku:
void funkcijaPrva();
void funkcijaDruga();

// deklaracije funkcija

int main() {
// ...
funkcijaPrva();
// ...
funkcijaDruga();
}
funkcijaPrva(){
// ...
}

// definicija funkcije

funkcijaDruga(){
// ...
funkcijaPrva();
// ...
}

// definicija funkcije

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
#include "funk1.h"
#include "funk2.h"
int main() {
// ...
funkcijaPrva();
// ...
funkcijaDruga();
}

// ukljuuje deklaracije funkcija

157

funk1.cpp
void funkcijaPrva(){
// ...
}

// definicija funkcije

funk2.cpp
#include "funk1.h"
void funkcijaDruga(){
// ...
funkcijaPrva();
// ...
}

// definicija funkcije

U gornjim kdovima vano je uoiti pretprocesorske naredbe za ukljuivanje datoteka


zaglavlja funk1.h, odnosno funk2.h, u kojima emo deklarirati funkcije definirane u
funk1.cpp, odnosno funk2.cpp:
funk1.h
extern void funkcijaPrva();

// deklaracija funkcije

funk2.h
extern void funkcijaDruga();

// deklaracija funkcije

Ova ukljuenja datoteka zaglavlja su neophodna zbog zahtjeva da deklaracija funkcije


mora prevoditelju biti poznata prije njenog prvog pozivanja. Budui da se u glavnoj
funkciji prozivaju obje funkcije, na poetku datoteke poglavni.cpp morali smo
ukljuiti obje deklaracije. Takoer, kako se funkcijaPrva() poziva iz
funkcijaDruga(), na poetku datoteke funk2.cpp morali smo ukljuiti deklaraciju
funkcije funkcijaPrva(). Ako se u neku datoteku i ukljui neka suvina datoteka
zaglavlja, nita bitno se nee dogoditi (osim to e se program prevoditi neto dulje).
Kako datoteke zaglavlja u principu ne sadre definicije, nego samo deklaracije, nee
doi do generiranja nepotrebnog kda.
Uoimo da su imena datoteka zaglavlja umjesto unutar znakova < > (manje od vee od), navedena unutar dvostrukih navodnika " ". To je naputak procesoru da te
datoteke prvo treba traiti u tekuem imeniku (direktoriju). Ako ih tamo ne pronae,
pretrauju se imenici koji su definirani u prevoditelju, prilikom njegova instaliranja.

158

Kljuna rije extern ispred deklaracija oznaava da su funkcije definirane u nekoj


drugoj datoteci ona nije neophodna za pravilno prevoenje zaglavlja.
Vrlo je vano dobro organizirati datoteke prilikom razvoja sloenih programa.
Pravilnim pristupom problemu organizacije kda moe se utediti velika koliina
vremena (te ivaca, kave i neprospavanih noi) prilikom razvoja. Organizacija se
dodatno komplicira uvoenjem klasa te predloaka funkcija i klasa. Zbog tih razloga,
organizaciji kda bit e posveeno zasebno poglavlje 14.

7.3. Tip funkcije


Tip funkcije odreuje kakvog e tipa biti podatak koji funkcija vraa pozivajuem
kdu. Tip se funkciji pridjeljuje prilikom deklaracije, tako da se ispred imena funkcije
navede identifikator tipa, na primjer:
double kvadrat(float x);
float kvadratniKorijen(float x);
char *gdjeSiMojaNevidjenaLjubavi();

Funkcija kvadrat() je tipa double, funkcija kvadratniKorijen() je tipa float, a


gdjeSiMojaNevidjenaLjubavi() je tipa char * (ona vraa pokaziva na znak).
Funkcija moe openito biti i korisniki definiranog tipa. Konkretnu vrijednost koju
funkcija vraa odreuje se pomou naredbe return u definiciji funkcije, to moemo
ilustrirati jednostavnim primjerom, funkcijom apsolutno():
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

double budui da se pretvorbom rezultata u naredbi return ne dobiva na tonosti

povratne vrijednosti. Meutim, ima smisla definirati funkciju:


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:
float xNaPetu = kvadrat(x) * kvadrat(x) * x;
double cNaKvadrat = kvadrat(a) + kvadrat(b);

Mogue je ak poziv funkcije smjestiti kao argument poziva funkcije:


float xNa4 = kvadrat(kvadrat(x));

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

int nadnevakUbroj(int dan, int mj, long LjetoGospodnje);


void danUtjednu(int);

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.
Naredba return je obavezna za izlazak iz funkcija koje vraaju neki
rezultat. Za funkcije tipa void naredba return se moe izostaviti u tom
sluaju prevoditelj e shvatiti da je izlaz iz funkcije na kraju njene definicije.
Funkcija tipa void ne moe se koristiti u aritmetikim operacijama, pa emo gore
definiranu funkciju ispisiKvadrat() stoga uvijek pozivati na sljedei nain:
ispisiKvadrat(10);

Nie navedeni pokuaj pridruivanja prouzroit e pogreku prilikom prevoenja:


float a = ispisiKvadrat(10);

// pogreka

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:
void lakonski(int odgovor) {
switch (odgovor) {
case 0:
cout << "Ne";
return;
// 1. return
case 1:
cout << "Da";
return;
// 2. return
}
cout << "Nekada sam bio tako neodluan,"
" a moda i nisam?!";
}
// 3. return

U gornjoj funkciji postoje dvije eksplicitne return naredbe, te podrazumijevani


return na kraju tijela funkcije.
Na kraju, uoimo jednu bitnu razliku kod deklaracija funkcija u programskom
jeziku C++ u odnosu na deklaracije u jeziku C: u programskom jeziku C, tip funkcije u
deklaraciji/definiciji se smije izostaviti u tom sluaju prevoditelj pridruuje toj
funkciji podrazumijevani tip int. Zato sve funkcije u C kdu ne moraju biti deklarirane
prije prvog poziva. Naleti li C-prevoditelj na poziv nedeklarirane funkcije, on e

161

pretpostaviti da je ta funkcija tipa int. Naprotiv, u programskom jeziku C++ tip


funkcije je obavezan i izostanak tipa u deklaraciji, odnosno nailazak na nedeklariranu
funkciju rezultira pogrekom tijekom prevoenja.

7.4. Lista argumenata


Argumenti funkcije su podaci koji se predaju funkciji da ih ona obradi na odgovarajui
nain postupkom odreenim u definiciji funkcije. Argument funkcije moe biti bilo koji
ugraeni ili korisniki definirani tip podatka (objekta), odnosno pokaziva ili referenca
na neki takav objekt. Tip void se ne moe pojaviti u listi argumenata funkcije (budui
da taj tip ne definira nikakvu konkretnu vrijednost). Dozvoljeno je prosljeivanje
pokazivaa na tip void.
7.4.1. Funkcije bez argumenata

Neke funkcije za svoj rad ne iziskuju argumente takve funkcije imaju praznu listu
argumenata, tj. unutar zagrada se u deklaraciji ne navode argumenti. Tako smo u
dosadanjim primjerima funkciju main() deklarirali bez argumenata:
int main() {
// ...
return 0;
}

ANSI standard za programski jezik C zahtijeva da se unutar zagrada kod deklaracije


funkcije bez argumenata navede kljuna rije void:
int main(void) {
// ...
}

Zbog kompatibilnosti, u jeziku C++ je dozvoljen i ovakav zapis. Meutim, dosljedno


gledano kljuna rije void je potpuno suvina, jer bi upuivala da se kao argument
pojavljuje nekakav podatak tipa void.
Funkcije bez argumenata se obino koriste za ispis poruka ili za ispis podataka iji
rezultat ovisi iskljuivo o kdu unutar same funkcije.
7.4.2. Prijenos argumenata po vrijednosti

Uobiajeni nain prijenosa argumenta u funkcije jest prijenos vrijednosti podatka, kao
to smo to ve radili s funkcijama faktorijel(), odnosno kvadrat() u ovom

Ovo je posljedica nedosljednosti nastalih tijekom razvoja programskog jezika C. Naime, u


izvornoj varijanti jezika C, argumenti funkcije nisu se deklarirali unutar zagrada, ve iza liste
parametara, a ispred tijela funkcije.

162

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;
n = DodajSto(n);
cout << "Radio " << n << endl;
return 0;
}

// ispisuje 1
// ispisuje 101

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.
Kada se funkciji argumenti prenose po vrijednosti, tada se njihova vrijednost
u pozivajuem kdu ne mijenja.
Ako su stvarni argument i formalni argument u deklaraciji razliitih tipova, tada se
prilikom inicijalizacije formalnog argumenta na stvarni argument primjenjuju
uobiajena pravila konverzije, navedena u poglavlju 2.
Kao stvarni argument funkcije moe se upotrijebiti i neki izraz. U takvim
sluajevima se izraz izraunava prije poziva same funkcije. Tako bismo tablicu kvadrata
brojeva mogli ispisati i pomou sljedeeg kda:
int i = 0;
while (i < 10) cout << kvadrat(++i) << endl;

Meutim, pri pisanju takvih izraza valja biti oprezan:


Kod funkcija s dva ili vie argumenata, redoslijed izraunavanja argumenata
nije definiran standardom.
Ilustrirajmo to pomou funkcije pow() za raunanje potencije xy, koja je deklarirana u
math.h kao:
double pow(double x, double y);

Gornja injenica prouzroit e da vrlo vjerojatno kd:


int n = 2;
cout << pow(++n, n) << endl;

// promjenjivi rezultat!

preveden na nekom prevoditelju ispisuje kao rezultat 27 (rezultat potenciranja: 33), a na


nekom drugom prevoditelju 8 (23), ovisno o tome da li se prvo izraunava vrijednost
prvog ili drugog parametra.

164

Pri koritenju izraza u pozivima funkcija s vie argumenata treba biti


umjeren. Daleko je sigurnije te izraze izvui ispred poziva funkcije.
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

7.4.3. Pokaziva i referenca kao argument

Ponekad se funkcija ne moe implementirati koritenjem prijenosa po vrijednosti.


Primjerice, pokuajmo napisati funkciju koja e zamijeniti vrijednosti dviju varijabli.
Nepromiljeni programospisatelj bi mogao pokuati taj problem rijeiti na sljedei
nain:
void zamijeni(int prvi, int drugi){
// pogreno
int segrt;
// pomona varijabla
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:
void zamijeniPok(int *prvi, int *drugi) {
int segrt = *prvi;
*prvi = *drugi;
*drugi = segrt;
}

Prilikom poziva, umjesto stvarnih vrijednosti a i b proslijedit emo adrese tih objekata:
zamijeniPok(&a, &b);

165

Pokazivai se, dodue, prenose po vrijednosti, to znai da e formalni argumenti prvi


i drugi sadravati kopije pokazivaa. Meutim, te kopije i dalje pokazuju na iste
lokacije na koje su pokazivali stvarni argumenti. Funkcija je napisana tako da obrauje
vrijednosti preko pokazivaa (zbog toga imamo operator * ispred argumenata prvi i
drugi) to zapravo znai da e se zamjena provesti na lokacijama na koje prvi i
drugi pokazuju. Nakon zavretka funkcije prvi i drugi umiru, no to nam nije
bitno, jer se promjena odvijala (i odrazila) na objektima a i b u pozivajuem kdu.
Ovakav nain prijenosa argumenata postoji u jeziku C, te je to i jedini nain
promjene vrijednosti u pozivajuem kdu. Jezik C++ nudi jo elegantnije rjeenje:
prijenos po referenci (engl. pass by reference) . Umjesto prijenosa pokazivaa, u
funkciju zamijeni() prenijet emo reference na vanjske objekte:
void zamijeniRef(int &prvi, int &drugi) {
int segrt = prvi;
prvi = drugi;
drugi = segrt;
}

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

Meutim, ako funkciji zamijeniRef() umjesto reference na int prenesemo referencu


na neki drugi tip podataka, prevoditelj e samo uputiti upozorenje:

166

float a = 10.;
int b = 3;
zamijeniRef(a, b);

// oprez!

Kako se proslijeeni tip razlikuje od tipa formalnog argumenta (potrebno je obaviti


konverziju float u int), prevoditelj e generirati privremenu varijablu tipa int u
koju e smjestiti osakaeni float. U funkciju e se prenijeti adresa te privremene
varijable. U funkciji e biti provedena zamjena vrijednosti izmeu privremenog objekta
i varijable b. Pri izlasku iz funkcije privremeni objekt se unitava pa e nakon gornjeg
poziva funkcije obje varijable imati vrijednost 10.
U pozivima funkcija koje imaju pokazivae ili reference kao argumente,
tipovi pokazivaa, odnosno referenci koji se prenose moraju tono
odgovarati tipovima u deklaraciji funkcije, jer se za njih ne provode nikakve
konverzije.
Vjerojatno je svakom paljivijem itatelju jasno da se u pozivima funkcije
zamijeniPok() i zamijeniRef() ne mogu kao argumenti navesti brojane
konstante. To je i logino ako znamo da nije mogue odrediti adresu konstante.
Pokazivai i reference se koriste kao argumenti funkcija kada se eli promijeniti
vrijednost objekta u pozivajuem kdu. To je pomalo u suprotnosti s osnovnom idejom
funkcije da su argumenti podaci koji ulaze u nju, a povratna vrijednost rezultat funkcije.
Mijenjaju li se vrijednosti argumenata, kd e postati neitljiviji, jer dolaze do izraaja
popratne pojave. Neupueni itatelj kda bit e prisiljen analizirati kd same funkcije da
bi pohvatao sve promjene koja funkcija provodi na argumentima.
Uvijek kada je to mogue, treba izbjegavati reference i pokazivae kao
argumente funkcija te argumente prenositi po vrijednosti. Time se
izbjegavaju popratne pojave.
Upotreba referenci i pokazivaa je ipak neizbjena kada funkcija treba istovremeno
promijeniti dva ili vie objekata, ili kada se funkciji moraju prenijeti velike strukture
podataka, poput polja (vidi sljedei odjeljak 7.4.5). U takvim prilikama, ako funkcija ne
mijenja vrijednosti argumenata, korisna je navika deklarirati te argumente
nepromjenjivima pomou kljune rijei const.
7.4.4. Promjena pokazivaa unutar funkcije

Poetnici su esto vrlo zbunjeni kad naiu na potrebu da unutar funkcije promijene
vrijednost nekog pokazivaa proslijeenog kao parametar. Na primjer, zamislimo da
elimo napisati funkciju UnesiIme() koja treba rezervirati memorijski prostor te
uitati ime korisnika. Pri tome je sasvim logino ime proslijediti kao parametar. No
postavlja se pitanje kojeg tipa mora biti taj parametar. Prosljeivanje pokazivaa na
znak nee biti dovoljno:

167

void UnesiIme(char *ime) {


ime = new char[100];
cin >> ime;
}
int main() {
char *korisnik;
UnesiIme(korisnik);
cout << korisnik << endl;
delete [] korisnik;
}

// ovo nee raditi

// lo poziv

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:
void UnesiIme(char **ime) {
*ime = new char[100];
cin >> *ime;

// parametar je pokaziva
// na pokaziva
// pristup preko pokazivaa

}
int main() {
char *korisnik;
UnesiIme(&korisnik);
cout << korisnik << endl;
delete [] korisnik;
}

// prosljeuje se adresa
// uvijek poistite za sobom!

Gornji primjer e sada funkcionirati: prosljeuje se adresa varijable korisnik, te se na


taj nain moe promijeniti njen sadraj. No sam program time postaje neitljiviji: u

168

pozivnom kdu je potrebno uzimati adresu, a u funkciji petljati s operatorom *. No


reference spaavaju stvar. Naime, pokaziva na znak je neki tip koji se moe proslijediti
po vrijednosti, a to znai da treba proslijediti referencu na njega. Za one koji su na Vi
sa sintaksom za deklaraciju tipova u jeziku C++, referenca na pokaziva na znak se
biljei char * & deklaracijom kljuno je proitati deklaraciju s desna na lijevo. Evo
kda koji koristi referencu na pokaziva:
void UnesiIme(char * & ime) {
ime = new char[100];
cin >> ime;

// parametar je referenca
// na pokaziva
// pristup preko reference

}
int main() {
char *korisnik;
UnesiIme(korisnik);

// prosljeuje se adresa,
// ali to nije potrebno
// eksplicitno navesti

cout << korisnik << endl;


delete [] korisnik;
}

7.4.5. Polja kao argumenti

Vidjeli smo da kod prijenosa po vrijednosti promjene argumenata unutar funkcije


nemaju odraza na stvarne argumente u pozivajuem kdu. Na prvi pogled, izuzetak od
tog pravila ine polja. Pogledajmo sljedei primjer:
void Pocisti(int polje[], int duljina) {
while (duljina--)
polje[duljina] = 0;
}
int main() {
int b[] = {5, 10, 15};
Pocisti(b, 3);
cout << b[0] << endl
<< b[1] << endl
<< b[2] << endl;
return 0;
}

Neupueni itatelj bi iz gornjeg kda mogao zakljuiti da se polje prenosi po vrijednosti


te da e nakon poziva funkcije Pocisti(), lanovi polja b[] ostati nepromijenjeni.
Meutim, izvoenjem programa uvjerit e se da poziv funkcije Pocisti() uzrokuje
trajno brisanje svih lanova polja. Paljivijem itatelju e odmah biti jasno o emu se
radi: polje se u biti ne prenosi po vrijednosti, ve se prenosi pokaziva na njegov prvi

169

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:
void Pocisti(int *polje, int duljina);

Zadatak. Napiite definiciju ovako deklarirane funkcije Pocisti() koristei aritmetiku


s pokazivaima.

Iako ovakvo ponaanje polja kao argumenta unosi odreenu nedosljednost u jezik, ono
je posve praktine naravi. Zamislimo da treba funkciji kao argument prenijeti polje od
nekoliko tisua lanova. Kada bi pri pozivu funkcije lanovi polja inicijalizirali lanove
novog, lokalnog polja u funkciji, proces pozivanja funkcije bi trajao vrlo dugo, ne samo
zbog operacija pridruivanja pojedinih lanova, ve i zbog vremena potrebnog za
dodjeljivanje memorijskog prostora za novo polje. Osim toga, lokalne varijable unutar
funkcije se smjetaju na stog, posebni dio memorije ija je duljina ograniena. Smjetaj
dugakih polja na stog vrlo bi ga brzo popunio i onemoguio daljnji rad programa.
Prilikom deklaracije je mogue, ali nije potrebno navesti duljinu polja (ograniimo
se za sada na jednodimenzionalna polja) prenosi se samo pokaziva na prvi lan. Zbog
toga, sljedee tri deklaracije gornje funkcije su iste:
void Pocisti(int polje[], int duljina);
void Pocisti(int polje[5], int duljina);
void Pocisti(int *polje, int duljina);

Duljina polja je navedena kao dodatni argument funkciji Pocisti(), to omoguava


da se funkcija moe pozvati za ienje polja proizvoljne duljine.
Znakovni nizovi su takoer polja, tako da za njih vrijede istovjetna razmatranja. To
znai da funkciji treba prenijeti pokaziva na prvi lan. Kako su znakovni nizovi
zakljueni nul-znakom, podatak o duljini niza nije neophodno prenijeti. Na primjer:
int DuljinaNiza(char *niz) {
int i = 0;
while (*(niz + i))
i++;
return i;
}

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

Problem prijenosa polja funkciji postaje sloeniji eli li se prenijeti


viedimenzionalno polje. Ako su dimenzije polja zadane u izvornom kdu, prijenos je
trivijalan:
#include <iostream.h>
#include <iomanip.h>
const int redaka = 2;
const int stupaca = 3;
void ispisiMatricu(float m[redaka][stupaca]) {
for (int r = 0; r < redaka; r++) {
for (int s = 0; s < stupaca; s++)
cout << setw(10) << m[r][s];
cout << endl;
}
}
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:
void ispisiMatricu(float m[][stupaca], int redaka) {
for (int r = 0; r < redaka; r++) {
for (int s = 0; s < stupaca; s++)
cout << setw(10) << m[r][s];
cout << endl;
}
}

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

izvornom kdu, ve se odabiru tijekom izvravanja programa. Sljedei pokuaj zavrit


e debaklom ve kod prevoenja:
void ispisiMatricu(float m[][], int redaka, int stupaca) {
// pogreka
for (int r = 0; r < redaka; r++) {
for (int s = 0; s < stupaca; s++)
cout << setw(10) << m[r][s];
cout << endl;
}
}

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>
void ispisiMatricu(float **m, int redaka, int stupaca) {
for (int r = 0; r < redaka; r++) {
for (int s = 0; s < stupaca; s++)
cout << setw(10) << ((float*)m)[r*stupaca+s];
cout << endl;
}
}
int main() {
int redaka = 2;
const int stupaca = 3;
float (*matrica)[stupaca] = new float[redaka][stupaca];
matrica[0][0] = 1.1;
matrica[0][1] = 1.2;
matrica[0][2] = 1.3;
matrica[1][0] = 2.1;
matrica[1][1] = 2.2;
matrica[1][2] = 2.3;
ispisiMatricu((float**)matrica, redaka, stupaca);
return 0;
}

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.
7.4.6. Konstantni argumenti

Kada se argumenti funkciji prenose po vrijednosti, podaci u pozivajuem kdu ostaju


zatieni od promjena u funkciji. Meutim, neki podaci (poput polja) ne mogu se
prenijeti po vrijednosti, ve ih treba prenijeti preko pokazivaa ili referenci, ime se
izlau opasnosti moebitnih promjena prilikom poziva funkcije. Programer koji e
pisati funkciju e sasvim sigurno voditi rauna o tome da ne mijenja vrijednosti
argumenata ako to nije potrebno. No lako se moe dogoditi da kasnije, prilikom
prepravke funkcije smetne tu injenicu s uma, ili da netko drugi krene u radikalne
zahvate na tijelu funkcije.
Da bi se izbjegle neprilike koje naknadne nepaljive promjene kda mogu
izazvati, preporuljivo je argumente koji se prenose preko pokazivaa ili
referenci, a koji se ne ele mijenjati, uiniti konstantnima.
Ilustrirajmo to na primjeru funkcije DuljinaNiza() koja nam je posluila za
izraunavanje duljine znakovnog niza. Argument niz e postati pokaziva na
nepromjenivi znakovni niz dodamo li u deklaraciji funkcije modifikator const ispred
identifikatora tipa:
int DuljinaNiza(const char *niz);

Svaki pokuaj promjene sadraja tog niza unutar funkcije prouzroit e pogreku
prilikom prevoenja:
int DuljinaNiza(const char *niz) {
// ...
*niz = 0;
// pogreka: pokuaj promjene
return i;
// nepromjenjivog objekta
}

Naravno da treba paziti da se kvalifikator const navede i u deklaraciji i u definiciji


funkcije.
Drugi vaan razlog zato je dobro koristiti kvalifikator const kod deklaracije
argumenata jest taj da se funkciji mogu uputiti kao argumenti podaci koji su deklarirani
kao nepromjenjivi. Da bismo to ilustrirali, deklarirat emo gornju funkciju bez
kvalifikatora const ispred parametra:
int DuljinaNiza(char *niz) {
// ....
}

173

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;
}

ak ako i ne mijenjamo vrijednosti na koje pokaziva pokazuje unutar funkcije


DuljinaNiza(), prevoditelj to ne moe znati. Iz svoje paranoje on e sprijeiti gornji
pokuaj pridruivanja nepromjenivog objekta promjenjivom parametru kako bi se
osiguralo da objekt u svakom sluaju ostane nepromijenjen (lat. object intacta). Zbog
toga ako funkcija ne mijenja svoje parametre, poeljno je to obznaniti javno tako da se
umetne modifikator const u listu parametara.
Naravno da je obrnuto prosljeivanje dozvoljeno: promjenjivi objekt slobodno se
moe pridruiti nepromjenjivom parametru. Time se ne naruava integritet objekta koji
se prosljeuje.
Gornje ponaanje je posljedica standarnih konverzija pokazivaa: pokaziva na
promjenjivi objekt se uvijek moe svesti na pokaziva na nepromjenjivi objekt, dok
obrnuto ne ide bez eksplicitne dodjele tipa.
7.4.7. Podrazumijevani argumenti

Postoje funkcije kojima se u veini poziva prenosi jedna te ista vrijednost argumenta ili
argumenata. Na primjer, u funkciji za raunanje odreenog integrala trapeznom
formulom tipina relativna pogreka rezultata moe biti vrijednost 104. Kako bi se
izbjeglo suvino navoenje eljene pogreke u sluaju da tipina pogreka zadovoljava,
prilikom poziva se pogreka moe izostaviti, a u deklaraciji funkcije navesti
podrazumijevana vrijednost pogreke.
Na poetku ovog poglavlja, u programu za raunanje binomnih koeficijenata (vidi
kd na str. 152) pretpostavili smo da imamo na raspolaganju funkciju faktorijel().
Bez ikakve optimizacije, binomne koeficijente smo raunali doslovno preko formule
p!
p
=
r
r !( p r )!
Svakom imalo bistrijem matematiaru jasno je da se dio umnoka u faktorijeli brojnika
moe pokratiti s (p r)! u nazivniku, tako da se ukupni broj mnoenja u raunu
smanjuje. Na primjer:
5 = 5! = 5 4 3 2 1 = 5 4
2 2! 3! (2 1)(3 2 1) 2 1
Kako iskoristiti tu injenicu u programu za raunanje binomnih koeficijenata? Mogli
bismo napisati dvije funkcije: jednu za raunanje faktorijele (u nazivniku), drugu za

174

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>
long faktorijel(int n, int stojBroj = 1) {
long umnozak = stojBroj;
while (++stojBroj <= n)
umnozak *= stojBroj;
return umnozak;
}
int main() {
int p, r;
cout << "p = ";
cin >> p;
cout << "r = ";
cin >> r;
cout << faktorijel(p, p-r) / faktorijel(r) << endl;
return 0;
}

U pozivu funkcije s oba argumenta, svaki od formalnih argumenata (n, odnosno


stojBroj) e biti inicijaliziran odgovarajuim vrijednostima (p, odnosno p - r).
Izostavi li se drugi argument u pozivu, stojBroj e biti inicijaliziran na vrijednost 1,
kako je zapisano u deklaraciji funkcije.
Napomenimo da e gornja funkcija faktorijel() korektno raditi samo za
pozitivne brojeve (ukljuivo i 0, jer je po definiciji 0!=1), te za sluajeve kada su oba
argumenta vea od 0 i kada je prvi argument vei ili jednak drugome.
Zadatak: Napiite funkciju faktorijel() koja e provjeravati da li argumenti
ispunjavaju ove uvjete, te u protivnom ispisivati poruku o pogreki i vraati vrijednost
1.

Oito je da funkcije s podrazumijevanim argumentom (ili vie njih) znae mogue


odstupanje od pravila da broj argumenata u pozivu funkcije mora biti jednak broju
argumenata u definiciji funkcije. Podrazumijevani argumenti kompliciraju postupak
sintaksne provjere koju provodi prevoditelj. Da bi se otklonile sve eventualne
nedoumice zbog neslaganja u broju argumenata, prevoditelj mora ve prilikom nailaska

175

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
// ...

Podrazumijevane vrijednosti argumenta navode se u deklaraciji funkcije. Ako


su deklaracija i definicija funkcije odvojene, tada se podrazumijevana
vrijednost u definiciji funkcije ne smije ponavljati.
}

Prema tome, u prethodnoj


podrazumijevane vrijednosti:

definiciji funkcije treba

izostaviti

pridruivanje

// deklaracija funkcije:
long faktorijel(int, int = 1);
// definicija funkcije:
long faktorijel(int n, int stojBroj) {
// podrazumjevana vrijednost je
// ve pridruena u deklaraciji
// ...
}

Uoimo kako je u deklaraciji izostavljeno ime formalnog argumenta kojem se


pridruuje podrazumijevana vrijednost. Iako djeluje neobino, u deklaraciji je to
dozvoljeno.
Druga vana stvar o kojoj treba voditi rauna pri pridruivanju podrazumijevanih
vrijednosti jest:

176

Argumenti s podrazumijevanom vrijednou moraju se nalaziti na kraju liste


argumenata funkcije.
To znai da ni u kom sluaju nau funkciju faktorijel() ne moemo deklarirati kao:
long faktorijel(int stojBroj = 1, int n);

// pogreka

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:
void GoodBadUgly(int, const char* = "Clint",
const char* = "Ellie",
const char* = "Lee");

U ovom primjeru ukazano je i kako treba paziti prilikom pridruivanja pokazivaa.


Praznina izmeu * i = je neophodna, jer bi u sluaju da napiemo
void redatelj(const char *= "Sergio");

// pogreka

prevoditelj prijavio pogreku, interpretirajui da se radi o operatoru *=.


7.4.8. Funkcije s neodreenim argumentima

Za neke funkcije je nemogue unaprijed tono znati broj i tip argumenata koji e se
proslijediti u pozivu funkcije. Takve funkcije se deklariraju tako da se lista argumenata
zakljuuje s tri toke (...), koje upuuju da prilikom poziva mogu uslijediti dodatni
podaci. itatelj koji je ikada vidio ili pisao programe u jeziku C zasigurno poznaje
standardiziranu funkciju printf() koja se koristi za ispis podataka (u jeziku C ne
postoje ulazni i izlazni tokovi cin, odnosno cout). Ona se moe koristi za ispis bilo
kojeg broja podataka razliitih tipova. Funkcija printf() u jeziku C++ mogla bi se
deklarirati kao:
int printf(const char* , ...);

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:
int printf(const char* ...);

// isto, ali bez zareza

177

Ovako deklariranu funkciju moemo pozivati na razne naine. Na primjer:


printf("Za C++ spremni!\n");
// ispis samo poruke
printf("Mercedes %d %s\n", broj, oznaka);
// ispis imena, cijelobrojne varijable
// 'broj' i znakovnog niza 'oznaka'

U posljednjem pozivu funkciji printf() prenose se tri argumenta. Prvi argument je


znakovni niz koji, uz tekst koji treba ispisati, sadri i podatke (%d i %s) o tipu preostala
dva argumenta: cjelobrojnom argumentu broj i znakovnom nizu oznaka. Umjesto
simbolikih imena mogli smo navesti i stvarne vrijednosti:
printf("Mercedes %d %s\n", 300, "SE");

Budui da je jedan argument deklariran, izostavljanje argumenata u pozivu funkcije ili


navoenje argumenta koji se ne moe svesti na deklarirani tip prouzroit e pogreku
prilikom prevoenja:
printf();
printf(13);

// pogreka: nedostaje argument


// pogreka: neodgovarajui tip argumenta

Prilikom prevoenja prevoditelj ne ita sadraj poetnog znakovnog niza u kojem su


definirani tipovi podataka koji slijede, tako da ne moe provjeriti da li su tipovi
podataka koji slijede odgovarajue definirani. Na primjer:
printf("2 + 2 je %s\n", 2 + 2);

// nepredvidiv rezultat

Prevoditelj e gornju naredbu prevesti bez poruke o pogreki, ali e izvoenje te


naredbe dati nepredvidivi ispis.
Budui da za funkcije s neodreenim argumentima prevoditelj ne moe
provesti provjeru potpisa funkcije, ispravnost poziva u najveoj mjeri ovisi o
autoru kda.
Kako argument nije deklariran, prevoditelj ne moe provjeriti tipove argumenata u
pozivu funkcije, a time niti provesti konverzije tipova. Stoga se char i short
implicitno pretvaraju i prenose kao int, a float se prenosi kao double (to esto nije
ono to korisnik oekuje).
U dobro pisanim programima koristi se zanemariv broj funkcija s neodreenim
argumentima. Da bi se izbjegle zamke koje uzrokuju funkcije s neodreenim brojem
argumenata i poboljala provjera tipa, valja koristiti funkcije s podrazumijevanim
argumentima i preoptereene funkcije (vidi odjeljak 7.8).
Funkcije s neodreenim argumentima se redovito koriste u deklaracijama funkcija
iz biblioteka pisanih u jeziku C, u kojem nije bilo niti podrazumijevanih argumenata niti
preoptereenja funkcija.

178

Za dohvaanje nespecificiranih argumenata programeru su na raspolaganju makro


naredbe iz standardne stdarg.h datoteke. Ilustrirajmo njihovu primjenu naom
inaicom funkcije printf():
#include <iostream.h>
#include <stdarg.h>
int spikaNaSpiku(char *format, ...) {
va_list vl;
// podatak tipa va_list
va_start(vl, format);
// inicijalizira vl
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

je crna kutija ). Prije nego to zaponemo itati vrijednosti pojedinih argumenata,


objekt v1 je potrebno inicijalizirati pozivom makro funkcije va_start() (takoer iz
stdarg.h). Slijedi petlja koja prolazi poetni znakovni niz, ispituje ga i ispisuje, znak
po znak. Nailaskom na znak %, ispituje se slijedi li ga neki od znakova: d, i, s ili c
(popis se moe proiriti) ako nije niti jedan od navedenih znakova, ispisuje poruku o
pogreki i prekida izvoenje funkcije, vraajui pozivnom kdu nulu. U protivnom,
pomou makro funkcije va_arg() uzima sljedei neimenovani argument. Funkciji
va_arg() je potrebno prenijeti tip parametra koji se eli proitati, to u naem primjeru
nije problem budui da ga znamo na osnovu podataka o formatu iz poetnog niza (ako
bi tip i u ovom trenutku bio nepoznat, najjednostavnije bi ga bilo uitati kao pokaziva
na niz i potom provesti odgovarajua ispitivanja). Makro naredba e vratiti vrijednost
argumenta te e se pomaknuti na sljedei argument iz liste. Prilikom sljedeeg poziva
funkcije va_arg() dobit emo vrijednost sljedeeg argumenta.
Prije povratka iz funkcije nuno je pozvati makro va_end(). Naime, funkcija
va_arg() moe modificirati stog (na koji je, izmeu ostalog, pohranjena i povratna
adresa), tako da povratak u pozivajui kd bude onemoguen; va_end() uklanja te
izmjene na stogu i osigurava pravilan povratak iz funkcije. Prilikom uspjenog povratka,
funkcija vraa pozivnom kdu 1; povratna vrijednost (0 ili 1) iskoritena je ovdje kao
pokazatelj uspjenosti izvoenja funkcije.
Uoimo vitiaste zagrade uz pojedine case grane kojima su naredbe grupirane u
zasebne blokove. One su neophodne, jer u pojedinim granama deklariramo lokalne
(privremene) varijable razliitih tipova, ovisnih o smjeru grananja, odnosno o tipu
podatka koji se oekuje kao sljedei argument.
Evo kako bi mogao izgledati poziv nae velemone inaice standardne funkcije
printf():
spikaNaSpiku("Dalmatinac i %d %s%c", 101, "dalmatine", 'r');

Zadatak. Proirite funkciju spikaNaSpiku() tako da prihvaa i sljedee formate: %f


za brojeve s pominim zarezom, %x za ispis brojeva u heksadekadskom formatu, %o za
ispis brojeva u oktalnom formatu. Takoer osigurajte pravilan ispis posebnih znakova:
\t, \n, \\, \", \'.

7.5. Pokazivai i reference kao povratne vrijednosti


Treba li funkcija pozivnom kdu vratiti neki brojani rezultat, najjednostavnije je
definirati funkciju tako da je onog tipa koji odgovara tipu rezultata, a u return naredbi
navesti eljenu povratnu vrijednost. Na primjer, trebamo li funkciju koja e na osnovi
poznatih odsjeaka na apscisi i ordinati kao rezultat vraati koeficijent smjera pravca
(tj. tangens kuta), definirat emo funkciju

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

double nagib(float x, float y) {


return y / x;
}

Prilikom poziva ove funkcije naredbom


float k = nagib(3.2, -12.1);

na stogu e se stvoriti lokalne varijable x i y kojima e biti pridruene vrijednosti


stvarnih argumenata (3.2, odnosno 12.1). Vrijednosti tih lokalnih varijabli e se potom
podijeliti, a rezultat dijeljenja e biti pohranjen na stog. Prilikom povratka iz funkcije,
pozivajui kd e taj rezultat skinuti sa stoga i pridruiti ga varijabli k. Ovo
pridruivanje podrazumijeva da se sadraj pohranjen na stogu preslikava na mjesto gdje
se nalazi sadraj varijable k, uz eventualne konverzije tipa (u gornjem primjeru double
u float).
Nakon povratka u pozivajui kd svi podaci na stogu koji su privremeno bili
stvoreni prilikom izvravanja funkcije se gube, ali oni nam ionako nisu vie bitni budui
da smo uspjeli dohvatiti i pokupiti rezultat funkcije. Naravno da ako funkciju nagib()
pozovemo tako da njen rezultat ne pridruimo odmah nekoj varijabli, rezultat funkcije
e ostati bespovratno izgubljen, iako ga je ona izraunala i pohranila na stog:
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:
char &dolijepi(const char*, const char*);

Pokuajmo definirati funkciju na sljedei nain:


char *dolijepi(const char *prvi, const char *drugi) {
char spojeni[80];
// lokalno polje znakova
char *indeks = spojeni;
while (*prvi)
*(indeks++) = *(prvi++);
while (*drugi)
*(indeks++) = *(drugi++);
*indeks = '\0';
return spojeni;
// pogreka: pokaziva
// na lokalni objekt
}

181

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.
Kao rezultat funkcije se nikada ne smije vraati referenca ili pokaziva na
lokalni objekt generiran unutar funkcije.
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:
char *dolijepi(const char *prvi, const char *drugi,
char *spojeni);

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.

7.6. ivot jednog objekta


I objekti nisu besmrtni postoji mjesto na kojima se stvaraju i gdje im se dodjeljuje
memorija, te mjesto gdje se unitavaju, odnosno gdje se memorija oslobaa. Ima vie
vrsta objekata s obzirom na njihovo trajanje. Takoer, neki objekti mogu u odreenom
trenutku postojati, ali ne moraju biti dostupni. Zbog toga, C++ jezik specificira nekoliko
smjetajnih klasa (engl. storage classes) koje odreuju nain na koji e se objekt
stvoriti i unititi, te kada e objekt biti dostupan. Smjetajna klasa objekta se specificira
prilikom njegove deklaracije.
7.6.1. Lokalni objekti

U poglavlju o argumentima funkcija spoznali smo da se prilikom poziva funkcije


generiraju privremeni objekti kojima se pridruuju vrijednosti stvarnih argumenata.
Budui da funkcija barata s preslikama podataka, za podatke prenesene po vrijednosti
promjene unutar funkcije nee imati nikakvog odraza na izvornike u kdu koji je
funkciju pozvao. Ovo je velika prednost kada se ele podaci sauvati od nekontroliranih
izmjena u pozivajuim funkcijama.
Takoer, pojedina funkcija u svojem radu moe za realizaciju eljenog algoritma
iziskivati niz pomonih objekata. Ti objekti nemaju smisla izvan same funkcije te je

Neki prevoditelji e na ovakav pokuaj vraanja vrijednosti prijaviti pogreku,


onemoguavajui daljnje prevoenje, odnosno povezivanje.

182

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:
void sortiraj(int *polje, int duljina) {
int i, j;
for (i = duljina - 1; i > 0; i--)
for (j = 0; j < i; j++)
if (polje[j] > polje[j + 1]) {
int priv = polje[j + 1];
polje[j + 1] = polje[j];
polje[j] = priv;
}
}

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:
auto int priv;

Automatska smjetajna klasa se podrazumijeva ako se nita drugo ne specificira, pa se


zbog toga kljuna rije auto koristi vrlo rijetko.
Lokalne varijable mogu imati i registarsku smjetajnu klasu (engl. register storage
class) koja se naznaava tako da se ispred deklaracije navede kljuna rije register:
register int priv;

Time se prevoditelju daje na znanje da se neka varijabla koristi vrlo intenzivno, te da se


radi performansi programa preporua smjetanje varijable u registar procesora umjesto u
glavnu memoriju. Prevoditelj to moe posluati, ali i ne mora. Jedna od situacija u
kojima e prevoditelj gotovo sigurno zanemariti registarsku smjetajnu klasu jest ako se
uzme adresa varijable: podatak u registru nema adresu. Bolji prevoditelji e i bez
eksplicitnog navoenja automatski varijable smjetati u registre ako to mogu uiniti.
7.6.2. Globalni objekti

Ponekad je ipak poeljno da promjene na varijablama imaju odraza i izvan tijela


funkcije. Pretpostavimo da u nekom programu imamo tri cjelobrojne varijable: dan,
mjesec i godina koje sadre podatke o tekuem datumu. Funkcija noviMjesec

183

zaduena je za promjenu varijable mjesec, ali i za promjenu varijable godina na kraju


kalendarske godine:
int noviMjesec(int mjesec, int godina) {
if (++mjesec > 12) {
mjesec = 1;
godina++;
// bez efekta
}
return mjesec;
}
int main() {
int dan = 30;
int mjesec = 1;
int godina = 1997;
mjesec = noviMjesec(mjesec, godina);
return 1;
}

Oito je da e se promjena mjeseca, preko povratne vrijednosti, odraziti u pozivnom


kdu. Meutim, promjena godine e se po povratku u glavnu funkciju izgubiti.
Poneki itatelj e se dosjetiti, te umjesto po vrijednosti datumske varijable prenositi
kao reference ili kao pokazivae. Funkcija e tada baratati (posredno) s izvornicima, pa
povratna vrijednost funkcije noviMjesec nije niti potrebna. Na alost, i ovakvo
rjeenje tek djelomino uklanja problem. Ilustrirajmo to sljedeom situaciju: recimo da
se funkcija noviMjesec ne poziva izravno iz glavne funkcije, ve se prethodno poziva
funkcija noviDan koja poveava dan u tekuem datumu. Oito e funkcija noviDan
tek povremeno pozivati funkciju noviMjesec. ak i kada bismo dan, mjesec i
godina prenosili preko pokazivaa na njih, trebali bismo pokaziva na godina
prenijeti funkciji noviDan samo zato da bi ona taj pokaziva mogla dalje prenijeti
funkciji noviMjesec:
int noviMjesec(int *mjesec, int *godina);
int noviDan(int *dan, int *mjesec, int *godina);

U deklaraciji funkcije e se pojaviti dodatni argumenti koji samoj funkciji nisu


potrebni, to e u svakom sluaju initi kd glomaznijim i neitljivijim. Zamislimo
samo kako bi to sve izgledalo za vie razina poziva funkcija. Suvini argumenti bi se
gomilali, i ve nakon nekoliko razina neke funkcije bi imale desetak argumenata.
Jednostavno rjeenje ovakvog problema je da se objekti koji trebaju biti dohvaani
iz nekoliko razliitih funkcija deklariraju kao globalni. Da bi neka varijabla postala
globalna, treba ju deklarirati izvan tijela funkcije. Vrlo je vjerojatno da e datumske
varijable trebati i drugim funkcijama, pa emo ih definirati ispred funkcije main():
int noviMjesec();

// deklaracija funkcije

184

int dan, mjesec, godina;

// globalne varijable

int main() {
dan = 30;
mjesec = 1;
godina = 1997;
mjesec = noviMjesec();
return 1;
}
int noviMjesec() {
if (++mjesec > 12) {
mjesec = 1;
godina++;
}
return mjesec;
}

Varijable dan, mjesec i godina deklarirane su kao globalne te su sada vidljive


funkcijama main() i noviMjesec() koje slijede iza deklaracije objekata, tako da ih ne
moramo posebno prenositi kao argumente u pozivu funkcija. tovie, u gornjem
primjeru nema potrebe da se vrijednost mjeseca vraa kao rezultat.
Objekti deklarirani izvan funkcije vidljivi su u svim funkcijama ije
definicije slijede u datoteci izvornog kda.
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.
Ako nisu eksplicitno inicijalizirani, globalni objekti e se inicijalizirati na
vrijednost 0. Lokalni objekti koji nisu eksplicitno inicijalizirani poprimit e
neku sluajnu vrijednost.
To znai da e pri izvoenju sljedeeg programa:
#include <iostream.h>
int globalna;

// neinicijalizirana globalna varijabla

int main() {
int lokalna;// neinicijalizirana lokalna varijabla

185

cout << globalna << endl;


cout << lokalna << endl;
}

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.
Globalne varijable treba koristiti im je mogue rjee.
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:
extern int varijabla_u_drugom_modulu;

Objekti deklarirani s extern imaju vanjsko povezivanje (engl. external linkage), to


znai da e biti dostupni drugim modulima prilikom povezivanja. Ako se nita ne
navede, podrazumijevano je da se radi o eksternoj deklaraciji i definiciji.
Poneki objekti mogu biti od koristi samo unutar jednog modula. Takvi objekti se
mogu uiniti statikima, ime se otvara mogunost da i drugi moduli deklariraju objekte
s istim nazivom. Ti objekti su zapravo lokalni za modul i imaju unutarnje povezivanje
(engl. internal linkage). Objekt se moe uiniti statikim tako da se ispred deklaracije
umetne kljuna rije static:
static int SamoMoj = 8;

Konstantni objekti, ako se drukije ne navede, automatski imaju statiku smjetajnu


klasu. Ako takve objekte elimo koristiti u drugim modulima, potrebno je objekt
deklarirati eksternim.
Ako se u funkciji deklarira lokalni objekt istog imena kao i globalni objekt, globalni
objekt e biti skriven. Na primjer:

186

int a = 10;

// globalna varijabla

int main() {
float a = 50;
cout << a << endl;
}

// lokalna varijabla
// ispisuje 50

U gornjem primjeru globalna varijabla a je unutar funkcije main() skrivena: navoenje


samo identifikatora a rezultira pristupom lokalnom objektu. No mogue je pristupiti
globalnom objektu tako da se ispred naziva objekta navede operator :: (dvije dvotoke)
operator za odreivanje podruja (engl. scope resolution operator). Naime, svi
globalni objekti pripadaju globalnom podruju, a ovim operatorom se eksplicitno
odreuje pristup tom podruju:
int a = 10;

// globalna varijabla

int main() {
float a = 50;
cout << a << endl;
cout << ::a << endl;
}

// lokalna varijabla
// lokalna varijabla: 50
// globalna varijabla: 10

7.6.3. Statiki objekti u funkcijama

Lokalne varijable unutar funkcije se inicijaliziraju svaki puta prilikom poziva funkcije.
Meutim, postoje situacije kada je poeljno da se vrijednost neke varijable inicijalizira
samo pri prvom pozivu funkcije, a izmeu poziva te funkcije da se vrijednost uva.
Istina, takva se varijabla moe deklarirati kao globalna, ali e tada biti dohvatljiva i iz
drugih funkcija i izloena opasnosti od nekontrolirane promjene. Rjeenje za ovakve
sluajeve pruaju statiki objekti (engl. static objects). Ove objekte treba razlikovati od
statikih globalnih objekata iz prethodnog odsjeka ovdje se radi o statikim
objektima unutar funkcija.
Dodavanjem kljune rijei static ispred oznake tipa, objekt postaje statiki on
se inicijalizira samo jednom, prilikom prevoenja, te se takav lan pohranjuje u datoteku
zajedno s izvedbenim kdom. Ilustrirajmo to sljedeim primjerom:
#include <iostream.h>
#include <stdlib.h>
void VoliMeNeVoli() {
static bool VoliMe = false;
VoliMe = !VoliMe;
if (VoliMe)
cout << "Voli me!" << endl;
else
cout << "Ne voli!" << endl;

// statiki objekt

187

}
int main() {
int i = rand() % 10 + 1;
while (i--)
VoliMeNeVoli();
}

// sluajni broj od 1 do 10

U glavnoj funkciji pomou standardne funkcije rand() (deklarirane u stdlib.h)


generira se sluajni broj izmeu 1 i 10, te se toliko puta poziva funkcija
VoliMeNeVoli(). U funkciji VoliMeNeVoli() deklarirana je statika varijabla
VoliMe tipa bool. Kod prevoenja ona se inicijalizira na vrijednost false (neistina),
ali se prilikom svakog poziva funkcije VoliMeNeVoli() njena vrijednost mijenja to
e prouzroiti naizmjenini ispis obiju poruka i na zaslonu e se pojaviti slijed oblika
Voli me!
Ne voli!
Voli me!
Ne voli!

Da je izostavljena deklaracija static, varijabla VoliMe bi bila pri svakom ulasku u


funkciju inicijalizirana na istu vrijednost (false) te bi svaki poziv funkcije dao ispis
iste poruke:
Voli
Voli
Voli
Voli

me!
me!
me!
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.

7.7. Umetnute funkcije


esto se koriste funkcije koje imaju vrlo kratko tijelo. Sam poziv, tijekom kojeg se
stvaraju i inicijaliziraju lokalne varijable, za takve kratke funkcije moe trajati znatno
dulje od njenog izvravanja. Ve smo imali primjer funkcije kvadrat() koja se sastoji
samo od jedne naredbe:
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:
inline double kvadrat(float x) {
return x * x;
}

Valja naglasiti da je to samo naputak prevoditelju, a ne i naredba. Prevoditelj e shodno


svojim mogunostima i procjeni taj naputak provesti u djelo ili e ga ignorirati. Svaki
bolji prevoditelj e generirati umetnuti kd za ovakvu funkciju kvadrat(). Meutim,
ako je tijelo funkcije sloenije, posebice ako sadri petlju, uspjeh uklapanja funkcije e
znaajno varirati od prevoditelja do prevoditelja.
Definicija umetnute funkcije mora prethoditi prvom pozivu funkcije u ovom
sluaju prevoditelju nije dovoljna deklaracija funkcije, budui da pri nailasku na prvi
poziv umetnute funkcije ve mora znati ime e poziv te funkcije nadomjestiti (ako e
to uope uraditi). Stoga se definicija umetnute funkcije obino navodi zajedno s
deklaracijama ostalih funkcija i klasa, najee u zasebnoj datoteci (o tome e biti
govora u poglavlju o organizaciji kda).
Koritenjem umetnutih funkcija eliminira se neophodni utroak vremena koji
nastaje prilikom poziva funkcije. Meutim, svaki poziv umetnute funkcije nadomjeta
se tijelom funkcije, to moe znatno poveati duljinu izvedbenog kda. Pretjerana
uporaba umetnutih funkcija moe znaajno produljiti postupak prevoenja, posebice ako
se programski kd rasprostire kroz nekoliko odvojenih datoteka. Promijeni li se tijelo
umetnute funkcije, prevoditelj mora proi cjelokupni kd da bi preslikao tu promjenu na
sva mjesta poziva. Naprotiv, pri promjeni tijela funkcije koja nije umetnuta, postupak
prevoenja treba ponoviti samo u datoteci u kojoj se nalazi definicija dotine funkcije.
Zbog navedenih injenica, oigledno treba biti vrlo obziran s koritenjem inline
funkcija i njihovu uporabu ograniiti na najjednostavnije funkcije. Najee se kao
umetnute definiraju funkcije koje vraaju vrijednost ili referencu na podatkovni lan
unutar nekog objekta o tome e biti vie rijei u poglavlju o klasama.
Jedan od nedostataka umetnutih funkcija je i nemogunost praenja toka
programom za simboliko otkrivanje pogreaka (engl. debugger) u izvedbenom kdu.
Budui da se pozivi funkcije nadomjetaju njenim tijelom, funkcija u izvedbenom kdu
ne postoji, pa se (najee) ne moe ni analizirati programom za simboliko otkrivanje
pogreaka. Zbog toga je, prilikom pisanja programa, najbolje za prvu ruku sve funkcije
napraviti kao obine. Tek nakon to se kd pokae ispravnim, neke se funkcije
redefiniraju kao umetnute, te se ispita pripadajui dobitak u izvedbenom programu.

189

7.8. Preoptereenje funkcija


Ako tip stvarnog argumenta funkcije u pozivu ne odgovara tipu navedenom u
deklaraciji, tada se provodi konverzija tipa i stvarni argument se svodi na tip formalnog
argumenta. Primjerice, u donjem kdu funkcija kvadrat() deklarirana je za argument
tipa double, a poziva se sa cjelobrojnim argumentom:
float kvadrat(float x) {
return x * x;
}
int main() {
int i = 16;
i = kvadrat(i);
}

Prilikom pridruivanja stvarnog argumenta (cjelobrojne konstante 16) formalnom


argumentu koji je tipa float, provodi se konverzija tipa argumenta. Unutar funkcije
barata se argumentom tipa float i kao rezultat se vraa podatak tog tipa. Meutim, u
pozivnom kodu povratna se vrijednost opet svodi na int. Kao to vidimo, pri pozivu
funkcije provedene su dvije suvine konverzije koje u sutini ne utjeu na tonost
rezultata, a jo k tomu produljuju poziv i izvoenje funkcije jer se mnoenje realnih
brojeva odvija dulje nego mnoenje cijelih brojeva.
Oito bi bilo daleko prikladnije definirati funkciju za kvadriranje cijelih brojeva
koja bi baratala sa cijelim brojem i vraala cjelobrojni rezultat. Ta funkcija bi openito
mogla imati bilo koje ime, ali je za razumijevanja kda najbolje kada bi se i ona zvala
kvadrat():
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:
float kvadrat(float);
int kvadrat(int);
int main() {
float x = 0.5;
int n = 4;

// deklaracije preoptereenih
// funkcija

190

cout << kvadrat(x) << endl;


cout << kvadrat(n) << endl;

// float kvadrat(float)
// int kvadrat(int)

prevoditelj prvom pozivu funkcije kvadrat() s argumentom tipa float pridruiti


float inaicu funkcije, dok e drugom pozivu s argument tipa int pridruiti int
inaicu funkcije.
Gledano sa stanovita prevoditelja, jedino to je zajedniko gornjim funkcijama jest
ime. Preoptereenje imena samo olakava programeru da smisleno poistovjeti funkcije
koje obavljaju sline radnje na razliitim tipovima podataka. Ovo je naroito praktino
kod funkcija koje koriste opeprihvaena imena, poput sin, cos, sqrt. Pritom valja
uoiti da se odluka o pridruivanju pojedine funkcije donosi tijekom prevoenja kda;
na sam izvedbeni kd to nema nikakvog odraza te bi izvedbeni kd bi (teoretski) trebao
biti potpuno identian kao i da su pojedine preoptereene funkcije imale meusobno
potpuno razliita imena. Stoga je u biti tonije govoriti o preoptereenju imena funkcija.
Meutim, preoptereenje imena postavlja dodatne probleme za prevoditelja: samo
ime funkcije vie nije dovoljno da bi se jednoznano odredila funkcija koja e se
koristiti za odreeni poziv. to ako u gornju glavnu funkciju ubacimo poziv funkcije
kvadrat() s nekim drugim tipom argumenta (primjerice double) za koji nije
definirana funkcija?
float kvadrat(float);
int kvadrat(int);

// deklaracije preoptereenih
// funkcija

int main() {
double dupliX = 3456.54321e49;
cout << kvadrat(dupliX) << endl;
}

// pogreka!

Budui da nije deklarirana funkcija kvadrat(double), prevoditelj je prisiljen


zadovoljiti se postojeim funkcijama, tj. mora provesti odgovarajuu konverziju tipa
stvarnog argumenta. Na raspolaganju su dvije funkcije kvadrat(): za argument tipa
float i za argument tipa int. Pretvorba double podatka u bilo koji od tih tipova u
openitom sluaju podrazumijeva kresanje podatka tako da su konverzije double u
float, te double u int ravnopravne ovakav poziv e izazvati dvojbu kod
prevoditelja i on e prijaviti pogreku. Slina situacija je i u sljedeem kdu:
long kvadrat(long);
float kvadrat(float);
int main() {
kvadrat(42);
kvadrat(3.4);
}

// pogreka: nedoumica je li
// kvadrat(long) ili kvadrat(float)
// isti problem

191

Brojanim konstantama nije eksplicitno odreen tip pa ih prevoditelj (implicitno)


interpretira kao podatke tipa int, odnosno double, koje prilikom poziva funkcije treba
pretvoriti u long ili float. Pretvorbe u ta dva tipa su ravnopravne tako da prevoditelj
ostaje u nedoumici i prijavljuje pogreke za oba poziva.
Naprotiv, neke pretvorbe tipova (npr. float u double ili int u long) su
trivijalne, tako da e sljedei kd biti preveden bez prijave o pogreki:
long apasalutno(long);
double apasalutno(double);

// samo deklaracije

int main() {
float x = -23.76;
cout << apasalutno(x) << endl;
cout << apasalutno(-473) << endl;
}

Oito je da moraju postojati tono odreena pravila po kojima prevoditelj prepoznaje


koju preoptereenu funkciju mora prilikom pojedinog poziva pridruiti. Pretraivanje se
provodi sljedeim redoslijedom:
1. Trai se funkcija za koju postoji egzaktno slaganje parametara. To podrazumijeva
slaganje kod kojeg nije potrebna nikakva konverzija tipa ili su potrebne samo
trivijalne konverzije (konverzija imena polja u pokaziva, imena funkcije u pokaziva
na funkciju, konverzije tipa u referencu i obrnuto, konverzija tipa u const).
2. Trai se funkcija kod koje treba provesti samo cjelobrojnu promociju (char u int,
short u int, odnosno u njihove unsigned ekvivalente, pobrojenja u int) i
pretvorbu float u double.
3. Trai se funkcija za koju treba provesti standardne konverzije (na primjer int u
double, int u unsigned int, pokaziva na izvedenu klasu u pokaziva na osnovnu
klasu).
4. Trai se funkcija za koju su neophodne korisniki definirane konverzije (o njima e
biti rijei kasnije, u poglavlju o klasama).
5. Trai se funkcija s neodreenim argumentima (...) u deklaraciji
Ako se u nekoj od toaka naie na dvije ili vie funkcija koje zadovoljavaju dotini
uvjet, poziv se proglaava neodreenim i prevoditelj prijavljuje pogreku. Valja imati na
umu da pri traenju odgovarajue funkcije redoslijed deklaracija preoptereenih
funkcija nema nikakav utjecaj na odabir.
Zadatak. Odredite za gornje primjere s pogrekom u kojim je tokama navedenog
redoslijeda nastupila neodreenost.

Ako funkcija ima vie argumenata, tada se gornji postupak nalaenja odgovarajue
funkcije provodi za svaki od argumenata. Ako jedna od funkcija osigurava bolje
slaganje za jedan od argumenata, a barem jednako dobro slaganje za ostale argumente,
ona e biti odabrana.

192

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) {
//...
}
void f(int &refi) {
//...
}

// pogreka: nedovoljna razlika

Isto vrijedi i za razliku izmeu argumenata tipa T, const T i volatile T, koje


prevoditelj nee razluiti. Nasuprot tome, razliku izmeu argumenata tipa T &
(referenca na tip T), const T & i volatile T &, odnosno izmeu T * (pokaziva na tip
T), const T * i volatile T * prevoditelj prepoznaje, tako da se funkcija s obzirom na
te argumente smije preoptereivati.
Pomutnju kod preoptereenja mogu izazvati podrazumijevani argumenti. Dvije
deklaracije parametara koje se razlikuju samo u podrazumijevanim argumentima su
potpuno ekvivalentne. Zbog toga e poziv funkcije f u glavnoj funkciji izazvati
nedoumicu kod prevoditelja:
#include <iostream.h>
void f(int i = 88) {
cout << i << endl;
}
void f() {
cout << "Nita!" << endl;
}
int main() {
f();

// pogreka: f(int) ili f()?

Funkcije se preoptereuju samo prema tipovima argumenata, ali ne i prema


tipu povratne vrijednosti.
}

To znai da je dozvoljeno funkciju kvadrat() preopteretiti funkcijama koje vraaju


jednake tipove podataka:

193

long kvadrat(int);
long kvadrat(long);

Meutim, na pokuaj preoptereenja:


double kub(float);
float kub(float);

// pogreka: obje funkcije


// su kub(float)

prevoditelj e dojaviti pogreku o pokuaju ponovne deklaracije funkcije, budui da su


argumenti obiju funkcija potpuno jednakih tipova.
Pisanje ovako trivijalnih preoptereenih funkcija, u kojima je algoritam potpuno isti
za svaki tip podataka je bitno jednostavnije ako se koriste predloci funkcija (vidi
poglavlje 7.13). Meutim, ponekad se algoritmi za razliite operacije svode pod isto
nazivlje. Primjerice, apsolutna vrijednost (modul) kompleksnih brojeva se rauna prema
bitno drukijem algoritmu nego primjerice realnih ili cijelih brojeva. Takoer,
optimizacija kda ponekad iziskuje koritenje razliitih algoritama. Ilustrirajmo to
funkcijom za potenciranje mocnica(): za decimalne potencije emo definirati funkciju
koja e potenciju raunati pomou formule
e yln( x ) (za x > 0)

dok emo za cjelobrojne potencije koristiti petlju u kojoj emo mantisu mnoiti
odreeni broj puta. Za raunanje prirodnog logaritma i eksponencijalne funkcije koristit
emo standardne funkcije log(), odnosno exp() deklarirane u math.h datoteci
zaglavlja.
#include <iostream.h>
#include <math.h>
double mocnica(double x, double y) {
if (x <= 0.) {
cerr << "Nemoan sam to izraunati!" << endl;
return 0.;
}
return exp(y * log(x));
}
double mocnica(double x, int y) {
int brojac = y > 0 ? y : - y;
double rezultat = 1.;
while (brojac-- > 0)
rezultat *= x;
return y > 0 ? rezultat : 1. / rezultat;
}

U biblioteci matematikih funkcija math ve postoji funkcija pow(double, double) koja


obavlja istu zadau.

194

Ovakvo preoptereenje funkcije osigurava toan rezultat potenciranja cjelobrojnim


eksponentom i za negativne baze:
cout << mocnica(-3, 3) << endl;
cout << mocnica(-3., 3) << endl;
cout << mocnica(-2., 3.) << endl;

// ispisuje -27
// ponovno
// ispisuje poruku

Prvom i drugom pozivu prevoditelj pridruuje funkciju mocnica(double, int), dok


treem pozivu funkciju mocnica(double, double).
Zanimljivo je uoiti da unato (prividno) kraem izvornom kdu funkcije
mocnica(double, double), njeno izvoenje traje znatno dulje. Uzrok tome su
logaritamska i eksponencijalna funkcija koje se u njoj koriste, a izraunavanje kojih
ukljuuje daleko kompleksnije operacije nego to je mnoenje u petlji druge funkcije.
Stoga preoptereenje funkcijom mocnica(double, int) moemo shvatiti i kao
odreenu optimizaciju izvedbenog kda koja za odreene vrijednosti (tipove)
argumenata omoguava bre raunanje potencije.
Iz ovih razmatranja je uoljivo koliko zbog sloenih pravila pronalaenja
odgovarajue funkcije valja biti oprezan prilikom preoptereenja funkcija. U svakom
sluaju treba izbjegavati situacije u kojima nije oito koja e funkcija biti izabrana iz
skupa preoptereenih funkcija.
Spomenimo na kraju da se osim funkcija mogu preoptereivati i operatori. Paljiviji
itatelj e se odmah sjetiti kako su aritmetiki operatori definirani za razliite tipove
podataka i da je shodno tipovima operanada i rezultat razliitog tipa. Podsjetimo se kako
operator + primijenjen na dva podatka tipa int daje rezultat tipa int, a ako se primijeni
na dva podatka tipa float, rezultat e biti toga tipa, itd. Prema tome, vie je nego oito
da je operator zbrajanja preoptereen za parove svih ugraenih tipova podataka u
sluaju da su operandi razliitih tipova, konverzijama se podaci svode na zajedniki tip i
primjenjuje odgovarajui (preoptereeni) operator.
Budui da su operatori definirani za sve ugraene tipove podataka, ne moemo ih
dodatno preoptereivati, osim ako uvodimo korisniki definirane tipove. Stoga emo o
preoptereivanju operatora govoriti nakon to upoznamo klase.
Zadatak. Napiite funkciju usporedi() koja e usporeivati dva argumenta koji joj se
prenose. Ako je prvi argument vei od drugoga, funkcija treba kao rezultat vratiti 1, ako
su oba argumenta meusobno jednaka, rezultat mora biti 0, a ako je drugi argument
vei, rezultat mora biti 1. Preopteretite funkciju tako da za argumente prihvaa
brojeve tipa double, te pokaziva na znakovni niz:
int usporedi(const double prviBr, const double drugiBr);
int usporedi(const char *prviNiz, const char *drugiNiz);

195

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;
}

Pozovemo li tu funkciju naredbom


int nf = faktorijel(3);

funkcija faktorijel() e pozivati samu sebe u tri razine:


1. Prilikom prvog poziva formalni argument e biti inicijaliziran na vrijednost n = 3. U
uvjetnom izrazu e funkcija faktorijel() biti pozvana po drugi put, ali sada sa
stvarnim argumentom 2.
2. U drugom pozivu formalni argument se inicijalizira na n = 2. Argument pozivajue
funkcije (n = 3) jo uvijek postoji, ali je on nedohvatljiv i potpuno neovisan od
argumenta drugopozvane funkcije. U uvjetnom izrazu e funkcija faktorijel()
biti trei puta pozvana, ali ovaj puta sa stvarnim argumentom 1.
3. U treem pozivu funkcije faktorijel() formalni argument se inicijalizira na
vrijednost 1 (u pozivajuim funkcijama formalni argumenti jo uvijek postoje s
neizmijenjenim vrijednostima). Budui da nije zadovoljen uvjet u uvjetnom izrazu,
funkcija faktorijel() se vie ne poziva, ve se kao povratna vrijednost pozivnoj
funkciji alje broj 1.
Slijedi postepeni povratak do poetnog poziva funkcije:
1. Vrijednost 1 koju je vratila treepozvana funkcija mnoi se s vrijednou formalnog
argumenta u drugopozvanoj funkciji (n = 2), te se taj umnoak vraa kao rezultat
prvopozvanoj funkciji.
2. Vrijednost 2 koju je vratila drugopozvana funkcija mnoi se s vrijednou formalnog
argumenta u poetnoj funkciji (n = 3), te se taj umnoak vraa kao rezultat kdu koji
je prvi pozvao funkciju faktorijel() s argumentom 3.
Izvoenje kda moe se simboliki prikazati slikom 7.2.
Mnoge matematike formule se mogu implementirati primjenom rekurzije.
Meutim, kod primjene rekurzija valja biti krajnje oprezan: lokalne varijable unutar
funkcija i adrese s kojih se obavlja poziv funkcije se pohranjuju na stog, pa preveliki
broj uzastopnih poziva funkcije unutar funkcije moe vrlo brzo prepuniti stog i
onemoguiti daljnje izvoenje programa. Daleko je sigurnije (i nerijetko efikasnije)

196

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);

Slika 7.2. Primjer rekurzije.

takve formule implementirati jednostavnim petljama. Rekurzije mogu biti vrlo efikasno
sredstvo za rukovanje posebnim strukturama podataka.

7.10. Pokazivai na funkcije


Zadajmo si sljedei zadatak: elimo napisati openitu funkciju Integral() za
numeriko integriranje pomou trapeznog pravila. Uz standardne ulazne parametre, kao
to su donja i gornja granica integracije, elimo da funkcija Integral() prihvaa i
ime podintegralne funkcije kao parametar. To bi nam omoguilo da jednom napisanu
funkciju Integral() moemo koristiti za bilo koju podintegralnu funkciju, ije emo
ime prenijeti kao parametar prilikom poziva, neto poput:
I1 = Integral(podintegralnaFunkcija, donjaGr, gornjaGr);
I2 = Integral(sin, 0., pi);

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

funkciju, tada izvoenje programa prebacujemo na memorijsku lokaciju na kojoj je


funkcija smjetena. To prebacivanje se odvija preko pokazivaa na tu funkciju, iako to u
dosadanjim razmatranjima nismo eksplicitno naglasili. To znai da je ime svake
funkcije u stvari pokaziva na lokaciju gdje poinje njen kd. Deklaracija funkcije
Integral() koja kao argumente prihvaa pokaziva na funkciju, te donju i gornju
granicu integrala izgledala bi ovako:
float Integral(double (*f)(double), double x0, double xn);

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:
float Integral(double *f(double), double x0, double xn);
// pogreno: deklaracija funkcije f

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 ( xn )
f ( x0 )
+ f ( x1 ) + f ( x2 ) + + f ( xn 1 ) +

2
2

f ( x) dx =&h

x0

gdje je h irina podintervala:


h=

x n x0
n

Naravno da je tonost aproksimacije to bolja to je broj podintervala n vei, jer se u tom


sluaju razlomljeni pravci vie pribliavaju zadanoj funkciji. Radi openitosti emo
postupak automatizirati, tako da e funkcija prvo raunati aproksimaciju samo za jedan
interval:

198

f ( x0 ) f ( x1 )
I 0 = h0
+
,
2
2

h0 = xn x0

a zatim e broj intervala udvostruavati, raunajui sve tonije aproksimacije integrala:


I1 =

h0 f ( x0 )
f ( x2 )
+ f ( x1 ) +

2 2
2

I2 =

h0 f ( x0 )
f ( x4 )
+ 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>
float Integral(double(*f)(double), double x0, double xn,
double relGrijeh = 1.e-5,
long CircusMaximus = 65536L);

f ( x)

x0

x1

x2

xn1

Slika 7.3. Trapezno pravilo.

xn

199

double Podintegralna(double x);


int main() {
cout << Integral(sin, 0, 3.14159) << endl;
cout << Integral(Podintegralna, 0, 1.) << endl;
return 0;
}
float Integral(double(*f)(double), double x0, double xn,
double relGrijeh, long CircusMaximus) {
long n = 1;
double h0 = xn - x0;
double sumaStara = 0.;
double sumaNova = f(x0) + f(xn);
do {
sumaStara = sumaNova / 2.;
sumaNova = 0.;
n *= 2;
for (long i = 1; i < n; i += 2)
sumaNova += f(h0 * i / n + x0);
sumaNova = sumaStara + sumaNova / n;
} while((n <= CircusMaximus) && (sumaNova != 0) &&
(fabs(1. - sumaStara / sumaNova) > relGrijeh));
return sumaNova * h0;
}
double Podintegralna(double x) {
return sin(x) * sin(x);
}

Na poetku kda ukljuena je datoteka zaglavlja math.h koja sadri deklaracije


matematikih funkcija; u programu se iz te biblioteke koriste funkcije sin() i fabs()
koje vraaju sinus, odnosno apsolutnu vrijednost argumenta. Osim toga, obratimo
panju u gornjem kdu na jo dva detalja.
Prvo, primijetimo sufiks L iza podrazumijevane vrijednosti za circusMaximus;
tim sufiksom se broj 65536 eksplicitno proglaava long int konstantom. Kako je taj
broj vei od najveeg mogueg int, u protivnom bi se moglo dogoditi da uslijed
nepredvienih konverzija prevoditelj krivo pohrani taj broj.
Drugo, uoimo uvjet za ponavljanje do-while petlje. On je sloen od tri uvjeta,
navedenih sljedeim redoslijedom:
1. je li n <= CircusMaximus,
2. je li sumaNova razliita od 0, te
3. da li se sumaNova relativno razlikuje od sumaStara za vie od relGrijeh.
Redoslijed je ovdje vaan i moe utjecati na pravilan rad funkcije. Prvo se ispituje
trivijalni uvjet (1) upravo zbog svoje jednostavnosti i kratkoe on je stavljen na
poetak. Tek ako je uvjet (1) ispunjen, prelazi se na uvjet (2), koji je umetnut zbog

200

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):
if ((x > 0) && (log(x) < 10) {
//...
}

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:
if ((x <= 0) || (log(x) < 10) {
//...
}

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:
typedef double (*pokFunkcija)(double);

U gornjoj deklaraciji je pokFunkcija pokaziva na funkciju koja uzima jedan


parametar tipa double i vraa tip dobule. Sada ako elimo deklarirati pokaziva na
funkciju jednostavno moemo napisati
pokFunkcija mojaFunkcija

Takoer, funkciju Integral() bismo mogli deklarirati ovako:


float Integral(pokFunkcija f, double x0, double xn,
double relGrijeh = 1.e-5,
long CircusMaximus = 65536L);

201

Pokazivae na funkciju moemo inicijalizirati tako da im dodijelimo adresu funkcije. Pri


tome, slino kao i kod polja, samo ime funkcije bez zagrada za parametre je
ekvivalentno adresi funkcije:
double xsinx(double x);
mojaFunkcija = xsinx;
mojaFunkcija = &xsinx; // ista stvar kao i gore

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:
double xy(double x, double y);
mojaFunkcija = xy;
// pogreka: nekompatibilni tipovi

Isto vrijedi i za meusobnu dodjelu dvaju pokazivaa. Pokazivaka aritmetika nema


smisla niti je dozvoljena za pokazivae na funkcije.
Podrazumijevani parametri nisu dio potpisa funkcije, te je stoga mogue dodijeliti
adresu neke funkcije pokazivau koji ima drukije podrazumijevane parametre:
void funkcija(int i = 100);
void (*pok)(int) = funkcija;

// dozvoljeno

Pri tome, ako sada funkciju elimo pozvati pomou pokazivaa pok podrazumijevane
parametre ne moemo koristiti:
pok(60);
pok();

// OK
// pogreka: pok nema podrazumijevanih
// parametara

Pokazivau pok moemo ak dodijeliti drugi podrazumijevani parametar:


void (*pok)(int = 80) = funkcija;
pok();
// dozvoljeno te se poziva funkcija(80)

202

7.11.

Funkcija main()

Funkcija main() po mnogim svojstvima odstupa od ostalih funkcija. U prvom


poglavlju smo nauili da svaki C++ program mora sadravati (samo jednu) funkciju
main() kojom se zapoinje izvoenje programa. Takoer, valja naglasiti da se funkcija
main() ne moe pozivati. Postoje dvije mogue varijante funkcije main():
int main() {
//...
}

koja ima praznu listu argumenata (ovaj oblik smo do sada iskljuivo i koristili), ili
int main(int argc, char *argv[]) {
//...
}

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>
int main(int argc, char* argv[]) {
if (argc < 4) {
cerr << "Nedovoljno parametara!"
return 1;
}
float operand1 = atof(argv[1]);
char operacija = *argv[2];
float operand2 = atof(argv[3]);
switch (operacija) {
case('+'):
cout << (operand1 + operand2) <<
break;
case('-'):
cout << (operand1 - operand2) <<
break;
case('*'):
cout << (operand1 * operand2) <<
break;

<< endl;

endl;

endl;

endl;

203

case('/'):
cout << (operand1 / operand2) << endl;
break;
default:
cerr << "Nepoznati operator " << operacija << endl;
return 1;
}
return 0;
}

Za pretvorbu znakovnog niza u realni broj (float) u primjeru je koritena funkcija


atof(), deklarirana u math.h datoteci.
Nazovemo li izvrni program koji se dobiva prevoenjem ovog kda racunaj,
tada e naredba
racunaj 2 / 3

(praznine izmeu naziva programa i prvog operanda, odnosno izmeu operanada i


operatora su obvezatne) iz operacijskog sustava ispisati kvocijent brojeva 2 i 3
(0.666667).
Umetnemo li u gornji kd naredbe za ispise argumenata, izvoenjem programa
gornjom naredbom saznat emo da su argumentima pridruene sljedee vrijednosti:
argc
argv[0]
argv[1]
argv[2]
argv[3]
argv[4]

=
=
=
=
=
=

4
"racunaj.exe"
"2"
"/"
"3"
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
"C:\\knjiga.cpp\\primjeri\\racunaj.exe").
Slijede
programa
(npr.
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() {
// ...
}

U tom sluaju se naredba return ne mora navesti, a prilikom povratka u operacijski


sustav se vraa vrijednost neka nedefinirana vrijednost. Ovakav oblik funkcije main()
nije propisan standardom, ali e ga mnogi prevoditelji bez problema prihvatiti.

7.12. Standardne funkcije


Jezik C++ nema funkcija koje su ugraene u sam jezik, no programeru za obavljanje
itavog niza tipinih operacija redovito na raspolaganju stoji mnotvo funkcija u
bibliotekama koje se isporuuju zajedno s prevoditeljem. Veina tih funkcija je
obuhvaena standardom, tako da se mogu koristiti bez bojazni po prenosivost kda.
Meutim, uz veinu prevoditelja se isporuuju i biblioteke sa specifinim funkcijama.
Koritenje takvih funkcija u kdu e ponekad znatno olakati pisanje programa, ali
treba voditi rauna da se takav kd vrlo vjerojatno nee moi prevesti nekim drugim
prevoditeljem.
Za funkcije iz priloenih biblioteka vrijede ista pravila kao i za funkcije koje sami
definiramo. Izmeu ostalog, to podrazumijeva da funkcija mora biti deklarirana prije
prvog poziva. Priloene biblioteke s funkcijama su obino u objektnom obliku (ve su
prevedene) radi to breg povezivanja izvedbenog kda (izvorni kd se ne isporuuje
radi zatite autorskih prava). Stoga su deklaracije tih funkcija pohranjene u posebnim
datotekama zaglavlja da bi se funkcija iz neke biblioteke mogla pravilno koristiti,
dovoljno je na poetku izvornog kda ukljuiti pripadajuu datoteku zaglavlja
pretprocesorskom naredbom #include.
Demonstrirat emo upotrebu standardnih funkcija programom koji pretvara
koordinate toke u ravnini zadane u pravokutnom koordinatnom sustavu u polarne
koordinate. Taj zadatak se obavlja sljedeim formulama:
r = x2 + y2

y
= arctan
x
Pri tome su x i y pravokutne koordinate toke, a r i radijus i kut radij-vektora toke. U
kdu emo koristiti dvije standardne funkcije: sqrt() koja vraa kvadratni korijen
argumenta, te atan2(double x, double y) koja vraa arkus tangensa za
koordinate toke na apscisi i ordinati. Obje funkcije su deklarirane u datoteci math.h

205

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.
Tablica 7.1. Neke ee koritene standardne datoteke zaglavlja

Naziv

Opis

complex.h
fstream.h
float.h
iomanip.h
iostream.h
limits.h
locale.h
math.h
stdarg.h

Kompleksni brojevi i funkcija


C++ tokovi za datoteni ulaz i izlaz
Podaci o realnim tipovima podataka
Ulazno-izlazni manipulatori za C++ tokove
Osnovni ulazno-izlazni C++ tokovi
Podaci o rasponima vrijednosti cjelobrojnih podataka
Funkcija sa zemljopisno- i jezino-specifinim podacima
Matematike funkcije
Makro funkcije i definicije za poziv argumenata funkcija deklariranih s
neodreenim brojem argumenata (...)
Rutine za konverziju, pretraivanje, sortiranje i sl.
Rutine za rukovanje znakovnim nizovima
Strukture za pohranu podataka o vremenu i datumu, te rutine za njihovu
obradu

stdlib.h
string.h
time.h

206

Koritenje veine matematikih funkcija je intuitivno jasno, tako da ih nema potrebe


detaljnije opisivati. Funkcije za rukovanje znakovnim nizovima iziskuju posebnu panju
zbog naina na koji se oni pohranjuju. Vrlo iroka primjena tih funkcija zahtjeva da im
posvetimo malo vie panje.
7.12.1. Funkcije za rukovanje znakovnim nizovima

Gotovo u svakom programu u kojem se ispisuju poruke, javlja se potreba za rukovanjem


znakovnim nizovima, na primjer kopiranjem ili povezivanjem znakovnih nizova.
Standardni matematiki operatori nisu definirani za znakovne nizove, tako da se (za
razliku od nekih drugih jezika) u programskom jeziku C++ ne mogu vriti jednostavna
pridruivanja ili zbrajanja znakovnih nizova. Primjerice, pokuaj generiranja dva
jednaka znakovna niza pomou operatora pridruivanja u sljedeem primjeru nee dati
oekivani rezultat:
char *zn1 = "Dora";
char *zn2 = zn1;

// preusmjerava pokaziva

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:
char *zn1 = "Dora ";
char *zn2 = "Krupieva";
char *zn3 = zn1 + zn2;

// pogreka: nepravilno
// zbrajanje pokazivaa

Za operacije na znakovnim nizovima programeru je na raspolaganju itav niz funkcija i


makro naredbi deklariranih u datoteci string.h. U njoj su definirane funkcije za
kopiranje nizova, nadovezivanje dvaju nizova, pretraivanje nizova, usporedbu dvaju
nizova, odreivanje duljine niza, pretvorbe velika-mala slova i slino. Upotreba
najkorisnijih funkcija pokazana je u sljedeem kdu:
#include <iostream.h>
#include <string.h>
int main() {
char *prvi = "mali";
char *drugi = "princ";
char *praznina = " ";
int ukupnaDuljina = strlen(prvi) + strlen(praznina) +
strlen(drugi);
char *oba = new char[ukupnaDuljina + 1];
strcpy(oba, prvi);

207

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() izraunava duljinu znakovnog niza. Ne smije se zaboraviti


sljedee:
Funkcija strlen() kao rezultat vraa duljinu niza bez zakljunog nulznaka. 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:
cout << strlen("\"101\"") << endl;

Funkcija strcpy(), ija je deklaracija oblika


char *strcpy(char *kamo, const char *odakle);

preslikava znakovni niz (zajedno sa zakljunim nul-znakom) na koji pokazuje drugi


argument, na lokaciju na koju pokazuje prvi argument (slika 7.4). Povratna vrijednost
char *oba
char *prvi

a l i \0

strcpy(oba, prvi);
char *oba

a l i \0

Slika 7.4. Djelovanje funkcije strcpy().

208

char *oba

a l i \0

char *praznina

\0

strcat(oba, praznina);
char *oba

a l i

\0

Slika 7.5. Djelovanje funkcije strcat().

funkcije je pokaziva na odredite preslikavanja (taj se podatak rijetko koristi). Iz


deklaracije je oito da niz koji se preslikava ostaje nepromijenjen. Meutim, budui da
se preslikavanjem mijenja sadraj memorije na koju pokazuje prvi argument,
neophodno je osigurati dovoljan prostor (ukljuujui i zakljuni nul-znak) u koji e se
izvorni niz preslikati. Na primjer, kd
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:
char *strcat(char *sprijeda, const char *straga);

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

Prije preslikavanja nizova funkcijama strcpy() i strcat() (ali i slinima),


obvezatno treba alocirati dovoljan prostor za odredini niz, zajedno s
njegovim zakljunim nul-znakom.
Zadatak. Zato sljedea naredba nije izvediva:
strcat("Dra", "Kataleni");

// pogreka

Zadatak: to e biti ispisano izvoenjem sljedeeg kda:


char deponij[25];
strcpy(deponij, "kanal");
strcpy(deponij + 3, "tata u F-duru");
cout << deponij << endl;

Funkcija strcmp() je deklarirana kao


int strcmp(const char *prviNiz, const char *drugiNiz);

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 nulznak 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:
char *prvi = "mama";
char *drugi = "Tata";
cout << strcmp(prvi, drugi) << endl;

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

7.12.2. Funkcija exit()

Sloeni programi se sastoje od mnotva funkcija koje se pozivaju iz funkcije main() ili
iz neke druge korisniki definirane funkcije. Struktura meusobnog pozivanja funkcija u
takvim programima nerijetko moe biti vrlo sloena. Tekoe nastupaju ako tijekom
izvoenja programa nastupi pogreka tako da je potrebno trenutano prekinuti izvoenje
programa. Regularni povratak iz funkcija u takvim situacijama moe biti vrlo
mukotrpan ili ak nemogu.
Ilustrirajmo to primjerom programa koji poziva funkciju za raunanje prirodnog
logaritma. Kao to znamo, logaritamska funkcija definirana je samo za pozitivne
brojeve vee od nule. to, meutim, napraviti ako se kojim sluajem u tijeku prorauna
toj funkciji proslijedi nula ili negativan broj? Kako e funkcija za raunanje logaritma
dati do znanja pozivajuoj funkciji da je rezultat nedefiniran, kad ona inae kao rezultat
moe vratiti bilo koji pozitivni ili negativni broj? Nije nam ostala na raspolaganju niti
jedna specijalna vrijednost koja bi signalizirala pozivajuoj funkciji neispravnost
rjeenja.
Jedno rjeenje je trenutano prekinuti izvoenje programa, za to moemo
iskoristiti funkciju exit() deklariranu u biblioteci stdlib.h:
void exit(int status);

Cjelobrojni argument status te funkcije predaje se operacijskom sustavu kao rezultat


programa, ekvivalentno onome koji se vraa u naredbi return na kraju glavne
funkcije. Pogledajmo kako bismo mogli napisati kd za navedeni program:
#include <iostream.h>
#include <math.h>
#include <stdlib.h>
// fja prirodni logaritam
double ln(double x) {
if (x <= 0) exit(1);
return log(x);
}
int main() {
cout << ln(2) << endl;
cout << ln(0) << endl;
cout << ln(5) << endl;
return 0;
}

// poziv funkcije exit()

// ciao da "program main"

U funkciji ln() poziva se standardna funkcija log() iz matematike biblioteke. Prije


poziva te funkcije provjerava se argument i ako je on manji ili jednak nuli, poziva se
funkcija exit(). Zbog toga e se prilikom drugog poziva funkcije ln() (argument
jednak nuli) prekinuti izvoenje programa, a trei poziv funkciji ln() nee niti biti
upuen.

211

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.

7.13. Predloci funkcija


U poglavlju o preoptereenju imena funkcija vidjeli smo kako se isti identifikator moe
koristiti za razliite definicije funkcija preoptereene funkcije su imale jednaka
imena, ali su se razlikovale prema argumentima. Preoptereenje funkcija omoguava
svoenje na zajedniko ime slinih operacija nad razliitim tipovima podataka.
Najoitiji primjer za to je bila funkcija za potenciranje mocnica() koju smo razliito
definirali za cjelobrojne, odnosno decimalne potencije (primjer na str. 193).
Meutim, esto su algoritmi za razliite tipove podataka potpuno identini. Ako se
takav problem rjeava preoptereenje, potrebno je definirati funkciju za svaki mogui
tip argumenta. Funkciju kvadrat() treba ponoviti za sve mogue tipove argumenata
unato injenici da one sadre potpuno isti kd:
inline int kvadrat(int x) {
return x * x;
}
inline float kvadrat(float x) {
return x * x;
}
inline double kvadrat(double x) {
return x * x;
}

U ovakvim sluajevima je umjesto preoptereenih funkcija daleko praktinije koristiti


predloke funkcija (engl. function templates) koji omoguavaju da se jednom definirani
kd prevede vie puta za razliite tipove argumenata. Sintaktiki, deklaracija predloka
funkcije ima oblik:
template <argument_predloka,...> deklaracija_funkcije;

Svaki argument predloka se sastoji od kljune rijei class i imena argumenta, te se


svaki argument mora pojaviti u listi parametara funkcije. Ako je deklaracija funkcije
ujedno i njena definicija, tada je deklaracija predloka ujedno i definicija predloka.
Pogledajmo kako bismo funkciju kvadrat() napisali pomou predloaka i time
izbjegli viestruko pisanje kda:
template <class Tip>
inline Tip kvadrat(Tip x) {

212

return x * x;
}

Ovom deklaracijom/definicijom je funkcija kvadrat() parametrizirana, tako da se tip


argumenta i tip funkcije mogu prema potrebi mijenjati. Prilikom prevoenja, prevoditelj
e za svaki poziv funkcije kvadrat() nadomjestiti Tip odgovarajuim tipom
podatka. Konkretno, sljedei pozivi e uzrokovati generiranje triju razliitih oblika
funkcije kvadrat():
float f = 1.2;
int i = 9;
double d = 3.14159;
cout << kvadrat(f) << endl;
cout << kvadrat(i) << endl;
cout << kvadrat(d) << endl;
cout << kvadrat(1.4) << endl;

//
//
//
//

float kvadrat(float)
int kvadrat(int)
double kvadrat(double)
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
template <class Tip>
void zamijeni(Tip &prvi, Tip &drugi) {
Tip privremeni = prvi;
prvi = drugi;
drugi = privremeni;
}

U ovom poglavlju su predloci funkcija obraeni samo informativno. Kako upotreba


predloaka funkcija posebno dobiva na znaaju uvoenjem klasa i predloaka klasa,
predloci funkcija e vrlo detaljno biti obraeni u poglavlju 10.

7.14. Pogled na funkcije ispod haube


Programeri koji programe piu u nekom viem programskom jeziku, kakav je i jezik
C++, vrlo rijetko trebaju znati ita o organizaciji i radu raunala. Prevoditelj i poveziva
e (ponekad ak uspjeno) prevesti njihov program pisan u jeziku razumljivom ljudimaprogramerima, u strojni kd, jedini jezik koji procesor u raunalu razumije. U tom
strojnom jeziku ne postoje objekti, konstante i funkcije, ve samo memorijske adrese.
Unato injenici da golema veina programera gotovo nikad nee morati ita znati o
organizaciji memorije i o tome kako se strojni kd izvodi, za cjelovito razumijevanje
problematike je ipak dobro razjasniti neke osnovne stvari. itatelji koji su familijarni s
tim pojmovima ili to znanje smatraju suvinim, slobodno mogu preskoiti ovaj odjeljak.

213

Prilikom pokretanja nekog programa, izvedbeni kd se s vanjske jedinice (diska,


diskete, CD-ROM-a) uitava u radnu memoriju raunala (RAM, engl. random access
memory) te se na njega prenosi izvoenje. Prilikom uitavanja i pokretanja programa,
dio memorije se iskoritava za smjetaj samog izvedbenog kda, a dio se koristi za
pohranjivanje podataka. Pritom je dio za pohranjivanje podataka podijeljen na tri
praktiki neovisna podruja: podatkovni segment (engl. data segment), hrpu (engl.
heap) i stog (engl. stack). Podatkovni segment (moe ih biti i vie) je podruje memorije
u kojem su smjetene globalni i statiki objekti. Hrpa je dio memorije koji je dostupan
izvedbenom kdu u bilo kojem trenutku izvoenja u taj dio se pohranjuju dinamiki
alocirani podaci. Stog je dio memorije na koji se pohranjuju lokalni podaci, dostupni
samo funkciji iji se kd trenutano izvodi (osim lokalnih podataka na stog se
pohranjuju jo i podaci potrebni prilikom poziva i povrata iz funkcija, no to emo jo
kasnije razjasniti).
Naravno da se sm program ne moe izvoditi bez mozga cijele akcije sredinje
procesorske jedinice (engl. Central Processing Unit, CPU) ili krae (mikro)procesora.
On interpretira naredbe strojnog kda i poduzima odgovarajue akcije: dohvaa podatke
iz memorije, obavlja operacije s njima i pohranjuje ih natrag u memoriju. Pritom on
koristi svoju internu memoriju registre (engl. registers). Radi se o vrlo skuenoj
memoriji u koju stanu svega osnovni podaci (primjerice int i float), ali ona ionako slui
samo za privremeno pohranjivanje podataka tijekom obrade. Osim nekoliko registara za
podatke koji se obrauju, za pravilan rad programa (a i cijelog raunala) bitna su dva
registra: pokaziva instrukcija (engl. instruction pointer) te pokaziva stoga (engl. stack
pointer). Pokaziva instrukcija u svakom trenutku sadri adresu memorije na kojoj se
nalazi sljedea instrukcija koju procesor mora izvesti. Procesor prilikom dohvaanja
instrukcije prvo oita sadraj pokazivaa instrukcija, iji sadraj upuuje na mjesto gdje
se nalazi sljedea izvedbena naredba. Pomou adrese iz pokazivaa instrukcija procesor
oitava naredbu iz dijela memorije u kojoj je smjeten izvedbeni kd te pristupa njenom
izvoenju. Istovremeno se povea sadraj pokazivaa instrukcija tako da on pokazuje na
sljedeu instrukciju programa.
Naie li se tijekom izvoenja programa na naredbu za skok na neku drugu
memorijsku adresu, procesor e adresu sadranu u instrukciji ubaciti u pokaziva
instrukcija, ime e se izvoenje programa automatski prebaciti na eljenu adresu.
Tekoe nastupaju kod poziva funkcija, jer se nakon izvoenja funkcije valja vratiti na
prvu naredbu iza naredbe koja je pozvala funkciju. Oito je da prilikom poziva funkcije
treba pohraniti adresu naredbe na koju se procesor po povratku iz funkcije mora vratiti.
Valja primijetiti da to pohranjivanje adrese nije trivijalno, budui da se unutar pozvane
funkcije moe pozvati jedna ili vie drugih funkcija, a za svaki od tih poziva treba
pohraniti pripadajue povratne adrese.
Vjerojatno je svakom jasno da e prilikom uzastopnih poziva funkcija jedne unutar
druge, prvo biti potrebna adresa vezana uz zadnji poziv funkcije, a tek potom ostalih
funkcija, redoslijedom obrnutim od redoslijeda pozivanja funkcija. Zbog toga se
povratne adrese pohranjuju na stog posebni dio memorije u kojem se podaci
dohvaaju obrnutim redoslijedom od redoslijeda kojim su tamo ostavljani. To znai da
e podatak koji je na stogu bio ostavljen posljednji, biti dohvatljiv kao prvi, a podatak
koji je bio ostavljen prvi bit e dohvatljiv posljednji. Takva struktura se esto oznaava

214

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
Ja ne vjerujem u klasne razlike, ali se sreom
miljenje mog sobara po tom pitanju razlikuje.
Marc, strip u londonskom The Times

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.

8.1. Kako prepoznati klase?


Jezik C++ uvodi kao znaajnu konceptualnu promjenu u nainu programiranja novi tip
podataka: klase (engl. class). One su temelj objektno orijentiranog pristupa
programiranju. Sama ideja uvoenja objekata u programiranje, iako revolucionarna,
dola je zapravo analiziranjem naina na koji funkcionira stvarni svijet. Ako se malo
pomnije udubimo u tokove podataka oko nas, doi emo do zakljuka da se mnoge
stvari mogu vrlo jednostavno modelirati pomou objekata. Objekt (engl. object) je
naziv za skup svojstava koja moemo objediniti u smislenu cjelinu. Pravila koja
propisuju od ega je pojedini objekt sagraen te kakva su njegova svojstva nazivaju se
klasama. Vrlo je vano uoiti razliku izmeu klase i objekta: klasa je samo opis, dok je
objekt stvarna, konkretna realizacija napravljena na temelju klase.
Objekti meusobno izmjenjuju informacije i trae jedan od drugoga usluge. Pritom
okolina objekta nita ne mora znati o njegovom unutarnjem ustrojstvu. Svaki objekt ima
javno suelje (engl. public interface) kojim se definira njegova suradnja s okolinom.
Ono odreuje koje informacije objekt moe dati te u kojem formatu. Takoer su
definirane i sve usluge koje objekt moe pruiti.
Interno se objekt sastoji od niza drugih objekata i interakcija meu njima. Nain na
koji se reprezentacija objekta ostvaruje jest implementacija objekta (engl.
implementation). Ona je najee skrivena od okoline kako bi se osigurala
konzistentnost objekta. Klasa se, dakle, sastoji od opisa javnog suelja i od
implementacije. To objedinjavanje javnog suelja i implementacije naziva se
enkapsulacija (engl. encapsulation).

216

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

Slika 8.1. Objektni preludij za klavir i klavir-timera u C++ duru

Obratimo nadalje panju na fenomen koji pokazuje da se


jedan objekt moe klasificirati postupno, od
jednostavnijeg prema sloenom. Moemo uvesti opu
Tipka
klasu tipki koja definira tipku kao ploicu koja se moe
pritisnuti i time proizvesti ton (slika 8.2). Nadalje,
+ boja
pojedina se tipka moe klasificirati kao crna ili bijela. Na
kraju, moe se uvesti klasa za svaku pojedinu tipku, na
primjer klasa za tipku G3. Ona definira objekt kao tipku
Tipka,
bijele boje, odreuje njenu poziciju na klavijaturi, te
bijela
tono definira ton koji se dobije pritiskom na nju. Ovime
smo izveli hijerarhiju klasa koja izgleda kao stablo: u
+ poloaj
korijenu stabla se nalazi openita klasa tipki koja
definira opa svojstva tipke. Klase crnih i bijelih tipaka
se izvode iz openite klase. To izvoenje se obavlja tako
Tipka,
da se zadre opa svojstva tipke koja se proiruju
bijela, G3
dodatnim svojstvima, primjerice bojom tipke. Neka
svojstva osnovne klase, kao to je veliina tipke,
redefiniraju se u novim klasama. Postupak kojim se
Slika 8.2. Nasljeivanje
izvodi nova klasa zove se nasljeivanje (engl.
inheritance) i jedan je od kljunih elemenata objektno
orijentiranog dizajna.
Tipka G3 se moe promatrati kao instanca klase G3. No njena specifina svojstva
koja ju svrstavaju u klasu G3 se po potrebi mogu zanemariti te se tu tipku moe
promatrati sa stajalita koje je definirano klasom bijelih tipki. Pri tome se zanemare
svojstva kao to je precizna definicija tona koji se proizvodi pritiskom na tipku pa se
tipka promatra samo sa stajalita boje. Moe se ii i dalje, te se ista tipka moe
promatrati sa stajalita osnovne klase, dakle, samo kao objekt koji se moe pritisnuti i

218

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 klavirtimera 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.

8.2. Deklaracija klase


U C++ terminologiji klasa nije nita drugo nego poseban tip podataka koji je potrebno
deklarirati prije poetka koritenja. Deklaracija se sastoji od dva osnovna dijela:
zaglavlja i tijela. Zaglavlje se sastoji od kljune rijei class iza koje slijedi naziv klase,
a tijelo slijedi iza zaglavlja i omeeno je parom vitiastih zagrada:
class naziv_klase { // ovo je zaglavlje
// ovdje dolazi tijelo
};

Klasu klavira moemo deklarirati ovako:

219

class Klavir {
// ...

Deklaracija klase se uvijek zavrava znakom ; (toka-zarez).

};

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:
Klavir Steinway, Petroff;

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.
8.2.1. Podatkovni lanovi

Svaka klasa moe sadravati podatkovne lanove. Evo primjera klase koja moe biti dio
grafikog suelja i definira objekt Prozor:
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 Izbornik; // ovo je deklaracija imena unaprijed
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.
8.2.2. Dohvaanje lanova objekta

Kako bi lanovi objekta bili od koristi, mora postojati nain kojim okolni svijet moe
pristupiti pojedinom lanu objekta. Za to se koriste operatori za pristup lanovima
(engl. member selection operators).

221

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

Ako imamo pokaziva na objekt klase Racunalo, lanovima se pristupa pomou


operatora -> (minus i vee od), tako da se s lijeve strane operatora navede pokaziva, a
s desne strane naziv lana:
Racunalo *pokRacunalo = &mojKucniCray;
pokRacunalo->megahertza = 16;

Za pristup lanovima preko objekata i referenci koristi se operator . (toka),


a u sluaju pristupa preko pokazivaa operator -> (minus i vee od).

8.2.3. Funkcijski lanovi

Dok su podatkovni lanovi klase analogni lanovima struktura poznatih jo iz jezika C,


u jeziku C++ elementi klase mogu biti i funkcijski lanovi (engl. member functions),
ponegdje jo nazvani i metodama (engl. methods). Oni definiraju skup operacija koje se
mogu obaviti na objektu.
Uzmimo za primjer program koji intenzivno koristi vektorski raun. Radi
jednostavnosti promatrat emo samo sluaj vektora u ravnini. Svaki vektor se moe

222

prikazati jednim objektom klase Vektor. Pratit emo vektore u Descartesovom


koordinatnom sustavu te e svaki vektor biti predstavljen pomou dva realna broja: ax
kao projekcija na os x i ay kao projekcija na os y. elimo li ostvariti mnoenje vektora
skalarom, to bi se u standardnom proceduralnom C-u moglo obaviti na sljedei nain:
struct Vektor {
float ax, ay;
};
void MnoziVektorSkalarom(struct Vektor v, float skalar) {
v.ax *= skalar;
v.ay *= skalar;
}
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);
};
void Vektor::MnoziSkalarom(float skalar) {
ax *= skalar;
ay *= skalar;
}

Definicija jednog vektora i njegovo mnoenje u tom sluaju izgleda ovako:


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

Pristup pojedinim funkcijama klase se obavlja ve opisanim operatorima za pristup


lanovima (odjeljak 8.2.2). S lijeve strane operatora se navodi objekt, referenca ili
pokaziva na objekt, dok se s desne strane nalazi naziv funkcijskog lana. Iza naziva u
zagradi je potrebno navesti stvarne argumente.
Prilikom poziva funkcije MnoziSkalarom() nije potrebno navoditi kao parametar
vektor na koji se mnoenje odnosi, jer se on ve nalazi s lijeve strane operatora za
pristup. lanovi ax i ay koji se pozivaju u funkciji e zapravo biti lanovi varijable v. U
nastavku moemo definirati dodatne vektore kao
Vektor normala, a2;

Njih mnoimo skalarima na sljedei nain:


normala.MnoziSkalarom(6.7);
a2.MnoziSkalarom(4.0);

Naziv funkcijskog lana mora biti jedinstven u podruju imena unutar klase.
To znai da moemo imati obinu funkciju MnoziSkalarom() izvan klase Vektor,
koja je potpuno nezavisna od funkcijskog lana klase Vektor. No unutar klase ne smije
postojati lan ili ugnijeena klasa istog imena. Nazivi pridruenih funkcija nisu vidljivi
izvan klase, tako da poziv
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.
8.2.4. Kljuna rije this

Moda se pitate kako uistinu funkcionira pozivanje funkcijskih lanova. Naposljetku,


raunalo na svom najniem nivou uope ne zna nita o objektima i funkcijama. Ono
barata najjednostavnijim pojmovima kao to su adrese, pokazivai i brojevi. Objekti su
podatkovni tipovi vrlo visokog nivoa apstrakcije te se moraju moi predstaviti
elementarnim raunalnim tipovima podataka.
Trik je u tome to se prilikom poziva svakog funkcijskog lana klase kao skriveni
parametar prosljeuje pokaziva na objekt kojemu taj lan pripada. Tom skrivenom
parametru se moe pristupiti unutar same funkcije pomou kljune rijei this.
Prevoditelj prilikom analiziranja funkcije MnoziSkalarom() nju zapravo interpretira
kao
void Vektor::MnoziSkalarom(float skalar, Vektor *this) {
this->ax *= skalar;
this->ay *= skalar;
}

a poziv funkcijskog lana nad objektom v interpretira kao


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);
};
Vektor &Vektor::MnoziSkalarom(float skalar) {

225

ax *= skalar;
ay *= skalar;
return *this;
}
Vektor &Vektor::ZbrojiSa(float zx, float zy) {
ax += zx;
ay += zy;
return *this;
}

Ovime je omogueno da u jednom redu pomnoimo neki vektor sa skalarom te mu


dodamo neki drugi vektor, na primjer:
Vektor v;
v.ax = 4;
v.ay = 5;
v.MnoziSkalarom(4).ZbrojiSa(5, -7);

Operator . se izvodi slijeva na desno ime se postie eljeni redoslijed operacija.


Kljuna rije this se takoer esto koristi prilikom razvoja objekata koji slue kao
spremite drugih objekata, kao na primjer dvostruko povezana lista (pojam vezanih lista
e biti detaljnije objanjen u odjeljku 9.2):
class ElementListe {
private:
ElementListe *Sljedeci, *Prethodni;
public:
void Dodaj(ElementListe *Novi);
// ...
};
void ElementListe::Dodaj(ElementListe *Novi) {
Novi->Sljedeci = Sljedeci;
Novi->Prethodni = this;
Sljedeci->Prethodni = Novi;
Sljedeci = Novi;
}

8.2.5. Umetnuti funkcijski lanovi

Ako je funkcija kratka tako da njena definicija stane u nekoliko redaka, onda je njenu
definiciju mogue navesti unutar same deklaracije klase. Time dobivano umetnutu
definiciju (engl. inline definition) funkcijskog lana. Prepiimo nau klasu Vektor tako
da funkcija MnoziSkalarom() bude umetnuta.

226

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

Funkcijski lan definiran unutar deklaracije klase je uvijek umetnut, bez


eksplicitnog koritenja kljune rijei inline.
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);
};
void Vektor::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.
8.2.6. Dodjela prava pristupa

U prethodnim primjerima paljiv itatelj je sigurno primijetio upotrebu kljune rijei


public koja do sada nije bila objanjena. Ona slui za definiranje prava pristupa
lanovima klase. Svaki lan moe imati jedan od tri mogua naina pristupa: javni
(engl. public), privatni (engl. private) i zatieni (engl. protected). Prava pristupa se
dodjeljuju tako da se u tijelu klase navede jedna od tri kljune rijei public, private i
protected iza kojih se stavlja dvotoka. Svi elementi, dakle funkcijski i podatkovni
lanovi te deklaracije ugnijeenih klasa koji slijede iza te rijei imaju pravo pristupa

227

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.
Ako se pravo pristupa ne navede eksplicitno, za klase se podrazumijeva
privatni pristup.
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;
void Pristup::Funkcija1(int brojac) {
// unutar funkcijskog lana objekta moe se
// pristupiti svim lanovima ravnopravno
a = brojac;
c = a + 5;
Funkcija2();
}
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;
}

Zadatak. Zato sljedea klasa nije pretjerano korisna:


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.

8.2.7. Formiranje javnog suelja koritenjem prava pristupa

Osobi koja se po prvi put susree sa C++ jezikom moe se uiniti da su komplikacije
oko prava pristupa nepotrebne, te da samo unose dodatnu entropiju u ve ionako
dovoljno kaotian nain programiranja, koji vie slii Brownovom gibanju. To je i
donekle tono u programima od nekoliko stotina redaka koji se napiu u jednom
popodnevu. No prilikom razvoja sloenih aplikacija sa stotinama tisua linija kda gdje

229

cijeli programerski tim radi na projektu, prava pristupa omoguavaju programerima da


odrede to je dostupno suradnicima koji koriste objekte, a to njima samima.
lanovi klase s javnim pristupom formiraju javno suelje objekta. Do javnog
suelja programer dolazi analizom uloge pojedinog objekta i naina njegovog
koritenja. Ono se zatim moe obznaniti suradnicima koji tono znaju to objektu
trebaju pruiti te to od njega mogu dobiti. Sadraj objekta za njih predstavlja crnu
kutiju. Implementaciju klase programer pie na osnovu javnog suelja, ime javno
suelje postaje neovisno o implementaciji. Kasnije se analizom moe ustanoviti da neka
druga implementacija omoguava bolje performanse programa. Objekt se moe

Dobar pristup objektno orijentiranom programiranju nalae da se javno


suelje objekta odvoji od njegove implementacije.
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);
};

Definicija funkcijskog lana MnoziSkalarom() je izostavljena jer se ne mijenja.


Implementacija klase pretpostavlja da se vektor pamti u Descartesovim koordinatama.
Javno suelje nita ne govori o koordinatama, ono samo omoguava da se svakom
vektoru utvrdi njegova projekcija na x i na y os, to je mogue neovisno o
koordinatnom sustavu u kojem je vektor zapamen. Sada je mogue promijeniti
koordinatni sustav u polarni, ustanovi li se da takav prikaz omoguava bolja svojstva
prilikom izvoenja. I na temelju takvog naina pamenja vektora mogu se
implementirati svi elementi javnog suelja. Funkcijski lanovi DajX(), DajY() i
PostaviXY() su napisani kao umetnuti, kako bi se postiglo bre izvrenje programa.
Veina suvremenih prevoditelja e generirati isti kd kao da se direktno pristupa
lanovima ax i ay tako da se performanse programa ne degradiraju.
Umetnuti funkcijski lanovi se vrlo esto koriste za dohvaanje privatnih
podatkovnih lanova kako bi se implementacija razdvojila od javnog
suelja. Kako se time program ne bi usporio, poeljno je koristiti umetnute
funkcijske lanove.

230

Zadatak. Modificirajte klasu Vektor tako da se vektori pamte u polarnom


koordinatnom sustavu. Dodajte nove elemente javnog suelja za postavljanje i itanje
komponenata vektora u tom sustavu, ali zadrite i sve stare elemente.
8.2.8. Prijatelji klase

Ponekad moe biti nuno da neka funkcija, funkcijski lan ili pak cijela klasa ima pravo
pristupa privatnim i zatienim lanovima neke druge klase. Pojedina klasa u tom
sluaju moe dodijeliti eljenoj funkciji, funkcijskom lanu neke klase ili itavoj klasi
pravo pristupa vlastitim privatnim i zatienim lanovima. Dio programa kojemu je
dodijeljeno pravo pristupa zove se prijatelj klase (engl. a friend to a class) koja mu je to
pravo dala.
Definirajmo za primjer funkciju koja e zbrajati dva vektora. Ona bi to mogla
uiniti pomou javnog suelja vektora, no znatno je prirodnije pristupati direktno
lanovima klase Vektor. Klasa se tada moe redefinirati ovako:
class Vektor {
friend Vektor ZbrojiVektore(Vektor a, Vektor b);
private:
float ax, ay;
public:
// ...
};
Vektor ZbrojiVektore(Vektor a, Vektor b) {
Vektor c;
c.ax = a.ax + b.ax;
c.ay = a.ay + b.ay;
return c;
}

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);
// ...
};
Vektor Matrica::MnoziVektorom(Vektor &v) {
Vektor rez;
rez.ax = a11 * v.ax + a12 * v.ay;
rez.ay = a21 * v.ax + a22 * v.ay;
return rez;
}

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

deklaraciji te na taj nain postaju umetnuti. Na primjer, funkciju ZbrojiVektore()


moemo definirati ovako:
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:
// ...
};

Funkcija ZbrojiVektore() e biti umetnuta, to osigurava njeno brzo izvoenje. Iako


je ona definirana unutar klase, vano je primijetiti da ona nije lan klase ovakav nain
pisanja funkcije samo nam omoguava da utedimo jednu kljunu rije inline.
Funkcija je inae posve jednaka kao da je napisana izvan klase.
Funkcijski lanovi druge klase se ne mogu definirati unutar friend deklaracije. To
je i razumljivo, jer se oni moraju definirati unutar klase kojoj pripadaju.
Zadatak. Deklarirajte hipotetsku klasu Covjek, te klasu Pas. Pri tome klasu Pas
postavite prijateljem klase Covjek. Je li time klasa Covjek prijatelj klase Pas?
Zadatak. Deklarirajte klasu Tocka s privatnim podatkovnim lanovima x i y koji e
pamtiti koordinatu toke u pravokutnom koordinatnom sustavu. Napiite funkciju za
raunanje udaljenosti toke od pravca koja e biti prijatelj i klase Pravac (vidi zadatak
na stranici 228) i klase Tocka, pristupat e izravno njihovim podatkovnim lanovima, te
e biti ovako deklarirana:
float UdaljenostTockaPravac(Pravac &p, Tocka &t);

8.3. Deklaracija objekata klase


Iako je stvaranje objekata klase ve bilo pokazano na primjerima, u ovom odjeljku
emo tome posvetiti dodatnu panju.
Deklaracija klase ne uzrokuje nikakvu dodjelu memorije memorija se dodjeljuje
tek kad se deklarira konkretan objekt. Klasa nije nita drugo nego tip podataka, pa
deklaracija objekata slijedi uobiajenu sintaksu gdje se iza identifikatora tipa navode
identifikatori objekata odvojeni zarezom:
Vektor strelicnik;
Vektor normala = strelicnik, polje[10];
Vektor *pokStrelicnik = &strelicnik, &refNormala = normala;

233

Ovom naredbom je definirano pet objekata. Prvi nema inicijalizacijski kd pa elementi


vektora strelicnik po njegovom stvaranju imaju vrijednosti koje su navedene u
podrazumijevanom konstruktoru klase (vidi odjeljak 8.4.2). Objekt normala ima
inicijalizator koji kae da se objekt inicijalizira tako da bude logiki jednak objektu
strelicnik. Ako klasa drukije ne specificira, inicijalizacija se provodi tako da se
meusobno pridrue svi elementi objekta strelicnik objektu normala. Klasa moe
definirati poseban postupak za inicijalizaciju pomou konstruktora kopije (vidi odjeljak
8.4.4). Objekt polje je zapravo polje od deset vektora. Mogu se definirati pokazivai i
reference na objekte. Tako je pokStrelicnik pokaziva na vektor i inicijalizira se
tako da pokazuje na objekt strelicnik. Referenca refNormala se inicijalizira tako
da pokazuje na objekt normala. Ponovo napominjemo da se referenca uvijek mora
inicijalizirati prilikom deklaracije.
Naziv objekta mora biti jedinstven u podruju imena u kojem je deklaracija
navedena. To znai da se za naziv objekta ne moe koristiti naziv neke postojee klase
ili nekog drugog objekta.

8.4. Stvaranje i unitavanje objekata


Kao to je ve reeno, klasa moe sadravati funkcijske lanove koji imaju posebne
namjene. Oni se pozivaju prilikom stvaranja i unitavanja objekata, kopiranja objekata,
prosljeivanja parametara i slino.
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.
Dodajmo klasi Vektor konstruktor koji e svaki novostvoreni vektor inicijalizirati na
nul-vektor:
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;
}

Kao to vidimo u gornjem primjeru, konstruktor klase moe biti preoptereen


definirana su dva konstruktora: Vektor::Vektor() koji nema parametara te
Vektor::Vektor(float, float). Naime, pojedina klasa moe biti inicijalizirana na
razliite naine. Za svaki od njih imat emo zasebni konstruktor kojih uzima specifine
parametre. Prvi konstruktor inicijalizira vektor na nul-vektor, dok drugi za parametre
uzima dva realna broja ime se vektor inicijalizira na eljenu vrijednost. Prilikom
preoptereenja konstruktora vrijede ista pravila kao i za preoptereenje obinih
funkcija. Ukratko, mora biti mogue jednoznano razluiti koji konstruktor je pozvan
na osnovu proslijeenih mu parametara. Konstruktor moe imati i podrazumijevane
parametre.
Konstruktor se automatski poziva prilikom stvaranja objekta klase. Pri tome se
parametri konstruktora navode nakon naziva objekta u zagradama. Na primjer, vektor
inicijaliziran na vrijednost (3.0, 7.0) e se stvoriti na sljedei nain:
Vektor mojInicijaliziraniVektor(3.0, 7.0);

Nakon alokacije memorijskog prostora za objekt, pozvat e se konstruktor kojemu e


se proslijediti parametri navedeni u zagradama i konstruktor e provesti inicijalizaciju.
Ako se eli pozvati konstruktor bez parametara, ne smije se navesti par ()
nakon naziva objekta. Prevoditelj to ne bi shvatio kao deklaraciju objekta,
ve deklaraciju funkcije bez parametara koja vraa objekt.
U sljedeem primjeru, bezParametara je vektor inicijaliziran konstruktorom bez
parametara, dok je oopsla, suprotno oekivanju, deklaracija funkcije bez parametara
koja vraa Vektor:
Vektor bezParametara;
Vektor oopsla();

// deklaracija vektora
// deklaracija funkcije bez parametara
// koja vraa Vektor

Primjena konstruktora posebno dolazi do izraaja u sluaju da klasa sadri pokazivae te


koristi dinamiku alokaciju memorije. Uzmimo na primjer da elimo napraviti klasu
Tablica koja sadri niz cijelih brojeva. Tablicu je potrebno moi pretraivati te

235

provjeravati na kojoj se poziciji odreeni broj nalazi, a potrebni su i funkcijski lanovi


za dodavanje i brisanje elemenata. Evo mogueg rjeenja:
#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);
};

Osnovna zadaa konstruktora jest inicijalizirati objekt tako da se dodijeli odreeni


memorijski prostor za tablicu:
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);
}

Preostali funkcijski lanovi za pristup i odravanje tablice izgledaju ovako:


void Tablica::PovecajNa(int NovaDulj) {
int *Nova=new int[NovaDulj];
assert(Nova != NULL);
for (int i = 0; i < BrojElem; i++)
Nova[i] = Elementi[i];
Duljina = NovaDulj;
delete [] Elementi;
Elementi = Nova;
}
void Tablica::DodajElem(int Elt) {
if (Duljina == BrojElem)
PovecajNa(Duljina + 10);
Elementi[BrojElem++] = Elt;
}

236

void Tablica::BrisiElem(int poz) {


assert(poz < BrojElem);
for (int i = poz; i < BrojElem - 1; i++)
Elementi[i] = Elementi[i+1];
BrojElem--;
}

Osim dodjele memorije, zadatak konstruktora je postavljanje podataka o broju


elemenata u tablici te podeavanje njene duljine. Prvi konstruktor nema parametara i on
inicijalizira tablicu na duljinu od deset elemenata. Drugi konstruktor ima kao parametar
poetnu duljinu tablice te on inicijalizira tablicu na eljenu duljinu.
Vano je uoiti injenicu da konstruktor ne alocira memoriju za smjetanje samog
objekta (tj. za smjetanje podatkovnih lanova Elementi, BrojElem, Duljina). Ta
memorija se dodjeljuje izvan konstruktora (u gornjem sluaju prevoditelj e sam
generirati kd koji e to obaviti). Konstruktor na ve alociranom objektu samo obavlja

Konstruktor ne alocira memoriju za pohranjivanje lanova objekta, no moe


dodatno alocirati memoriju koju objekt koristi.
posao inicijalizacije. Postupak inicijalizacije klase Tablica zahtijeva dodjelu posebnog
podruja memorije za pohranjivanje tablice.
Paljivi itatelj je zasigurno primijetio makro funkciju assert() koja do sada nije
objanjena. Jo paljiviji itatelj je vjerojatno i naslutio da ona slui za provjeru
ispravnosti dobivenog pokazivaa. Makro funkcija assert() je definiran u standardnoj
datoteci zaglavlja assert.h, a funkcionira tako da provjerava uvjet koji je zadan kao
parametar. Ako je uvjet zadovoljen, program se normalno nastavlja, a ako nije, program
se prekida i ispisuje se poruka o pogreki, uvjet koji ju je izazvao, te podaci o mjestu na
kojem se pogreka dogodila. Takav nain pisanja programa je vrlo praktian prilikom
razvijanja programa jer se tada mogu provjeriti svi vani uvjeti koji e garantirati
ispravno funkcioniranje programa. Takvi testovi, meutim, nepotrebno usporavaju
program, pa kada je program jednom istestiran, poeljno ih je iskljuiti iz izvedbenog
kda. Tada naredbom
#define NDEBUG

moemo definirati simbol NDEBUG (to je skraenica od No Debug). Program je


potrebno ponovo prevesti, a svi pozivi assert() makro funkcije bit e zamijenjeni
praznom naredbom. Tako se jednim potezom mogu iz programa ukloniti sve nepotrebne
provjere. O tome e jo biti dodatno rijei u poglavlju 14 koje se bavi pretprocesorom.
Kao to iz kda klase Tablica vidimo, definicija konstruktora se razlikuje od
definicije obine funkcije. Naime, prije nego to se ue u tijelo konstruktora, postoji
mogunost inicijalizacije pojedinih podatkovnih lanova, tako da se iza imena
konstruktora stavi dvotoka te se navede naziv podatkovnog lana i u zagradi njegova

237

poetna vrijednost. Ako je potrebno inicijalizirati vie lanova, oni se razdvajaju


zarezima. Ta inicijalizacijska lista zavrava vitiastom zagradom koja oznaava poetak
tijela konstruktora. U gornjem primjeru smo na taj nain inicijalizirali podatkovne
lanove Elementi i BrojElem:
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

Inicijalizacija lanova prije ulaska u tijelo konstruktora je dobra


programerska praksa.
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) { /* ... */ }

unato svim oekivanjima radi pogreno. Kako je podatkovni lan BrojElem u


deklaraciji klase stavljen iza deklaracije pokazivaa Elementi, on e biti inicijaliziran
na vrijednost 10 tek nakon izvoenja operatora new i dodjele memorije. Prilikom
inicijalizacije lana Elementi lan BrojElem e sadravati nedefiniranu vrijednost i
zbog toga e dodjela memorije biti pogreno obavljena.
Objekti se ne inicijaliziraju prema redoslijedu navoenja u inicijalizacijskoj
listi konstruktora, ve prema slijedu deklaracije u klasi.
Ranije navedeni redoslijed pozivanja konstruktora vrijedi i u sluaju da klasa sadri
objekte druge klase. Njihovi konstruktori se pozivaju prije nego to se ue u tijelo
konstruktora klase koja sadri objekte. Uzmimo za primjer klasu Par koja sadri dva
vektora:

238

class Par {
Vektor prvi, drugi;
public:
Par(float x1, float y1, float x2, float y2) :
drugi(x2, y2), prvi(x1, y1) {}
// ...
};

Konstruktor klase Par::Par() poziva konstruktore prvi i drugi objekata lanova


klase. Budui da se lanovi inicijaliziraju prema redoslijedu deklaracije, prvo e se
inicijalizirati lan prvi, a tek onda drugi. U sluaju da je konstruktor klase Par bio
napisan kao
Par(float x1, float y1) : prvi(x1, y1) {}

odnosno da je izostavljen konstruktor za lan drugi, prevoditelj bi automatski pozvao


konstruktor bez parametara za drugi on bi bio inicijaliziran kao nul-vektor. Ako
konstruktor bez parametara ne bi postojao, prijavila bi se pogreka prilikom prevoenja
Ne mogu pronai Vektor::Vektor().
Svi objekti-lanovi neke klase e biti inicijalizirani konstruktorom
navedenim u inicijalizacijskoj listi konstruktora dotine klase. Ako je neki
objekt izostavljen iz inicijalizacijske liste, pozvat e se podrazumijevani
konstruktor.
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:
Vektor *usmjernik = new Vektor;

Ova naredba deklarira pokaziva usmjernik na objekt klase Vektor te poziva


operator new koji stvara novi dinamiki vektor u memoriji. Vrijednost pokazivaa
usmjernik se postavlja na adresu novostvorenog objekta. Kako nisu specificirani
nikakvi parametri, poziva se konstruktor bez parametara. Ako elimo novopeeni objekt
inicijalizirati konstruktorom s parametrima, argumente konstruktora emo navesti u
zagradama iza naziva klase:
Vektor *usmjernik = new Vektor(12.0, 3.0);

U ovom sluaju se poziva konstruktor Vektor::Vektor(float, float) te se ax i


ay inicijaliziraju na 12.0 i 3.0. Kao i kod stvaranja automatskih objekata klase, kod
koritenja operatora new potrebno je navesti parametre pomou kojih se moe

239

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:
Pravac::Pravac(Tocka &t1, Tocka &t2);

8.4.2. Podrazumijevani konstruktor

Konstruktor bez parametara se naziva podrazumijevani konstruktor (engl. default


constructor). Ako u klasi nije eksplicitno definiran niti jedan konstruktor, prevoditelj e
automatski generirati podrazumijevani konstruktor u kojem e pokuati inicijalizirati
sve podatkovne lanove njihovim podrazumijevanim konstruktorom. Klasa
class Podrazumijevani {
int broj;
Vektor v;
};

nema konstruktora. Zato prevoditelj generira podrazumijevani konstruktor koji e


ostaviti lan broj neinicijaliziran, dok e za lan v pokuati pronai podrazumijevani
konstruktor u klasi Vektor. Kako klasa Vektor ima definiran podrazumijevani
konstruktor, prevoditelj e moi generirati podrazumijevani konstruktor za klasu
Podrazumijevani. Ako za klasu Vektor podrazumijevani konstruktor ne bi bio
definiran, prevoditelj bi prijavio pogreku Ne mogu pronai Vektor::Vektor().
Automatska generacija podrazumijevanog konstruktora je onemoguena ako je za
klasu definiran barem jedan konstruktor. To znai da u klasi
class ImaKonstruktor {
int i;
public:
ImaKonstruktor(int ii) : i(ii) {}
};

zbog toga to je definiran jedan konstruktor, prevoditelj nee sam generirati


podrazumijevani konstruktor. Kako definirani konstruktor uzima jedan parametar, klasa
ImaKonstruktor nee imati podrazumijevani konstruktor, te e prilikom definiranja
objekata klase uvijek biti neophodno navesti parametar.
Dobro je uoiti da prevoditelj, za klase koje imaju kao lanove reference i
konstantne objekte, ne moe sam generirati podrazumijevani konstruktor. Razlog je u
tome to se reference i konstantni lanovi moraju inicijalizirati u inicijalizacijskoj listi
konstruktora te im se vrijednost kasnije ne moe mijenjati. Prevoditelj ne zna kako treba

240

inicijalizirati referencu, odnosno konstantan lan, pa zbog toga ne stvara


podrazumijevani konstruktor. Ako je podrazumijevani konstruktor potreban, korisnik ga
mora definirati sam. Naalost, prevoditelj nee javiti korisniku nikakvu pogreku ili
upozorenje kojim bi mu na to skrenuo panju.
Prevoditelj e generirati podrazumijevani konstruktor ako klasa nema
definiran niti jedan drugi konstruktor, te ako ne sadri konstantne lanove ili
reference.
Prilikom koritenja podrazumijevanih parametara treba biti oprezan. U primjeru
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.
8.4.3. Poziv konstruktora prilikom definiranja objekata

Prilikom definiranja objekata mogue je navesti parametre koji se prosljeuju


konstruktoru klase. Ti se parametri navode nakon naziva identifikatora objekta u
zagradama, slino parametrima funkcije, kao u sljedeem primjeru:
Vektor normala1(12.0, 3.0)
Vektor normala2;

Nakon dodjele memorijskog prostora za objekt normala1, pozvat e se konstruktor


Vektor::Vektor(float, float) koji e inicijalizirati podatkovne lanove ax i ay
na 12.0 i 3.0. Kako kod deklaracije objekta normala2 nisu definirani parametri, bit e
pozvan podrazumijevani konstruktor pa e objekt biti inicijaliziran kao nul-vektor.
Prevoditelj prilikom deklaracije objekta analizira parametre i pokuava pronai
odgovarajui konstruktor. Pri tome se primjenjuju pravila o preoptereenju funkcija i o
konverziji parametara prilikom poziva funkcije. Ako je konstruktor pronaen, objekt se
inicijalizira pomou njega. U sluaju da nije pronaen, prevoditelj e javiti pogreku
prilikom prevoenja. Deklaracija objekta normala2 ne bi mogla biti obavljena ako
klasa Vektor ne bi sadravala podrazumijevani konstruktor. Pri tome valja imati na
umu da klasu koja nema niti jedan konstruktor, prevoditelj sam nadopunjuje
podrazumijevanim konstruktorom.

241

Konstruktori za pojedine tipove objekata pozivaju se na raznim mjestima u


programu, ovisno o vrsti objekta:

za lokalne (automatske) objekte konstruktor se poziva prilikom deklaracije objekta,


za statike i globalne objekte prije ulaska u funkciju main(),
za dinamike objekte prilikom poziva operatora new.

8.4.4. Konstruktor kopije

Ponekad je potrebno stvoriti objekt koji je preslika ve postojeeg objekta. Takvu


inicijalizaciju obavlja posebno definiran konstruktor kopije (engl. copy constructor). On
se razlikuje od ostalih konstruktora po tome to ima samo jedan parametar i to referencu
na konstantan objekt iste klase. Evo primjera za konstruktor kopije klase Vektor:
Vektor::Vektor(const Vektor &refVektor) :
ax(refVektor.ax), ay(refVektor.ay) {}

Uloga konstruktora kopije je preslikavanje lanova vektora refVektor u objekt koji


upravo nastaje. Sada je mogue napraviti sljedee deklaracije:
Vektor vektor1(12.0, 3.0);
Vektor vektor2 = vektor1;
Vektor vektor3(vektor2);

Objekti vektor2 i vektor3 se inicijaliziraju preko podataka objekta vektor1 i na


kraju oba sadre 12 i 3 u svojim ax i ay lanovima.
Konstruktor kopije se takoer koristi za stvaranje privremenih objekata prilikom
prosljeivanja objekata u funkciju i njihovog vraanja, no to e biti zasebno objanjeno.
Klasa ne mora definirati konstruktor kopije. Ako on nije eksplicitno definiran,
prevoditelj e ga generirati sam, neovisno o tome postoji li jo koji drugi konstruktor.
Taj konstruktor kopije e pozvati konstruktor kopije za svaki pojedini podatkovni lan.
Za ugraene tipove konstruktor kopije jednostavno dodjeljuje njihovu vrijednost
odgovarajuem podatkovnom lanu objekta. Za podatkovne lanove koji su objekti
korisniki definiranih klasa, koristi se konstruktor kopije definiran za tu klasu. Iz ovoga
je jasno da bi podrazumijevani konstruktor kopije u sluaju klase Vektor bio dovoljan
jer bi uinio isto to radi i na eksplicitno definirani konstruktor.
No u sluaju klase Tablica stvari su drukije. Ta klasa sadri pokaziva na
memoriju kojemu se vrijednost odreuje prilikom stvaranja objekta. Ako bi konstruktor
kopije jednostavno inicijalizirao sve lanove kopije vrijednostima lanova originala, oba
bi objekta pokazivala na isti objekt. To se moe ilustrirati slikom 8.3.
Imamo objekt A i od njega pomou konstruktora kopije napravimo objekt B. Kako
nismo definirali konstruktor kopije za klasu Tablica, prevoditelj sam generira
podrazumijevani konstruktor kopije. On inicijalizira kopiju tako da prekopira vrijednost
svakog lana pa to ini i s pokazivaem Elementi. Na kraju imamo dvije tablice koje

242

Tablica A
int*

Tablica B
int*

int*

int*

Elementi

Elementi

ELEMENTI

Slika 8.3. Objekti A i B pokazuju na isto memorijsko podruje

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);
};
Tablica::Tablica(const Tablica &refTabl) :
Elementi(new int[refTabl.Duljina]),
Duljina(refTabl.Duljina),
BrojElem(refTabl.BrojElem) {
assert(Elementi != 0);
for (int i = 0; i < BrojElem; i++)

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];
}

Za konstruktore kopije vrijede ista pravila za inicijalizaciju lanova kao i za obine


konstruktore.
Klase koje sadre pokazivae na dinamiki alocirane segmente memorije
gotovo uvijek iziskuju korisniki definiran konstruktor kopije.

Zadatak. Deklarirajte klasu ZnakovniNiz koja e sadravati privatni pokaziva na


znakovni niz. Dodajte konstruktor koji e kao parametar imati pokaziva na znakovni
niz te inicijalizirati objekt pomou proslijeenog niza. Takoer, dodajte konstruktor
kopije koji e omoguiti kopiranje objekata. Obratite panju na zakljuni nul-znak.

8.4.5. Inicijalizacija referenci i konstantnih lanova

Klase mogu sadravati konstantne lanove i reference na objekte. Takvi lanovi imaju
poseban tretman prilikom inicijalizacije i pristupa njihovoj vrijednosti. Promotrimo
poblie o emu se radi.
Iz prethodnih poglavlja znamo da se referenca mora inicijalizirati prilikom
deklaracije. Ne postoji mogunost ostaviti referencu neinicijaliziranu ili ju naknadno
preusmjeriti. U klasama se referenca moe deklarirati, ali za razliite objekte iste klase
ona moe referirati razliite podatke. Na primjer, moemo definirati klasu Par koja e
opisivati ureeni par vektora, s time da vektori nee biti dio klase, ve e svaki Par
sadravati po dvije reference na vektore. Evo deklaracije klase:
class Par {
public:
Vektor &refPrvi, &refDrugi;
// nastavak slijedi ...
};

Ako se koristi operator pridruivanja za pristup referenci, mijenja se vrijednost


referiranog objekta, a ne reference:
Par p;
// ovakva inicijalizacija nije ispravna
Vektor v(12.0, 3.0);
p.refPrvi = v; // mijenja se vrijednost referiranog objekta

Takoer, ne moemo referencu inicijalizirati prilikom deklaracije, jer za svaki Par


referenca ima drugu vrijednost. Jedino rjeenje je inicijalizirati referencu u konstruktoru,
prilikom stvaranja objekta. Kako bi se naznailo da se ne pristupa referiranom objektu,
nego vrijednosti reference, ona se mora inicijalizirati u inicijalizacijskoj listi
konstruktora. Ako klasa sadri referencu te ako ju ne inicijaliziramo na taj nain, dobit

244

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:
Vektor a(10.0, 2.0), b(-2.0, -5.0);
Par p(a, b);
// ispravna inicijalizacija

Reference se obavezno moraju inicijalizirati, te se to mora uiniti u


inicijalizacijskoj listi konstruktora.
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
};

Sada se lan maxDuljina obavezno mora inicijalizirati, i to se mora uiniti u


inicijalizacijskoj listi konstruktora:

245

Tablica::Tablica() : maxDuljina(50),
Elementi(new int[10]),
BrojElem(0), Duljina(10) {
}
Tablica::Tablica(int BrElem, int duljina) :
maxDuljina(duljina), Elementi(new int[10]),
BrojElem(0), Duljina(BrElem) {
}

Prvi konstruktor inicijalizira lan maxDuljina na vrijednost 50, dok ga drugi postavlja
na proslijeenu vrijednost.
Konstantni podatkovni lanovi objekta se obavezno moraju inicijalizirati, te
se to mora uiniti u inicijalizacijskoj listi konstruktora.

8.4.6. Konstruktori i prava pristupa

Konstruktori takoer podlijeu pravilima prava pristupa. Ovisno o mjestu u deklaraciji


klase oni imaju javni, privatni ili zatieni pristup. Prilikom deklaracije objekta
prevoditelj odreuje koji se konstruktor poziva. Ako je izabrani konstruktor nedostupan
zbog, primjerice privatnog prava pristupa, objekt ne moe biti deklariran i prevoditelj
javlja pogreku prilikom prevoenja. Prepravimo klasu Vektor tako da
podrazumijevani konstruktor bude privatni:
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

Deklaracija vektora vektor1 u funkciji main() javlja pogreku prilikom prevoenja


jer podrazumijevani konstruktor ima privatna prava pristupa. Naprotiv, vektor vektor2
se moe normalno deklarirati jer pozvani konstruktor ima javni pristup. Vektor priv u
funkcijskom lanu NekaFunkcija() se moe deklarirati unato tome to je
podrazumijevani konstruktor privatan njemu se moe pristupiti unutar klase.
Ako je potrebno onemoguiti stvaranje objekata neke klase (osim u posveenim
dijelovima programa, kao to su prijateljske funkcije i slino), to se moe postii tako da
se svi konstruktori uine privatnima ili zatienima. Slino vrijedi i za konstruktor
kopije: ako elite onemoguiti kopiranje objekta, konstruktor kopije uinite privatnim.
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.
Klasa Vektor ne zauzima nikakve dodatne resurse te zato nema potrebe za
eksplicitnom definicijom destruktora. No klasa Tablica ima pokaziva na dinamiku
memoriju Element, stoga je prilikom unitavanja objekta potrebno osloboditi memoriju
dodijeljenu za pohranjivanje lanova tablice. Zato klasu moramo proiriti na sljedei
nain:
class Tablica {
// ...
public:
~Tablica();
// ...
};

247

Tablica::~Tablica() {
delete [] Elementi;
}

U destruktoru se pomou operatora delete oslobaa memorija koja je bila zauzeta u


konstruktoru. Koritenje klase Tablica bez definiranog destruktora prouzroilo bi
neispravan rad programa jer bi se memorija stalno troila stvaranjem objekata klase
Tablica, a nikada se ne bi oslobodila. Prije ili kasnije, takav program bi potroio svu
memoriju raunala i prestao s radom.
Dinamiki objekti se ne unitavaju automatski nakon izlaska iz bloka u kojem su
stvoreni ve o njihovom unitavanju mora voditi rauna programer. Za unitavanje se
koristiti operator delete. On prije dealokacije memorije poziva destruktor. Operator se
koristi tako da se iza kljune rijei delete navede pokaziva na objekt koji se eli
obrisati, na primjer:
// alokacija dinamikog objekta
Vektor *pokNaStrelicnik = new Vektor(12.0, 3.0);
// dealokacija objekta
delete pokNaStrelicnik;

Obratite panju na to da dinamike objekte klase treba eksplicitno brisati operatorom


delete iako klasa Vektor ne mora imati (niti nema) destruktor. U suprotnom bi
memorija alocirana prilikom stvaranja objekta ostala vjeno zauzeta. Mnogi operativni
sistemi imaju mehanizme pomou kojih nakon zavretka programa oslobaaju svu
memoriju koju je on zauzeo, no na njih se ne valja oslanjati.
Pravilo dobrog programiranja je osloboditi svu zauzetu memoriju prije
zavretka programa.
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

U ovom primjeru ispis poruke moe se iskljuiti tako da se definira pretprocesorski


simbol NDEBUG. Naziv tog simbola nije sluajno izabran. Naime, definiranje tog
simbola e iskljuiti sve testove makro funkcijom assert(). Ako i za nae provjere
iskoristimo isti simbol, jednim potezom emo ubiti sve provjere ispravnosti. U
gornjem primjeru se koristi uvjetno prevoenje. Prevoditelj e prevesti naredbe
navedene izmeu pretprocesorskih direktiva #ifndef i #endif ako je simbol NDEBUG
nije definiran. U suprotnom naredbe nee biti umetnute u program. Detaljnije o
pretprocesorskim naredbama bit e govora u zasebnom poglavlju.
Postoji situacija u kojoj je eksplicitni poziv destruktora potreban, a to je kada se
objekt alocira na tono odreenom mjestu u memoriji pomou operatora new. U tom
sluaju se zauzeta memorija ne oslobaa, nego se samo unitava objekt u njoj. Primjer
za to e biti dan u sljedeem odjeljku.
Zadatak. Klasi ZnakovniNiz iz zadatka na stranici 243 dodajte destruktor tako da se
resursi zauzeti u konstruktoru ispravno oslobode.
Zadatak. Osim dealokacije memorije, destruktor moe obavljati i druge operacije, na
primjer za brojenje objekata. Deklarirajte klasu Muha koja e imati konstruktor i
destruktor. Takoer, deklarirajte globalnu cjelobrojnu varijablu koja e u svakom
trenutku sadravati broj muha u kompjutoru. (Vjerujte nam na rije, taj broj uvijek tei
u beskonanost!) Prilikom stvaranja objekta, sadraj varijable se uveava, a prilikom
unitavanja smanjuje.
8.4.8. Globalni i statiki objekti

Kao to je mogue stvoriti globalne i statike cjelobrojne ili realne varijable, mogue je
stvoriti globalne i statike objekte korisniki definiranih klasa. Na primjer:
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; }
};
Kompleksni a(10, 20);
Kompleksni sumiraj(Kompleksni
static Kompleksni suma(0,
suma.Postavi(suma.DajRe()
suma.DajIm()
return suma;
}
int main() {

&ref) {
0);
+ ref.DajRe(),
+ ref.DajIm());

249

Kompleksni zbroj = sumiraj(a);


// ...
}

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
Konstruktori za globalne i statike objekte e se pozvati prije ulaska u
funkciju main(), a destruktori nakon zavretka funkcije main() ili nakon
poziva funkcije exit().
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.

8.5. Polja objekata


U programima se vrlo esto koriste polja istovrsnih tipova podataka. Jezik C++
posjeduje mehanizme za efikasno definiranje i pristup elementima polja. Sintaksa
definiranja polja objekata je usklaena s nainom definiranja polja ugraenih tipova.
Polje od tri vektora deklarira se, analogno polju cijelih brojeva, na sljedei nain:
Vektor polje[3];

U ovom primjeru svaki element polja e biti inicijaliziran podrazumijevanim


konstruktorom. Ako takav konstruktor ne postoji, dobit emo pogreku prilikom
prevoenja Ne mogu pronai funkciju Vektor::Vektor().

250

Ako se polje deklarira bez inicijalizacijske liste, objekti polja se


inicijaliziraju podrazumijevanim konstruktorom.
No postoji i mogunost specifikacije konstruktora za svaki element polja posebno, kao
u primjeru
Vektor polje[3] = { Vektor(12., 3.),
Vektor(),
Vektor(-3., 5.) };

U ovom sluaju polje ima inicijalizacijsku listu u kojoj je specificiran konstruktor s


pripadnim parametrima za svaki lan polja. Prvi i trei vektor inicijaliziraju se
brojevima 12 i 3 odnosno 3 i 5, dok se drugi vektor postavlja na nul-vektor. Ukoliko ne
postoji podrazumijevani konstruktor, eksplicitno navoenje inicijalizacijske liste s
konstruktorom za svaki lan je jedini nain alociranja polja objekata.
Mogue je takoer i dinamiki alocirati polje objekata. Polje od tri vektora se moe
alocirati na sljedei nain:
Vektor *polje = new Vektor[3];

Kako prilikom dinamike alokacije polja nema mogunosti navoenja inicijalizacijske


liste, podrazumijevani konstruktor mora postojati kako bi prevoditelj ispravno
inicijalizirao lanove polja.
Elementi dinamiki alociranih polja se uvijek inicijaliziraju
podrazumijevanim konstruktorom, koji mora postojati da bi dinamika
alokacija mogla biti obavljena.
Unitavanje dinamiki alociranih polja objekata se obavlja operatorom delete s time
to se nakon samog operatora stavljaju znakovi [] (otvorena i zatvorena uglata zagrada,
kao i kod dinamiki alociranih polja ugraenih tipova vidi odjeljak 4.2.6), nakon ega
se navodi pokaziva na prvi element polja, kao u sljedeem primjeru:
delete [] polje;

Upotreba znakova [] je obavezna, jer oni naznaavaju prevoditelju da pokaziva


pokazuje na polje objekata alociran operatorom new, a ne samo na jedan objekt.
Prilikom izvoenja ove naredbe bit e automatski dodan kd koji e ustanoviti koliko
je lanova bilo alocirano. Zatim e se za svaki element posebno pozvati destruktor, a na
kraju e cjelokupna memorija biti osloboena. Izostavljanjem uglatih zagrada kao u
naredbi

251

delete polje;

// pogreno: pozvat e se destruktor samo


// za prvi lan

bio bi pozvan samo destruktor za prvi element polja, a ne i za ostale, budui da


prevoditelju nigdje nije naznaeno da je polje polje. Dodue, ispravna koliina
memorije bi bila osloboena, ali svi objekti, osim prvoga, ne bi bili ispravno uniteni
destruktorom. Ukoliko bi primjerice sadravali pokazivae na dodatno alocirane
segmente memorije, oni bi zauvijek ostali u memoriji i nikada ne bi bili osloboeni.
Time dobivamo memorijsku napuklinu (engl. memory leak) gdje se koliina slobodne
memorije postupno smanjuje. Program naoko dobro funkcionira dok mu u jednom
trenutku ne ponestane memorije i tada se zablokira. Takvim pogrekama je vrlo teko
ui u trag.
Prilikom dealokacije polja potrebno je koristiti operator delete [] koji
poziva destruktor za sve lanove polja. U protivnom dobit emo
memorijsku napuklinu, te e se koliina raspoloive memorije stalno
smanjivati.
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
Vektor *pokPolje = new (pokMjesto) Vektor(5, 6);

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>
const int velicina = 5;
Vektor *AlocirajPolje() {
int polje[velicina][2] = {{12, 3}, {0, 0}, {1, 1},
{6, -7}, {-2, -9}};
// alocirat emo memoriju za cijelo polje
char *pok = new char[sizeof(Vektor) * velicina];
for (int i = 0; i < velicina; i++)

252

// stvara se objekt klase Vektor


// na mjestu odreenom pokazivaem
// pok + sizeof(Vektor) * i
new (pok + sizeof(Vektor) * i)
Vektor(polje[i][0], polje[i][1]);
return (Vektor *)pok;
}

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;

Kako memorija nije alocirana naredbom


pok = new Vektor[5];

prevoditelj nee moi pozvati destruktore za lanove polja. Dealokacija polja se mora
obaviti eksplicitnim pozivanjem destruktora za svaki lan polja:
void DealocirajPolje(Vektor *polje, int vel) {
for (int i = 0; i < vel; i++)
polje[i].~Vektor();
delete [] (char *)polje;
}

Kao to se iz primjera vidi, za svaki element se eksplicitno poziva destruktor, a


memorija se na kraju dealocira tako da se pokaziva na prvi element pretvori u
pokaziva na char te se oslobodi.
Zadatak. Potrebno je realizirati objekte koji se koriste u programu za raunarsko
igranje aha. Deklarirajte klasu SahovskoPolje koja e opisivati jedno polje te e
pamtiti koja se figura nalazi na polju. Figure su definirane pomou pobrojenja:
enum Figure {bijeliPjesak, bijeliTop, bijeliKonj,
bijeliLovac, bijelaKraljica, bijeliKralj,
crniPjesak, crniTop, crniKonj, crniLovac,
crnaKraljica, crniKralj};

Pojedina fugura e se na polje postavljati pomou funkcijskog lana Postavi(Figure


fig). Oznaka figure na polju e se moi proitati pomou lana KojaFigura(). Zatim
deklarirajte polje Sahovnica od 8 stupaca i 8 redaka objekata SahovskoPolje (pri
tome podesite konstruktor klase SahovskoPolje tako da je mogue polje deklarirati).
Takoer postavite polje tako da odraava poetnu ahovsku poziciju.

253

8.6. Konstantni funkcijski lanovi


Objekti klasa mogu biti deklarirani kao konstante pomou kljune rijei const. Takvi
objekti se postavljaju na svoju inicijalnu vrijednost u konstruktoru te im se kasnije
vrijednost ne moe promijeniti. No vrijednost objekta se ionako najee ne mijenja
direktnim pristupom podatkovnim lanovima, nego se koriste funkcijski lanovi koji
mogu promijeniti vrijednost nekog podatkovnog lana. U tom sluaju postoji opasnost
da, iako je objekt deklariran kao konstantan, poziv funkcijskog lana ipak promijeni
objekt. Kako bi se onemoguilo mijenjanje konstantnih objekata preko funkcijskih
lanova, uveden je koncept konstantnih funkcijskih lanova kojima se moe regulirati
pristup konstantnim objektima.
Funkcijski lan se deklarira konstantnim tako da se iza zatvorene zagrade koja
zavrava definiciju liste parametara navede kljuna rije const kao u primjeru
class NekaKlasa {
void FunkcijskiClan(int, float) const;
};

Time se prevoditelju daje na znanje da funkcijski lan ne mijenja vrijednost objekta.


Takav funkcijski lan se moe slobodno pozvati na konstantnom objektu. Konstantni
funkcijski lan je ogranien na operacije koje ne mijenjaju vrijednost niti jednog
podatkovnog lana objekta u suprotnom e se dobiti pogreka prilikom prevoenja.
Kada se objekt klase definira konstantnim, na njemu su dozvoljeni pozivi iskljuivo

Konstantni funkcijski lanovi su ogranieni iskljuivo na itanje podatkovnih


lanova, te se zbog toga mogu pozivati na konstantnim objektima.
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

Funkcije DajX() i DajY() samo itaju vrijednosti vektora pa su definirane


konstantnima. Funkcija PostaviXY() mijenja vrijednost objekta pa bi pokuaj njenog
definiranja konstantnom funkcijom rezultirao pogrekom prilikom prevoenja.
Konstruktori i destruktori ne mogu biti konstantni. Sada je mogue definirati konstantne
vektore:
int main() {
const Vektor v(12, 3);
cout << "X: " << v.DajX() << endl;
cout << "Y: " << v.DajY() << endl;
v.PostaviXY(4, 5);
return 0;
}

// OK
// OK
// pogreka

Konstantnost (engl. constness) ili nekonstantnost funkcijskog lana je dio


potpisa funkcije. To znai da klasa moe sadravati konstantnu i
nekonstantnu verziju funkcijskog lana s istim parametrima.
U sluaju konstantnog objekta poziva se konstantni, a u sluaju obinog objekta obini
funkcijski lan. Klasu vektora moemo modificirati tako da u sluaju poziva lana
PostaviXY() na konstantnom objektu ispiemo poruku o pokuaju mijenjanja
konstantnog objekta:
class Vektor {
// ...
void PostaviXY(float x, float y);
void PostaviXY(float, float) const;
};
void Vektor::PostaviXY(float x, float y) {
ax = x;
ay = y;
}
void Vektor::PostaviXY(float, float) const {
cout << "Promjena konstantnog objekta." << endl;
}

No mogue je zamisliti situaciju u kojoj i konceptualno konstantna funkcija mora


promijeniti vrijednost nekog podatkovnog lana objekta. Na primjer, neka radi mjerenja
brzine programa elimo brojati koliko puta smo pozvali funkcijski lan DajX().
Funkcijski lan konceptualno ostaje konstantan jer ne mijenja vrijednost objekta, no
mora poveati vrijednost brojaa pomou kojeg se prati broj pristupa objektu. Broja
e biti podatkovni lan klase, pa ga zbog toga nee biti mogue mijenjati u
konstantnim funkcijskim lanovima. Problem se moe rijeiti tako da se pomou
eksplicitne dodjele tipa odbaci konstantnost pojedinog podatkovnog lana, s tom

255

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;
};
float Vektor::DajX() const {
((int&)broji)++;// dodjelom se ukida konstantnost
return ax;
}
float Vektor::DajY() const {
((int&)broji)++;
// dodjelom se ukida konstantnost
return ay;
}

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;
};
float Vektor::DajX() const {
broji++;// dozvoljeno jer je broji deklariran kao
// mutable
return ax;

256

}
float Vektor::DajY() const {
broji++;
// dozvoljeno jer je broji deklariran kao
// mutable
return ay;
}

Ako se objekt deklarira konstantnim, tada se iz vanjskog programa ne moe promijeniti


sadraj javnih podatkovnih lanova. No lanovi deklarirani pomou mutable se mogu
mijenjati:
class NekaKlasa {
public:
int neDiraj;
mutable int radiStoHoces;
};
// ...
const NekaKlasa obj;
obj.neDiraj = 100;
obj.radiStoHoces = 101;

// objekt obj je konstantan


// pogreka: obj je konstantan
// OK: lan je mutable

Razmotrimo ukratko koliko je korisno koritenje konstantnih objekata. Mnogi


programeri smatraju konstantne objekte samo dodatnom gnjavaom te ih ne koriste. No
intenzivnim koritenjem konstantnih objekata dobiva se dodatna sigurnosna provjera
prilikom prevoenja.
Ako klase koje razvijate koristite iskljuivo za vlastite potrebe, tada moete ali ne
morate koristiti konstantne funkcijske lanove kao i konstantne parametre. No ako elite
klase koje piete staviti na raspolaganje i drugim programerima, tada je dobra praksa da
se lanovi koji ne mijenjaju objekt uine konstantnima. U suprotnom, programer koji
inae koristi konstantne objekte je prisiljen ne koristiti ih, jer na konstantnim objektima
ne moe koristiti niti jedan funkcijski lan. Koritenje konstantnih lanova je dobra
programerska navika.
Koritenje konstantnih funkcijskih lanova je poeljno ako razvijate klase
koje e koristiti drugi programeri.

Zadatak. Promijenite klasu SahovskoPolje tako da lanove koji to dozvoljavaju


deklarirate konstantnima. Takoer, deklarirajte polje Zavrsnica konstantnih objekata
klase SahovskoPolje koje e odraavati raspored figura u nekoj zavrnici po vaem
izboru. Uvjerite se da je za takve objekte mogue pozvati konstantne funkcijske lanove
tako da ispiete sadraj ahovske ploe na ekran.

257

8.7. Funkcijski lanovi deklarirani kao volatile


Objekti klase mogu biti deklarirani pomou kljune rijei volatile. Time se
prevoditelju daje na znanje da se vrijednost objekta moe promijeniti njemu nepoznatim
metodama te zbog toga treba izbjegavati optimizaciju pristupa objektu. Takvim
objektima se moe pristupati iskljuivo pomou funkcijskih lanova deklariranih
pomou kljune rijei volatile (vidi odsjeak 2.4.6).
Na objektima koji su definirani kljunom rijei volatile mogu se pozvati samo
konstruktori, destruktor te funkcijski lanovi koji su deklarirani kao volatile. Kljuna
rije volatile se u deklaraciji funkcijskog lana stavlja iza zagrade koja zavrava listu
parametara poput kljune rijei const. Funkcijski lan moe istodobno biti i const i
volatile. I volatile kvalifikator je dio potpisa lana te se lan moe preopteretiti s
volatile i obinom varijantom. To je ilustrirano sljedeim primjerom:
class PrimjerZaVolatile {
public:
void Funkcija1();
void Funkcija2() volatile;
};

// obina funkcija
// 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;
}

8.8. Statiki lanovi klase


Klase koje smo u dosadanjim primjerima deklarirali imale su zasebne skupove lanova
za svaki objekt koji se stvorio. U nekim je sluajevima, meutim, potrebno dijeliti
podatke izmeu objekata iste klase. To se moe ostvariti statikim podatkovnim i
funkcijskim lanovima.
8.8.1. Statiki podatkovni lanovi

Ponekad je potrebno definirati podatkovni lan koji e biti zajedniki svim lanovima
klase. To moe, na primjer, biti broja koji broji koliko je stvoreno objekata te klase ili
moe biti statusni lan koji odreuje ponaanje klase. Taj problem bi se klasinim
pristupom mogao rijeiti tako da se deklarira globalna varijabla kojoj pristupaju
funkcijski lanovi klase. No to rjeenje nije elegantno jer je globalna varijabla dostupna
i izvan klase te joj pristup ne moe biti privatan.

258

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;
int MojBroj;
public:
Brojeni();
~Brojeni();
};

// statiki podatkovni lan

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.
Statiki lanovi su zajedniki za sve objekte neke klase. Oni su slini
globalnim varijablama s tom razlikom to su njihova imena vidljiva
iskljuivo iz podruja klase, te se podvrgavaju pravilima pristupa.
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;
bb.Brojac = 5;
bb.IspisiBrojace();
return 0;
}

// pristup izvana operatorom ::


// isto kao i prethodna naredba

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;
};

Statiki lanovi mogu biti navedeni kao podrazumijevani parametri funkcijskim


lanovima, dok nestatiki lanovi ne mogu, kao u primjeru
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);
};

8.8.2. Statiki funkcijski lanovi

Postoje funkcijski lanovi koji pristupaju samo statikim lanovima klase. Istina, oni se
mogu realizirati kao obini funkcijski lanovi. Meutim, ovakvo rjeenje ima manu to
se za poziv lana mora stvoriti objekt samo da bi se poziv obavio. To je nepraktino jer
objekt u biti nije potreban; funkcija koju pozivamo pristupa iskljuivo statikim
lanovima i ne mijenja niti jedan nestatiki lan koji ovisi o objektu. Takva se funkcija
moe deklarirati kao statika funkcija klase, tako da se ispred povratnog tipa umetne
kljuna rije static. Za poziv takve funkcije nije potrebno imati objekt, nego se
jednostavno navede naziv funkcije, kao da se radi o obinoj funkciji.

261

Da bismo ilustrirali koncept statikih funkcija, dodajmo klasi Brojeni statiku


funkciju VrijednostBrojaca(), koja vraa vrijednost statikog lana Brojac:
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.

8.9. Podruje klase


Svaka klasa ima svoje pridrueno podruje (engl. scope). Imena podatkovnih i
funkcijskih lanova pripadaju podruju klase u sklopu koje su definirani. Dva lana ne
mogu imati isti naziv u jednom podruju, no lanovi istih imena mogu nesmetano
koegzistirati ako su definirani u razliitim podrujima. Globalno definirane varijable,
objekti, funkcije, tipovi, klase i pobrojenja pripadaju globalnom ili datotenom podruju
(engl. global or file scope). Svi funkcijski lanovi imaju pristup imenima podruja klase
kojoj pripadaju. To znai da, ako se navede ime nekog objekta, ono e se prvo potraiti
u podruju klase. Ako je potrebno pristupiti objektu iz globalnog podruja koristi se
operator za razluivanje podruja :: (dvije dvotoke). Promotrimo sljedeu situaciju:
int duljina = 10;
class Polje {
public:
Polje() : { duljina = ::duljina;
pokPolje = new int[duljina]; }
private:
int duljina, *pokPolje;
};

U konstruktoru se navodi podatkovni lan s imenom duljina. Iako je on deklariran iza


definicije umetnutog konstruktora, prevoditelj e prepoznati da ime bez operatora ::

263

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
int duljina = 12;
void funkcija() {
int dulj = duljina;
int duljina = 24;
dulj = duljina;
int d = ::duljina;

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

referencira se ::duljina
globalna duljina postaje
skrivena
pridruuje se 24
pristupa se globalnoj varijabli
i pridruuje se 12

Dobra je programerska praksa izbjegavati zbunjujue i nejasne deklaracije


varijabli. Ako postoji mogunost nesporazuma prilikom razluivanja
podruja, dobro je navesti podruje pomou operatora za razluivanje
podruja.
Tako u primjeru
int duljina = 12;
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;
}

Iako se primjer mogao realizirati znatno elegantnije, dobro demonstrira upotrebu


operatora za razluivanje podruja. Operator :: se koristi tako da se s lijeve strane
navede naziv klase ijem podruju se eli pristupiti dok se s desne strane navede
identifikator iz tog podruja. Ako se ne navede ime klase, pristupa se globalnom
podruju.
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.

8.9.1. Razluivanje podruja

Kada se identifikator navede u programu bez izriito navedenog podruja kojem on


pripada, koriste se pravila za razluivanje podruja (engl. scope resolution) kako bi se
ono jednoznano odredilo. Taj postupak se provodi prema sljedeem skupu pravila:
1. Pretrauju se deklaracije u tekuem bloku. Ako se identifikator pronae, podruje je
odreeno. U suprotnom se pretrauje podruje unutar kojeg je blok smjeten.
2. Ako je blok smjeten unutar funkcijskog lana, pretrauje se podruje funkcijskog
lana. Pronae li se lokalna deklaracija koja odgovara identifikatoru, podruje je
odreeno. U protivnom se pretrauje podruje klase.
3. Pretrauje se podruje klase. Ako se pronae podatkovni lan istog naziva kao i
identifikator ije se podruje trai, identifikator se pridruuje tom podatkovnom lanu
te je podruje odreeno. U suprotnom se pretrauje nadreeno podruje.
4. Ako je klasa nenaslijeena i definirana u globalnom podruju, nadreeno podruje je
globalno podruje. Ako se pronae globalna deklaracija, podruje je odreeno. U
suprotnom se prijavljuje pogreka da identifikator nije pronaen. Ako je klasa

265

nenaslijeena i definirana kao ugnijeena klasa, nadreeno podruje je podruje


klase u koju je klasa ugnijeena. Poglavlje o ugnijeenim klasama objanjava ovaj
sluaj detaljnije. Za nenaslijeene klase definirane u lokalnom podruju nadreeno
podruje je podruje bloka u kojem je klasa definirana. Za naslijeene klase
nadreeno podruje ine sva podruja klasa od kojih klasa nasljeuje.
Ako je identifikator naveden pomou operatora za razluivanje podruja, pretrauje se
samo ono podruje koje je navedeno (podruje klase ili globalno podruje). Sve
navedeno vrijedi, kako za identifikatore podatkovnih lanova i varijabli, tako i za
identifikatore funkcijskih lanova i funkcija.
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:
int duljina, func1(), func2(int);
class IgraSkrivaca {
public:
void func1(int);
// skriva ::func1()
int func2;
// skriva ::func2(int)
void duljina(int, float); // skriva ::duljina
};

Reference na globalne identifikatore duljina, func1() i func2(int) moraju unutar


klase biti navedene kao ::duljina, ::func1() i ::func2(int). Na primjer
void IgraSkrivaca::duljina(int, float) {
func1(); // pogreka: poziva se IgraSkrivaca::func1(int)
// a ne ::func1()
}

8.10. Ugnijeene klase


Klase koje smo do sada deklarirali bile se smjetene u globalnom podruju, to znai da
se njihovim imenima moe pristupiti iz bilo kojeg dijela programa. Klase se, osim toga,
mogu deklarirati u podruju klase i lokalno. Tako deklarirane klase zovu se jo i
ugnijeene klase (engl. nested classes).
Primjenu ugnijeenih klasa ilustrirat emo primjerom. Zamislimo da elimo
ostvariti mehanizam za praenje alokacije memorije te emo napraviti objekt koji je
sposoban pratiti listu alociranih dijelova memorije. Prilikom alokacije segmenta

266

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;
Alokacija(size_t vel, void *pok, int lin,
char *dat);
~Alokacija();
void Ispisi();
};
Alokacija *prva, *zadnja;

267

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::Dodaj(size_t vel, void *pok,
int lin, char *dat) {

268

Alokacija *alo = new Alokacija(vel, pok, lin, dat);


alo->pret = zadnja;
alo->slijed = NULL;
if (zadnja)
zadnja->slijed = alo;
else
prva = alo;
zadnja = alo;
}
int MemorijskiAlokator::Brisi(void *pok) {
Alokacija *priv = prva;
while (priv) {
if (priv->pocetak == pok) {
if (priv->slijed)
priv->slijed->pret = priv->pret;
else
zadnja = priv->pret;
if (priv->pret)
priv->pret->slijed = priv->slijed;
else
prva = priv->slijed;
return 1;
}
priv = priv->pret;
}
return 0;
}
void MemorijskiAlokator::Ispisi() {
Alokacija *priv = prva;
while (priv) {
priv->Ispisi();
priv = priv->slijed;
}
}

Ugnijeena klasa ne moe direktno referencirati podatkovni ili funkcijski nestatiki


lan okolne klase. Vano je uoiti da su objekti okolne i ugnijeene klase meusobno
neovisni. Na primjer, stvaranje objekta okolne klase ne implicira stvaranje objekta
ugnijeene i obrnuto. Zato je pokuaj pristupa nekom lanu okolne klase iz
ugnijeene klase ekvivalentan pokuaju pristupa lanu iz glavnog programa: niti jedan
lan nema smisla sam za sebe ukoliko se ne promatra u kontekstu nekog objekta.
Drukija je situacija sa statikim lanovima. Njima Ugnijeena klasa moe pristupati
direktno, ne navodei podruje eksplicitno.
Podruje ugnijeene klase je umetnuto u podruje okolne klase. To znai da e
lan prva klase MemorijskiAlokator sakriti moguu globalnu varijablu prva. Na
primjer:

269

int prva;
class MemorijskiAlokator {
class Alokacija {
// ...
void Funkcija();
};
Alokacija *prva;
// ...
};
void MemorijskiAlokator::Alokacija::Funkcija() {
prva = 0;
// pogreka prilikom prevoenja
}

Pokuaj referenciranja globalne varijable prva rezultira pogrekom prilikom prevoenja


Identifikator prva se ne moe upotrebljavati bez objekta. Da bi se to ispravilo
potrebno je referencirati globalno podruje operatorom za razluivanje podruja.
Korektna definicija funkcijskog lana glasi:
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;
}

Nakon definicije objekta aloc gdje je klasa referencirana pomou operatora za


razluivanje podruja, mogue je posve legalno pristupati njegovim javnim lanovima.
Puni naziv ugnijeenih klasa obuhvaa naziv okolne klase odvojen
operatorom :: od naziva klase.

8.11. Lokalne klase


Klasa se takoer moe deklarirati u sklopu lokalnog podruja funkcije ili funkcijskog
lana. Njena je definicija vidljiva samo unutar bloka u kojem je definirana te u
blokovima unutar tog bloka. Za razliku od globalnih i ugnijeenih klasa, lokalna klasa
mora biti potpuno definirana unutar deklaracije. To znai da svi funkcijski lanovi
moraju biti umetnuti. Time je donekle ograniena upotreba lokalnih klasa na klase iji
funkcijski lanovi obavljaju posao od nekoliko linija kda. Lokalna klasa ne moe
deklarirati statike lanove.
Funkcija u sklopu koje je klasa definirana nema nikakvih posebnih prava pristupa
privatnim i zatienim lanovima lokalne klase. Pravo pristupa se moe dodijeliti tako
da se funkcija deklarira prijateljem klase. No skrivanje informacija u sluaju lokalnih
klasa nema previe smisla: lokalna klasa je vidljiva samo unutar funkcije gdje je
definirana tako da niti jedna druga funkcija ne moe pristupiti lanovima lokalne klase.
Zato su najee svi lanovi lokalne klase javni. Lokalna klasa moe pristupiti samo
statikim varijablama, tipovima i pobrojenjima definiranim u sklopu funkcije. Klasa ne
moe pristupati nestatikim automatskim varijablama. Evo primjera za deklaraciju
lokalne klase:
void SaLokalnomKlasom(int duljina) {
static int sirina = 8;
typedef void (*pokFunc)(int, int);
class Lokalna {
public:
pokFunc pf;
// OK: pokFunc je lokalni tip
int broj;

271

void Clan() {
broj = sirina;

//
//
broj += duljina; //
//

OK: irina je statika


varijabla
pogreka: duljina je
automatska varijabla

}
};
Lokalna lokVar;
// ... tijelo funkcije
}

8.12. Pokazivai na lanove klase


Jezik C++ je openamjenski jezik to znai da su njegova sintaksa i semantika
dizajnirane tako da zadovolje veinu zahtjeva koji se mogu postaviti prilikom
rjeavanja praktinih problema. Vaan cilj kojemu se teilo je openitost kda. To znai
da se jednom napisani kd moe koristiti za razliite svrhe. Mehanizam koji
omoguava vei stupanj openitosti nude pokazivai na lanove klasa (engl. class
member pointers). To je novi tip podataka koji se konceptualno razlikuje od obinih
pokazivaa te e njihova primjena biti objanjena na sljedeem primjeru.
Zamislimo da razvijamo programski paket za vektorsko crtanje. Jedna od funkcija
takvog programa je rezanje svih linija izvan odreenog pravokutnog podruja (engl.
clipping). Kako se crte preteno sastoji od niza linija, razvit emo klasu koja e
objedinjavati podatke o liniji. Klasa e se, prirodno, zvati Linija. Podaci o poloaju
linije na ekranu e se pohranjivati pomou etiri cjelobrojna lana (X1, Y1, X2 i Y2) koji
e pamtiti koordinate poetne i krajnje toke linije u pravokutnom koordinatnom
sustavu. Klasa e sadravati funkcijski lan Odrezi() koji e za parametar dobiti
koordinate gornjeg lijevog i donjeg desnog kuta pravokutnika te e njegova zadaa biti
podeavanje koordinata toaka linije tako da linija bude posve unutar zadanog

X2,Y2

odrezujue podruje
X1,Y1

Slika 8.4. Dijelovi linije koji padaju izvan podruja odrezivanja se uklanjaju

272

pravokutnika. Na slici 8.4 je prikazan taj problem.


Ako se malo pomnije udubimo u na problem, primjeujemo da se zadatak svoenja
linije unutar pravokutnika sastoji od dva sutinski ista dijela: prvo je potrebno odrezati
koordinate poetne, a zatim koordinate zavrne toke. Postupak koji se primjenjuje u
oba sluaja je isti, jedina je razlika to se jednom u proraunu koriste lanovi X1 i Y1, a
u drugom lanovi X2 i Y2. Kako je sam postupak proraunavanja koordinata toaka
dovoljno sloen, svaki ozbiljniji programer e teiti tome da rijei proraun openito, a
zatim ga primjeni jednom na poetnu, a drugi put na zavrnu toku. Jedno od rjeenja
koje se samo od sebe namee je smjetanje algoritma za proraun jedne toke u zasebnu
funkciju kojoj se prosljeuju pokazivai na cjelobrojne varijable s koordinatama toke
za koju se proraun obavlja. Ovakvo rjeenje, iako dosta razborito, nije primjenjivo u
naem sluaju.
Problem lei u tome to je adresa pojedinih lanova za svaki objekt drukija. To je
samo po sebi jasno, jer je svaki objekt smjeten u zaseban memorijski prostor pa su i
adrese pojedinih lanova unutar objekta razliite. Zato prilikom pisanja klase ne
moemo unaprijed znati memorijsku adresu pojedinih lanova. Ono to moemo znati
su relativne udaljenosti pojedinih lanova od poetka objekta, a one su za pojedini tip
objekata uvijek iste. Na primjer, podatkovni lanovi objekata klase Vektor su u svim
objektima smjeteni jedni iza drugih po istom redoslijedu. lan ax se nalazi na poetku
objekta, dok je lan ay smjeten dva bajta od poetka objekta. Stoga bismo funkciji koja
obavlja proraun mogli proslijediti kao parametre udaljenosti lanova.
Pokazivai na elemente klasa omoguavaju upravo to. Openito, oni mogu
pokazivati na podatkovne i na funkcijske lanove.
8.12.1. Pokazivai na podatkovne lanove

Neka je klasa Linija definirana na sljedei nain:


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::*

U deklaraciji je navedeno da pokaziva pokazuje na cjelobrojni lan, a lan pripada


klasi Linija. Deklaracija varijable pokKoord koja pokazuje na lan klase Linija
izgleda ovako:

273

int Linija::*pokKoord;

Vano je uoiti sutinsku razliku izmeu pokazivaa na cjelobrojnu varijablu i na


cjelobrojni lan klase. Neispravno je primijeniti operator za uzimanje adrese na lan
klase i dodijeliti rezultat obinom pokazivau
int *pokClan = &Linija::X1;

// neispravno

zato jer X1 nije smjeten negdje u memoriji raunala. To ne treba mijeati s


dohvaanjem adrese lana konkretnog objekta:
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:
int Linija::*pokKoord = &Linija::X1;

// ispravno

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;
};
class Linija { /* ... */ };
// ...
int Elipsa::*pokNaCjelobrojni = &Elipsa::ekscentricitet;
// neispravno: pokaziva pokazuje na cjelobrojni lan,
// a pokuano mu je pridruiti adresu realnog lana
int Linija::*pokNaClanLinije = &Elipsa::cx;
// neispravno: pokaziva pokazuje na lan klase Linija,
// a pridruena mu je adresa lana klase Elipsa
float Elipsa::*pokNaClanElipse = &Elipsa::ekscentricitet;
// ispravno: pokazivai se slau po tipu i po klasi

274

Svrha pokazivaa na lan klase je identifikacija pojedinog lana. Najee se


pokazivai na lanove implementiraju kao brojevi koji pokazuju udaljenost lana od
poetka objekta.
Pokaziva na lan klase ne sadri adresu nekog konkretnog objekta u
memoriji, ve njegova vrijednost jednoznano identificira neki lan klase.
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:
Linija objLinija, *pokLinija = &objLinija;
int main() {
int Linija::*pokCjelobrojni = &Linija::X1;
// pristup preko operatora .*
objLinija.*pokCjelobrojni = 7;
pokCjelobrojni = &Linija::Y1;
// pristup preko operatora ->*
pokLinija->*pokCjelobrojni = 9;
cout << objLinija.X1 << endl << objLinija.Y1 << endl;
return 0;
}

Nakon prevoenja i izvoenja gornjeg primjera dobit e se ispis


7
9

Pristup podacima preko pokazivaa na lanove se obavlja pomou operatora


->* (minus, vee od i zvjezdica) i .* (toka i zvjezdica).
Za razliku od operacija dozvoljenih nad obinim pokazivaima, skup dozvoljenih
operacija nad pokazivaima na lanove je suen. Kako pokazivai na lanove ne
pokazuju na stvarno mjesto u memoriji raunala, nije ih mogue implicitno konvertirati

275

u tip void *. Takoer nema implicitne konverzije u aritmetiki tip te nije podrana
pokazivaka aritmetika. To znai da naredbom
if (pokNaClan)

// neispravno: nema konverzije

nije mogue provjeriti je li pokaziva nul-pokaziva.


Mogue je definirati polje pokazivaa na lan. Postavljanje svih lanova klase
Linija na nulu moe se obaviti pomou polja pokazivaa:
void PostaviNaNulu(Linija &refLinija) {
int Linija::*pokLinija[] = {&Linija::X1,
&Linija::Y1,
&Linija::X2,
&Linija::Y2 };
for (int i = 0; i < 4; i++)
refLinija.*pokLinija[i] = 0;
}

Iako je ovakav nain brisanja elemenata u najmanju ruku bizaran te se jednostavnije


moe obaviti pomou etiri pridruivanja, ima prednosti u sluaju kada je broj lanova
klase velik.
Pokazivae na lanove mogue je prosljeivati kao parametre funkcijama.
Prikaimo rjeenje problema svoenja linije u pravokutnik pomou pokazivaa na
lanove.
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);
};
void Linija::Odrezi(int px1, int py1, int px2, int py2) {
OdreziTocku(&Linija::X1, &Linija::Y1,
X2 - X1, Y2 - Y1, px1, 1);
OdreziTocku(&Linija::X1, &Linija::Y1,
X2 - X1, Y2 - Y1, px2, 0);
OdreziTocku(&Linija::Y1, &Linija::X1,
Y2 - Y1, X2 - X1, py1, 1);
OdreziTocku(&Linija::Y1, &Linija::X1,
Y2 - Y1, X2 - X1, py2, 0);
OdreziTocku(&Linija::X2, &Linija::Y2,
X1 - X2, Y1 - Y2, px1, 1);
OdreziTocku(&Linija::X2, &Linija::Y2,

276

X1 - X2, Y1 - Y2, px2, 0);


OdreziTocku(&Linija::Y2, &Linija::X2,
Y1 - Y2, X1 - X2, py1, 1);
OdreziTocku(&Linija::Y2, &Linija::X2,
Y1 - Y2, X1 - X2, py2, 0);
}
void Linija::OdreziTocku(int Linija::*pok1,
int Linija::*pok2,
int v1, int v2, int p, int tip) {
if ((this->*pok1 < p && tip) ||
(this->*pok1 > p && !tip)) {
float param = (p - this->*pok1)/((float)v1);
if ((param > 0 && tip) || (param < 1 && !tip)) {
this->*pok2 = this->*pok2 + param * v2 + 0.5;
this->*pok1 = p;
}
else
this->*pok1 = this->*pok2 = -1;
}
}

Objasnimo nain rada ovog programa. Funkcijski lan Odrezi() je zaduen za


svoenje linije unutar zadanog pravokutnika pri emu parametri px1, py1, px2 i py2
odreuju koordinate gornjeg lijevog i donjeg desnog ugla pravokutnika. Funkcijski lan
poziva OdreziTocku() osam puta.
Funkcijski lan OdreziTocku() ima za parametre dvije koordinate toke te dvije
cjelobrojne varijable v1 i v2. Toka i par (v1, v2) zajedno odreuju parametarsku
jednadbu pravca koji ima vektor smjera odreen pomou v1 i v2, a prolazi kroz
zadanu toku. Parametar p odreuje koordinatu pravca u odnosu na koji se promatra
poloaj zadane toke. Parametar tip vrijednou 1 odreuje da toka mora imati prvu
koordinatu veu od koordinate pravca, dok vrijednou 0 odreuje da toka mora imati
prvu koordinatu manju od koordinate pravca. Ako se testom ustanovi da se toka nalazi
s pogrene strane promatranog pravca, izrauna se vrijednost parametra u kojoj toka
tono lei na pravcu te se pomou njega izrauna vrijednost druge koordinate. Radi
pravilnog zaokruenja prilikom konverzije realne vrijednosti u cjelobrojnu, dodaje se
vrijednost 0.5.
Algoritam podeavanja koordinate na pravac je podjednak kada promatramo x
koordinatu poetne toke te pravce x = px1 i x = px2 kao i kada promatramo y
koordinatu zavrne toke te pravce y = py1 i y = py2. Zato on rjeava openiti sluaj.
Funkcijski lan OdreziTocku() provjerava odnose izmeu lana na koji pokazuje
pok1 i pravca odreenog s p, te pomou njih izraunava vrijednost lana na koji
pokazuje pok2. Funkcijski lan Odrezi() je zaduen da primjereno pozove openiti
algoritam, prosljeujui jednom pokaziva na lan X1 kao prvi parametar i pokaziva na
lan Y1 kao drugi parametar, a drugi put pokaziva na Y1 kao prvi parametar i
pokaziva na X1 kao drugi parametar.

277

Vano je primijetiti da se prilikom pristupanja lanovima preko pokazivaa koristi


operator ->* kojem se s lijeve strane nalazi kljuna rije this. To znai da se pristupa
lanovima objekta za koji je pozvan funkcijski lan OdreziTocku().
8.12.2. Pokazivai na funkcijske lanove

Analogno pokazivaima na funkcije, mogue je definirati pokazivae na funkcijske


lanove. Pretpostavimo da elimo kontrolirati poziciju nekog grafikog objekta na
ekranu raunala, pri emu je svaki objekt prikazan jednim primjerkom klase
GrafObjekt. Ta klasa sadri funkcijski lan Crtaj() potreban za crtanje objekta te
lanove Gore(), Dolje(), Lijevo() i Desno() koji pomiu objekt u zadanom
smjeru za neki unaprijed odreen korak. lanovi imaju cjelobrojnu povratnu vrijednost
koja, postavljena na jedinicu, oznaava da je objekt pomaknut do ruba ekrana. Inae je
povratna vrijednost nula. Takoer pretpostavimo da postoji funkcijski lan
PostaviVelicinu() s dva parametra koji definiraju eljenu veliinu i irinu objekta.
eljeli bismo dodati lan PonoviPomak() koji bi ponavljao pomak u eljenom
smjeru odreeni broj puta. Kada bi algoritmi za pomak bili smjeteni u funkcijama te ne
bi bili lanovi klasa, funkcija PonoviPomak() bi kao parametar uzimala pokaziva na
funkciju pomaka i broj ponavljanja. No takvo rjeenje nije mogue, jer radimo s
funkcijskim lanovima klase, a ne s obinim funkcijama. Ne moemo, primjerice,
odrediti pokaziva na funkciju Gore() potrebno je koristiti pokazivae na funkcijske
lanove. Slino pokazivaima na podatkovne lanove, pokazivai na funkcijske lanove
ne odreuju direktno adresu funkcije na koju pokazuju, ve samo identificiraju odreeni
funkcijski lan klase. Da bismo pozvali funkcijski lan pomou pokazivaa na njega,
potrebno je navesti objekt ili pokaziva na objekt za koji se funkcijski lan poziva kao u
sljedeem primjeru:
class GrafObjekt {
// ... detalji implementacije su nebitni
public:
void Crtaj();
int Gore();
int Dolje();
int Lijevo();
int Desno();
void PostaviVelicinu(int sirina, int visina);
// funkcija PonoviPomak ima parametar smjer koji je tipa
// pokaziva na funkcijski lan klase GrafObjekt koji ne
// uzima parametre i vraa cijeli broj:
void PonoviPomak(int (GrafObjekt::*smjer)(), int puta);
};

278

Pokaziva na funkcijski lan se deklarira tako da se navede tip povratne vrijednosti,


klasa funkcije i njeni parametri; na primjer deklaracija pokazivaa pokNaFClan na
funkcijski lan klase GrafObjekt koji vraa cjelobrojnu vrijednost i nema parametara
izgleda ovako:
int (GrafObjekt::*pokNaFClan)();

U zagradama se navode parametri funkcijskog lana. Ako se eli definirati pokaziva na


lan koji uzima dva cjelobrojna parametra i ne vraa vrijednost pie se:
void (GrafObjekt::*pokNaDrugiClan)(int, int);

Vrijednost pokazivau se pridodjeljuje na sljedei nain:


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:
GrafObjekt grObj, *pokNaGrObj = &grObj;
// uzimanje adrese funkcijskog lana
int (GrafObjekt::*bezparam)() = GrafObjekt::Gore;
// poziv funkcijskog lana preko pokazivaa
(grObj.*bezparam)();
void (GrafObjekt::*dvaparam)(int, int);
dvaparam = GrafObjekt::PostaviVelicinu;
(pokNaGrObj->*dvaparam)(5, 6); // poziv preko pokazivaa

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

pokaziva na lan pretvori u pokaziva na funkciju, na koju se zatim primijeni operator


za poziv. Naprotiv, naredba

279

grObj.*bezparam();

bi se interpretirala kao
grObj.*(bezparam());

to ima znaenje pozovi funkciju bezparam() i njenu povratnu vrijednost vei za


operator .* te pristupi lanu. Takav poziv rezultira pogrekom prilikom prevoenja jer
bezparam nije pokaziva na funkciju.
Pokazivai na funkcijske lanove su, ba kao i pokazivai na funkcije, ovisni o
potpisu funkcije. To znai da je pokazivau mogue dodjeljivati samo pokazivae na
lanove koji imaju istu povratnu vrijednost i iste parametre te pripadaju istoj klasi:
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 jednoznano su odreeni tipom povratne


vrijednosti i potpisom funkcijskom lana te klasom kojoj pripadaju.
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:
void GrafObjekt::PonoviPomak(int (GrafObjekt::*smjer)(),
int puta) {

280

for (int i = 0; i < puta; i++)


if ((this->*smjer)()) break;
}

Parametar smjer je pokaziva na funkcijski lan koji odreuje smjer pomaka, te e


pokazivati na Gore(), Dolje(), Lijevo() ili Desno(). Broj pomaka je sadran u
parametru puta. Ako bismo eljeli pomaknuti neki objekt prema dolje etiri puta, to
bismo mogli uiniti na sljedei nain:
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;
};

Napiite funkciju DoktorePomozite() koja e kao parametar dobiti pokaziva na


objekt klase Tijelo te pokaziva na neki lan klase koji pokazuje na lan koji uzrokuje
probleme. Funkcija mora prvo provjeriti je li organ uope bolestan (tako da provjeri da
li je u lanu na koji pokaziva pokazuje upisano true). Ako nije, potrebno je ispisati
poruku o tome da je pacijent hipohondar, a u suprotnom je potrebno izlijeiti dotini
organ tako da se u njega upie true. Obratite panju da ta funkcija mora imati pristup
unutranjosti pacijenta (u punom smislu rijei), pa zbog toga mora biti deklarirana
prijateljem klase Tijelo (u stvarnosti je to ve diskutabilno).

8.13. Privremeni objekti


Objekti kojima smo rukovali do sada su bili uvijek eksplicitno stvoreni: ili su nastali kao
posljedica deklaracije ili je memorija za njih dodijeljena pomou operatora new. Osim
takvim izriitim naredbama, prevoditelj moe ponekad sam stvoriti neimenovani
privremeni objekt (engl. unnamed temporary) koji slui za privremeno pohranjivanje
vrijednosti. Ako prevoditelj na odreenom mjestu stvori privremeni objekt, on je
odgovoran za njegovo pravovremeno unitavanje. Tona mjesta na kojima se uvode
privremeni objekti, njihovo unitavanje i kopiranje nisu strogo definirana standardom

281

C++ jezika i u velikoj mjeri ovise o implementaciji prevoditelja. Svi primjeri navedeni u
ovom poglavlju su prevedeni pomou Borland C++ 4.5 prevoditelja.
8.13.1. Eksplicitno stvoreni privremeni objekti

Privremeni objekt se moe eksplicitno stvoriti tako da se navede naziv klase i u


okruglim zagradama parametri konstruktora. To svojstvo se esto koristi kod objekata
koji mogu graditi matematike izraze.
Vratimo se za trenutak natrag na problem vektorskog rauna. Zamislimo da elimo
napisati funkciju koja zbraja dva vektora. U odsjeku 8.2.4 je dan primjer funkcijskog
lana ZbrojiSa() koji zbraja vektore, no ono to sada elimo postii je neto sasvim
drugo. lan ZbrojiSa() se poziva za objekt klase Vektor i kao parametre ima x i y
komponente vektora s kojim se vektor zbraja. Sada elimo ostvariti funkciju (ne
funkcijski lan!) koja e kao parametre imati tri objekta klase Vektor. Funkcija e
zbrojiti prva dva operanda i rezultat smjestiti u objekt proslijeen kao trei argument.
Ponovimo deklaraciju klase i navedimo kd funkcije ZbrojiVektore():
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; }
};
void ZbrojiVektore(Vektor &a, Vektor &b, Vektor &rez) {
rez.ax = a.ax + b.ax;
rez.ay = a.ay + b.ay;
}

Zbrajanje dvaju vektora moemo obaviti tako da deklariramo tri objekta klase Vektor
te pozovemo funkciju ZbrojiVektore():
Vektor a(10.0, 2.8);
Vektor b(-2.0, 5.0);
Vektor c;
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;
ZbrojiVektore(Vektor(10.0, 2.8), Vektor(-2.0, 5.0), 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;
}
void ZbrojiVektore(Vektor &a, Vektor &b, Vektor &rez) {
cout << "Zbrajam" << endl;
rez.ax = a.ax + b.ax;
rez.ay = a.ay + b.ay;
cout << "Zbrojio sam" << endl;
}

Prije nego to pogledamo kako se stvaraju i unitavaju privremeni objekti, potrebno je


imati na umu da stvaranje i unitavanje tih objekata nije strogo definirano standardom te
ovisi o implementaciji prevoditelja. Tako primjerice Borland C++ 2.0 unitava sve
objekte na kraju bloka umjesto na kraju izraza. Navedeni ispisi se odnose na Borland
C++ 4.5. tovie, prilikom manipuliranja privremenim objektima mnogi prevoditelji
koriste razne trikove kojima optimiraju izvrenje programa i izbjegavaju nepotrebna
kopiranja. Radi toga dobiveni ispis moe ovisiti o stupnju optimizacije koju prevoditelj
provodi.
U sljedeem programskom odsjeku imamo dva eksplicitna poziva konstruktora
klase Vektor kojima se stvaraju privremeni objekti:
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;
}

Nakon izvoenja se dobiva sljedei ispis:


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;
}

Dobiveni ispis je ovakav:


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

8.13.2. Privremeni objekti kod prijenosa parametara u funkciju

U gornjem primjeru parametri funkcije za zbrajanje vektora su bile reference na objekte


klase Vektor. To znai da funkcija nije dobivala kopiju objekta. Ako bi funkcija

285

mijenjala sadraj parametara, mijenjao bi se i sadraj objekta navedenog u


parametarskoj listi. To se svojstvo koristi kod parametra rez: rezultat izraunavanja se
smjeta u objekt na koji pokazuje referenca te tako funkcija vraa vrijednost.
este su situacije kada funkcija mora dobiti kopiju objekta nad kojim obavlja razne
proraune. U tom se sluaju izmjene objekta ne odraavaju na objekt koji je naveden
kao stvarni parametar u pozivu funkcije. Kopija koju funkcija dobiva stvara se
konstruktorom kopije. Dodajmo konstruktor kopije i funkciju koja kao argument dobiva
vektor prenesen po vrijednosti te ga postavlja na nul-vektor (korisnost te funkcije je
upitna, no posluit e za demonstraciju).
class Vektor {
// ... ovdje idu deklaracije
public:
Vektor(const Vektor &ref);
};
Vektor::Vektor(const Vektor &ref) :
ax(ref.ax), ay(ref.ay), redbr(++brojac) {
cout << "Stvoren vektor pomou konstruktora kopije "
<< "pod brojem " << redbr << endl
<< "X: " << ax << "
Y: " << ay << endl;
}
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:
Uao sam u main
Stvoren vektor pod brojem 1
X: 12
Y: 3
Pozivam NaNulu
Stvoren vektor pomou konstruktora kopije pod brojem 2
X: 12
Y: 3
Uao u NaNulu
Postavio sam
Uniten vektor pod brojem 2

286

X: 0
Y: 0
Zavravam
Uniten vektor pod brojem 1
X: 12
Y: 3

Najprije je stvoren vektor c i inicijaliziran na vrijednosti 12 i 3. Zatim je pozvana


funkcija NaNulu(). Kako funkcija za parametar ima objekt koji se prenosi po
vrijednosti, dobit e privremenu kopiju vektora c koja se stvara pomou konstruktora
kopije. Ako konstruktor kopije nije definiran, koristi se automatski generirani
konstruktor kopije. Zatim se izvodi tijelo funkcije NaNulu(). Funkcija postavlja
vrijednost privremenog vektora na nulu i zavrava.
Privremeni objekt nastao kao posljedica prijenosa po vrijednosti se unitava uvijek
nakon izlaska iz funkcije i to tako da se poziva destruktor. Iz ispisa se vidi da destruktor
unitava objekt koji je postavljen na (0, 0). Zatim zavrava i glavni program. Unitava
se vektor c koji i dalje ima vrijednosti 12 i 3 vidimo da objekt sadrava svoje poetne
vrijednosti.
Konstruktor kopije mora biti dostupan u dijelu kda u kojem se parametar
prosljeuje. To u ovom sluaju znai da on mora imati javni pristup. U suprotnom e
prevoditelj dojaviti pogreku prilikom prevoenja da konstruktor kopije nije dostupan.
Prenoenje objekata po vrijednost je oito sporije nego prenoenje po referenci jer
se uvijek stvara dodatna kopija objekta nad kojom funkcija radi. Prenoenje objekata po
referenci je uinkovitije. No pri tome je potrebno biti vrlo paljiv, jer pri tome moe
doi do vrlo neugodnih pogreaka.
Promotrimo situaciju u kojoj se adresa lokalnog objekta dodjeljuje globalnom
pokazivau. Kako se privremeni objekt unitava nakon zavretka funkcije, nakon
zavretka funkcije pokaziva e pokazivati na memoriju koja vie ne sadrava objekt.
Takvi visei pokazivai (engl. dangling pointers) predstavljaju veliku opasnost po
program:
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;
}

U funkciji se uzima adresa parametra i dodjeljuje globalnom pokazivau. Nakon


zavretka funkcije privremeni objekt se unitava, no pok ivi i dalje te pokazuje na

287

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;
}

Funkcija NaNulu() se u ovom sluaju poziva tako da joj se za parametar navodi


privremeni objekt. Prema gore iznesenim pravilima moe se oekivati da e taj objekt
biti kopiran u privremeni objekt koji e biti proslijeen funkciji. Oba privremena
objekta bit e unitena nakon izlaska iz funkcije. No s Borland C++ 4.5 prevoditeljem
dobivamo sljedei ispis:
Uao sam u main
Pozivam NaNulu
Stvoren vektor pod brojem 1
X: 12
Y: 3
Uao u NaNulu
Postavio sam
Uniten vektor pod brojem 1
X: 0
Y: 0
Zavravam

Prevoditelj je izbjegao stvaranje jedne kopije tako to je privremeni objekt direktno


proslijedio funkciji. Prevoditelj ak nee provjeravati dostupnost konstruktora kopije jer
ga uope nee pozivati.
Slino se deava i prilikom inicijalizacije objekata. U poglavlju o konstruktoru
kopije reeno je da se inicijalizacija
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;
}

bi se mogla provesti tako da se stvori privremeni objekt koji se zatim konstruktorom


kopije preslika u a te se privremeni objekt uniti. No ispis koji se dobiva je sljedei:
Uao sam u main
Stvoren vektor pod brojem 1
X: 12
Y: 3
Uniten vektor pod brojem 1
X: 12
Y: 3

Vidi se da je privremeni objekt stvoren odmah na mjestu predvienom za vektor a.


Time je izbjegnuto nepotrebno kopiranje. U ovom sluaju, iako se konstruktor kopije ne
poziva, potrebno je da konstruktor kopije bude dostupan (to jest da ima javni pristup).
8.13.3. Privremeni objekti kod vraanja vrijednosti

esto je neophodno vratiti objekt kao rezultat funkcije. Dobar kandidat za tako neto je
funkcija ZbrojiVektore(). Prepravimo funkciju tako da uzima za parametre dva
operanda te vraa zbroj kao rezultat. Pri tome joj je potrebno dodijeliti prava pristupa
privatnim lanovima klase Vektor.
Vektor ZbrojiVektore(Vektor &a, Vektor &b) {
cout << "Uao u ZbrojiVektore" << endl;
Vektor rez(a.ax + b.ax, a.ay + b.ay);
return rez;
}

Kada se vraa vrijednost iz funkcije, pomou konstruktora kopije stvara se privremeni


objekt koji se inicijalizira objektom navedenim u naredbi return. Stoga konstruktor
kopije mora biti dostupan. Vraeni objekt se automatski unitava nakon to se izrauna
izraz koji je pozvao funkciju. Prilikom pozivanja funkcije sada moemo rezultirajuu
vrijednost funkcije pridruiti nekom objektu klase Vektor, kao u sljedeem primjeru:
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;
}

Ako prevedemo i izvedemo gornji primjer, dobit emo sljedei ispis:


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

Na poetku se stvaraju tri vektora a, b i c, a zatim se poziva funkcija


ZbrojiVektore(). U njoj se stvara lokalni objekt pod brojem 4 koji privremeno nosi
rezultat. Kada prevoditelj naie na naredbu return, pomou konstruktora kopije stvori
objekt pod brojem 5 koji vraa vrijednost, a lokalni objekt pod brojem 4 se unitava.
Vrijednost rezultata se preslikava u objekt c. Budui da operacija dodjele nije drukije
definirana, dodjela se obavlja tako da se vrijednost svakog lana privremenog objekta
dodijeli svakom lanu objekta c. Tako i objekt c ima broj 5. Zatim se unitava
privremeni objekt pod brojem 5. Zavretkom programa se briu lokalni objekti a, b i c.
Iako se moda na prvi pogled ini neloginim da se objekt 5 brie dva puta, treba se
sjetiti da je zbog toga to operacija dodjele nije posebno definirana, objektu c dodijeljen
takoer broj 5. Prvi put se brie privremeni objekt, dok se drugi put brie lokalni objekt
c.
Ovakav redoslijed kopiranja je podloan optimizaciji. Umjesto da se stvara lokalni
objekt rez koji se zatim smjeta u rezultat funkcije, mogue je stvoriti privremeni
objekt koji uva rezultat te se nakon zavretka funkcije jednostavno proglaava
rezultatom. Prepravimo funkciju ZbrojiVektore() i u tom smislu:

290

Vektor ZbrojiVektore(Vektor &a, Vektor &b) {


return Vektor(a.ax + b.ax, a.ay + b.ay);
}

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
X: 12
Y: 3
Stvoren vektor pod brojem
X: -2
Y: -6
Stvoren vektor pod brojem
X: 0
Y: 0
Ulazim u ZbrojiVektore
Uao u ZbrojiVektore
Stvoren vektor pod brojem
X: 10
Y: -3
Uniten vektor pod brojem
X: 10
Y: -3
Zavravam
Uniten vektor pod brojem
X: 10
Y: -3
Uniten vektor pod brojem
X: -2
Y: -6
Uniten vektor pod brojem
X: 12
Y: 3

1
2
3

4
4

4
2
1

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

// definicije konstruktora i funkcijskih lanova


// se ne mijenjaju
int &Tablica::Element(int indeks) {
return Elementi[indeks];
}

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
Da sam bio prisutan prilikom Stvaranja,
dao bih nekoliko korisnih savjeta
za bolji ustroj svemira.
Alfonso X, Mudri (12211284)

Strukture su tipovi podataka naslijeeni iz jezika C i obogaeni objektno orijentiranim


dodacima. U C++ varijanti strukture se u svojoj biti ne razlikuju znaajno od klasa. U
sljedeem poglavlju bit e objanjene razlike izmeu struktura i klasa s posebnim
osvrtom na razlike u deklaracijama u odnosu na C jezik.
Takoer, bit e obraeni tipovi podataka koji omoguavaju efikasnije koritenje
memorije: unije i polja bitova. Unije su posebni tipovi podataka koji omoguavaju
smjetaj vie razliitih tipova podataka na isto mjesto u memoriji. Polja bitova
omoguavaju jednostavan pristup pojedinim bitovima nekog podatka umjesto koritenja
sloenih bitovnih podataka.

9.1. Struktura ili klasa?


U svojoj C++ varijanti struktura (engl. structure) je tip podataka koji ima ista svojstva
kao klasa. Nain deklaracije je identian s tom razlikom to se koristi kljuna rije
struct. Struktura moe sadravati podatkovne i funkcijske lanove, konstruktore,
destruktore i kljune rijei za dodjelu prava pristupa. Mala, no bitna razlika u odnosu na
klase, je u tome to ukoliko se ne navede pravo pristupa elementi imaju podrazumijevan
javni pristup (za razliku od klasa koje imaju podrazumijevan privatni pristup). Evo
primjera strukture koja uva podatke zaposlenog radnika u poduzeu:
struct Zaposleni {
char *ime;
int brojGodina;
char spol;
char *odjel;
int brojUzdrzavaneNejaci;
unsigned short IzracunajPlacu();
};

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;
};
struct Par parBrojeva;

// deklaracija u C stilu

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:
Par parBrojeva;

// deklaracija u C++ stilu

Strukture se od klasa razlikuju i po tome to se, u sluaju da se ne navede tip


nasljeivanja, podrazumijeva javno nasljeivanje (kod klasa se podrazumijeva privatno
nasljeivanje). Inae strukture imaju svojstva identina klasama: mogu sadravati
statike lanove, reference, druge objekte i sl.
Strukture su vrste klasa kod kojih se podrazumijeva javni pristup lanovima,
te javno nasljeivanje, ukoliko drukije nije naznaeno.
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:
enum VrstaUnosa { unosOperator, unosBroj };
class Unos {
public:
VrstaUnosa tip;
int broj;
char *nizOperator;
};

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:
union Unos saTipkovnice;

// deklaracija la jezik C

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;
};
};

Definirajte funkcijske lanove CjVrij(), DVrij() i ZnVrij() za pristup


vrijednostima. lanovi moraju ispisati poruku o pogreki ako se pokua pristupiti krivoj
vrijednosti.

9.3. Polja bitova


Ako se u klasi pamti vie brojanih podataka od kojih se svaki moe smjestiti unutar
nekoliko bitova, neracionalno je koristiti posebni cjelobrojni lan za svaki od njih.
Pogodnije je upakirati vie podatkovnih lanova u memorijski prostor kojeg zauzima
jedna cjelobrojna varijabla i time utedjeti na memorijskom prostoru. Nadalje, esto je
prilikom pisanja programa koji direktno kontroliraju sklopove raunala potrebno
omoguiti kontrolu pojedinih bitova u jednoj cjelobrojnoj rijei. Slian problem bio je
razraen u odsjeku 2.4.11. Sa stanovita programera elegantnije rjeenje pruaju polja
bitova (engl. bit-fields).
Polja bitova su posebni podatkovni lanovi klase koji su svi upakirani u to manji
memorijski prostor. Jedno polje se definira tako da se iza naziva cjelobrojnog lana
doda znak : (dvotoka) iza kojeg se konstantnim izrazom navede broj bitova koliko ih
lan zauzima. Uzastopno navedena polja bitova e biti upakirana u jednu cjelobrojnu
varijablu. Na primjer:
class Prekid
public:
unsigned
unsigned
unsigned
};

{
short dozvoljen : 1;
short prioritet : 3;
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

10. Preoptereenje operatora


Napredak civilizacije se zasniva na proirenju
broja vanih operacija koje moemo obaviti, a
da ne razmiljamo o njima.
Alfred North Whitehead (1861-1947),
Introduction to Mathematics (1911)

Osnovno svojstvo C++ jezika je enkapsulacija pojam koji oznaava objedinjavanje


podataka i operacija. Operatori nisu nita drugo nego operacije definirane za neki tip
podataka, pa bi bilo vrlo praktino definirati operatore za korisnike tipove. Na primjer,
zbrajanje objekata klase Vektor se najprirodnije provodi tako da se na dva vektora
primjeni operator zbrajanja +. Jezik C++ to omoguava, a postupak definiranja
operatora za razliite operande se zove preoptereenje operatora (engl. operator
overloading).
Osim redefiniranja ve postojeih operacija, C++ jezik omoguava i definiranje
korisnikih konverzija. Time klase postaju jo slinije ugraenim tipovima podataka jer
se korisniki definirane konverzije automatski primjenjuju kada se odreeni objekt nae
na mjestu na kojem se oekuje neki drugi tip.

10.1. Korisniki definirane konverzije


Kako bi korisniki definirani tipovi bili to sliniji ugraenim tipovima, mogue je
definirati operacije koje automatski konvertiraju objekt neke klase u objekt neke druge
klase ili neki ugraeni tip i obrnuto. Konverzija u opem sluaju, sa stanovita klase,
moe biti dvosmjerna: moe se konvertirati objekt klase u neki drugi tip i moe se bilo
koji drugi tip konvertirati u objekt klase.
Konverzije se najee primjenjuju prilikom prosljeivanja objekata kao
parametara funkcijama, slino kao kad se cjelobrojni podatak prosljeuje kao parametar
funkciji koja oekuje float broj. Prevoditelj e prilikom prevoenja stvoriti privremeni
realni broj koji e imati vrijednost cjelobrojnog podatka te e se taj privremeni realni
broj proslijediti funkciji.
Problem konverzija promatrat emo na primjeru klase ZnakovniNiz. esti su
prigovori na sistem manipulacije znakovnim nizovima C++ jezika. Operacije kao to su
nadovezivanje i pridruivanje nizova su dosta komplicirane te zahtijevaju intenzivno
koritenje sustava za alokaciju memorije. Zgodno je imati tip podataka kojim bi se to
pojednostavnilo.

300

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
};

Namee se pitanje koje su sve konverzije prikladne za klasu ZnakovniNiz. Kako


postoji ve niz ugraenih funkcija koje manipuliraju znakovnim nizovima, jedna od
konverzija je svakako konverzija objekta klase ZnakovniNiz u pokaziva na prvi znak.
Drugim rijeima, na mjestima gdje se oekuje char *, ako se nae objekt
ZnakovniNiz potrebno je objekt zamijeniti vrijednou pokazivaa pokNiz. Druga
mogua konverzija je obrnutog smjera, naime, konverzija pokazivaa na znakovni niz u
objekt klase ZnakovniNiz.
10.1.1. Konverzija konstruktorom

Konstruktor koji ima jedan parametar obavlja konverziju podatka iz tipa parametra u
objekt klase. Dodajmo klasi ZnakovniNiz konstruktor, te destruktor koji e osloboditi
zauzetu memoriju. Kako bismo tono pokazali kako se provodi proces konverzije,
umetnut emo u konstruktor i destruktor naredbe koje ispisuju poruke prilikom
stvaranja i unitavanja objekta. Odmah emo dodati i konstruktor kopije koji e nam
kasnije biti potreban.
Konstruktor s jednim parametrom obavlja konverziju iz tipa parametra u tip
klase. Konstruktor kopije nije operator konverzije, jer bi on konvertirao tip u
samoga sebe.

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(const ZnakovniNiz &ref) :
pokNiz(new char[strlen(ref.pokNiz) + 1]) {
strcpy(pokNiz, ref.pokNiz);
}
ZnakovniNiz::~ZnakovniNiz() {
cout << "Uniten niz: " << pokNiz << endl;
delete [] pokNiz;
}

Konverzije se koriste najee prilikom deklaracije objekata te prilikom prosljeivanja


funkcijama:
void Funkcija(ZnakovniNiz niz) {
cout << "Pozvana funkcija s parametrom: ";
cout << niz.DajPokazivac() << endl;
}
int main() {
ZnakovniNiz a = "Niz a";
Funkcija("parametar");

//
//
//
//

konverzija prilikom
inicijalizacije
konverzija prilikom
prenoenja parametara

return 0;
}

Nakon izvoenja gornjeg programa dobiva se slijedei ispis:


Stvoren
Stvoren
Pozvana
Uniten
Uniten

niz: Niz a
niz: parametar
funkcija s parametrom: parametar
niz: parametar
niz: Niz a

Prilikom deklaracije objekta a obavljena je konverzija niza s desne strane znaka = u


objekt klase ZnakovniNiz. Slino se deava i prilikom prosljeivanja parametra
funkciji Funkcija(). Prije ulaska u funkciju stvara se privremeni objekt pomou
konstruktora s jednim parametrom. Objekt se prosljeuje funkciji te se obrauje, a kada
funkcija zavri, objekt se unitava. Na kraju se unitava i objekt a.
Ako je potrebno, standardne konverzije se primjenjuju prije poziva konstruktora.
Konverzija konstruktorom se primjenjuje samo ako nikakva druga konverzija nije

302

mogua. To znai da e se najprije pokuati standardna konverzija, a zatim e se pozvati


konverzija konstruktorom.
10.1.2. Eksplicitni konstruktori

Ponekad moe biti potrebno deklarirati eksplicitni konstruktor, odnosno konstruktor koji
se nee pozivati implicitno kao operator konverzije. To se moe uiniti tako da se
ispred deklaracije konstruktora navede kljuna rije explicit:
class Razlomak {
private:
float brojnik, nazivnik;
public:
// eksplicitni konstruktor
explicit Razlomak(float raz) : brojnik(raz),
nazivnik(1.0) {}
};

U gornjem kdu je definiran konstruktor koji obavlja konverziju realnog broja u


razlomak. No kako je definiran kao eksplicitan, on se nee pozivati osim u sluajevima
kada to programer eksplicitno zahtijeva, na primjer, eksplicitnom dodjelom tipa ili
izravnim pozivom konstruktora:
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

Konstruktori deklarirani pomou kljune rijei explicit se nee koristiti


kao operatori konverzije.

10.1.3. Operatori konverzije

Pomou operatora konverzije mogue je definirati pravila pretvorbe podataka u neki


drugi tip. Konverzija se obavlja pomou posebnog funkcijskog lana, ija je deklaracija
sljedeeg oblika:
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:
void Ispisi(char *pokZnak) {
cout << "Ispisujem niz: " << pokZnak << endl;
}
void Ispisi(int broj) {
cout << "Ispisujem broj: " << broj << endl;
}
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);

Mogue je koristiti i oblik konverzije koji slii funkcijskom pozivu:


Ispisi(int(a));

// isto kao i (int)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:
void PisiLong(long broj) {
cout << "Long broj: " << broj << endl;
}
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

const ZnakovniNiz &DajNiz() const { return naziv; }


operator int() { return indeks; }
operator ZnakovniNiz() { return naziv; }
};

Prevoditelj nikada nee provesti korisniki definiranu konverziju na rezultatu


konverzije. Iako je mogue Identifikator pretvoriti u ZnakovniNiz, a taj u
char *, to se nee provesti, te je slijedei poziv funkcije neispravan:
void PisiZnakovniNiz(char *niz) {
cout << niz;
}
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;
}

U ovom primjeru pretvorba tipa omoguava pristup implementaciji objekta ime se


naruava integritet objekta. To se moe sprijeiti tako da se umjesto konverzije u

306

char * uvede konverzija u const char *. Time se onemoguava pristupanje


pokazivanom znaku.

Nesmotreno napisane definicije operatora konverzije mogu vanjskom


programu omoguiti pristup implementaciji objekta korisnik objekta moe
naruiti njegov integritet, te time osnovni koncepti objektnom
programiranja padaju u vodu.
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);
};
ZnakovniNiz:: ZnakovniNiz(const Identifikator &ref) :
pokNiz(new char[strlen(ref.DajNiz().pokNiz)]) {
strcmp(pokNiz, ref.DajNiz().pokNiz);
}

Sljedei poziv je neispravan jer nije jasno da li se Identifikator treba pretvoriti u


ZnakovniNiz pomou operatora klase Identifikator ili pomou konstruktora
klase ZnakovniNiz:
void NekaFunkcija(ZnakovniNiz);
int main() {
Identifikator a(5, ZnakovniNiz("znn"));
NekaFunkcija(a);
return 0;
}

Programer mora eksplicitno naznaiti eljenu konverziju eksplicitnom pretvorbom tipa


tako da se operator pretvorbe pozove sintaksom za poziv funkcijskih lanova:
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

e inicijalizirati objekt godina na vrijednost "1997".


Zadatak. Promijenite operator konverzije u cijeli broj klase ZnakovniNiz tako da
rezultat bude broj sadran u nizu, ili 0 ako niz ne sadri broj. Na primjer, niz "-35" se
mora pretvoriti u cijeli broj -35, dok niz "1a4d" ne sadri ispravan broj te se mora
pretvoriti u broj 0.
Zadatak. Zadane su klase Tocka, Poligon i Pravokutnik koje opisuju geometrijske
likove u ravnini:
class Tocka {
public:
int x, y;
};
class Pravokutnik {
private:
Tocka gornjaDesna, donjaLijeva;
};
class Poligon {
private:
int brojVrhova;
Tocka *vrhovi;
};
Tocka je opisana pomou svojih x i y koordinata. Pravokutnik opisuju dvije toke:
gornja desna i donja lijeva. Poligon se sastoji od lana koji pokazuje broj vrhova
poligona, te od pokazivaa na niz toaka koje daju koordinate vrhova. Potrebno je
realizirati pretvorbu iz klase Pravokutnik u klasu Poligon. Pri tome konverziju
rijeite na dva naina: jednom kao funkcijski lan klase Pravokutnik, a drugi put kao
lan klase Poligon.

Zadatak. Konstruktor klase Razlomak promijenite tako da realni broj svede na najblii
razlomak (pri tome razlomak mora imati cijeli brojnik i prirodni nazivnik).

10.2. Osnove preoptereenja operatora


Ne mogu se svi operatori C++ jezika preopteretiti. Pet operatora navedenih u tablici
10.1, se ne mogu preopteretiti, dok su dozvoljeni operatori navedeni u tablici 10.2.
Tablica 10.1. Operatori koji se ne mogu preopteretiti

.*

::

?:

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.
Tablica 10.2. Operatori koji se mogu preopteretiti

+
~
++
+=
<<=

!
--=
>>=

*
,
<<
/=
[]

/
=
>>
%=
()

%
<
==
^=
->

^
>
!=
&=
->*

&
<=
&&
|=
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;
}
Vektor operator+(Vektor &a, Vektor &b) {
return Vektor(a.ax + b.ax, a.ay + b.ay);
}

309

Definirani operator + se moe koristiti na slijedei nain:


#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;
}

Ovako definirani operator za zbrajanje se ni po emu ne razlikuje od operatora za


zbrajanje brojanih tipova, primjerice cijelih brojeva. Izraz
c = a + b;

se interpretira kao
c = operator+(a, b);

Ovakav direktan poziv preoptereenog operatora je posve legalan, iako je daleko


praktinije koristiti operatore unutar izraza (zbog toga je uope preoptereenje
operatora i uvedeno).
Prilikom preoptereenja operatora ogranieni smo na operatore definirane za
nae klase nije mogue preopteretiti operatore za ugraene tipove.
Time su zlobni i tati programeri uskraeni za zadovoljstvo pisanja potpuno neitljivog
koda u kojem bi operator + za cijele brojeve imao znaenje dijeljenja.

10.3. Definicija operatorske funkcije


Svaki preoptereeni operator je definiran svojom operatorskom funkcijom. Ona
potpuno potuje sintaksu obinih funkcija te ima svoj naziv, argumente i povratnu
vrijednost. Operatorske funkcije mogu biti preoptereene, pod uvjetom da se mogu
razlikovati po svom potpisu.
Kako je definiranje operatora dozvoljeno samo za korisniki definirane klase,
barem jedan od argumenata operatorske funkcije uvijek mora biti objekt, pokaziva na
objekt ili referenca na objekt neke klase. Broj argumenata operatora se ne moe
mijenjati pa niti jedan od parametara ne moe imati podrazumijevanu vrijednost.
Operatorska funkcija moe biti definirana kao funkcijski lan klase, a moe biti
definirana i izvan klase kao obina funkcija. Za neke operatore nemamo mogunost
izbora operatorske funkcije za operatore

310

[]

()

->

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 operator+(const X &lijevo, 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.
Operatori se mogu preopteretiti samo za korisniki definirane tipove. U
sluaju da se operatorska funkcija definira izvan klase, barem jedan od
parametara mora biti klasa.
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*();
Y operator*(const Y &ref);
};

// unarni *
// binarni *

// definicija izvan klase


void operator&(const Y &ref);
// unarni &
Y operator&(const Y &lijevo, const Y &desno); // binarni &

Pojedine varijante operatora koriste se na slijedei nain:


Y a, b;
int main() {
*a;
//
a * b; //
&a;
//
a & b; //
return 0;
}

a.operator*();
a.operator*(b);
operator&(a);
operator&(a, b);

Iako se ini da je svejedno je li operatorska funkcija definirana unutar ili izvan klase,
postoji sutinska razlika u jednom i drugom pristupu.
Ako je operator definiran kao funkcijski lan, na lijevi argument se ne
primjenjuju korisniki definirane konverzije.
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);
};
Kompleksni Kompleksni::operator+(const Kompleksni &d) {
return Kompleksni(real + d.real, imag + d.imag);
}
Kompleksni operator-(const Kompleksni &l,
const Kompleksni &d) {

312

return Kompleksni(l.real + d.real, l.imag + d.imag);


}

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;
}

Pokuaj zbrajanja realnog i kompleksnog broja rezultira pogrekom prilikom prevoenja


jer se poziv
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

Sada se na oba parametra mogu primijeniti pravila konverzije podataka, te je poziv


ispravan. Simetrini operatori se obino definiraju izvan klase upotrebom konverzije i
definicijom izvan klase izbjegnuta je potreba da se definiraju posebno varijante
Kompleksni operator-(float l, const Kompleksni &d);
Kompleksni operator-(const Kompleksni &l,
const Kompleksni &d);
Kompleksni operator-(const Kompleksni &l, float d);

Nije mogue definirati operator zbrajanja koji u svojim parametrima ne


spominje niti jedan objekt klase, jer bi se time promijenilo znaenje
ugraenog operatora.

313

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:
Kompleksni operator+(float l, float d); // pogreka

Operatori redovito moraju pristupati implementacijskim detaljima klase. Kako se


simetrini operatori najee deklariraju izvan klase, oni pod normalnim uvjetima
nemaju pristup privatnim i zatienim lanovima. Tada ih je kljunom rijei friend
mogue uiniti prijateljem klase i time im omoguiti pristup implementaciji.
Zadatak. Implementirajte operatore +=, -=, *= i /= za objekte klase Kompleksni.
Operatorske funkcije neka budu deklarirane izvan klase, kako bi se konverzija mogla
primjenjivati na objekte s obje strane znaka. Obratite panju na tipove argumenata.
Uputa: poeljno je izbjei preesta kopiranja objekta. Takoer, potrebno je omoguiti
izraz tipa
a = b += c;

koji je sasvim korektan za ugraene tipove.


Zadatak. Omoguite usporeivanje kompleksnih brojeva operatorom ==. Razmislite o
tome da li e se operator definirati unutar ili izvan klase.
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
ZnakovniNiz a, b("Neto je trulo u naem C++ kdu.");
a = b;

314

podrazumijevani operator = e jednostavno lanu pokNiz objekta a dodijeliti


vrijednost lana pokNiz objekta b, te e zapravo oba objekta pokazivati na isti komad
memorije. Zbog toga emo definirati vlastiti operator pridruivanja:
class ZnakovniNiz {
// ...
public:
ZnakovniNiz& operator=(const ZnakovniNiz &desno);
};
ZnakovniNiz& ZnakovniNiz::operator=(const ZnakovniNiz
&desno) {
if (&desno != this) {
delete [] pokNiz;
pokNiz = new char[strlen(desno.pokNiz) + 1];
strcpy(pokNiz, desno.pokNiz);
}
return *this;
}

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;

bi se memorija na koju pokazuje pokNiz oslobodila prije dodjele, ime bi se unitio i


sadraj desnog operanda jer je to isti objekt. Operator pridruivanja moe imati
proizvoljnu povratnu vrijednost, no najee se vraa referenca na objekt kojem se
vrijednost dodjeljuje. Time se omoguava uzastopno dodjeljivanje. Naredba
ZnakovniNiz a, b;
b = a = "copycat";

se interpretira kao
b.operator=(a.operator=(ZnakovniNiz("copycat")));

te se vrijednost dodjeljuje i objektu a i objektu b. Time se postie da korisniki


definiran operator pridruivanja ima isto ponaanje kao i ugraeni operator.
Promotrimo jo nain na koji su deklarirani parametri u operatorskim funkcijama.
Mnogi operatori kao operande uzimaju reference na objekte, ime se postie bri rad
programa jer nema nepotrebnog kopiranja pomou konstruktora kopije. Parametri se
definiraju kao konstantni, to omoguava pozivanje operatora i za objekte deklarirane
konstantnima. Povratna vrijednost se kod operatora pridruivanja i operatora

315

obnavljajueg pridruivanja (engl. update assignment) najee definira kao referenca


na objekt kojem se vrijednost pridruuje. Kod operatora kao to su zbrajanje i
oduzimanje povratna vrijednost se definira kao objekt, a ne referenca na objekt, jer
operator izraunava rezultat koji se smjeta u privremeni objekt te se unitava na kraju
izraza u kojem je nastao.
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);

Klasi ZnakovniNiz emo dodati operator [] za pristup proizvoljnom znaku niza.


Operator e imati cjelobrojni parametar n te e vratiti referencu na n-ti znak niza.
Vraanje reference omoguava da se operator moe navesti i s lijeve i s desne strane
operatora za pridruivanje.
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
ZnakovniNiz a("Ivica i Marica");
cout << a[0] << a[6] << a[8];

Takav pristup lanovima znakovnog niza e posebno cijeniti prelaznici s jezika


PASCAL, s obzirom da je upravo takva sintaksa ugraena u PASCAL.
Operator [] se moe preopteretiti sa samo jednim parametrom nije mogue
definirati indeksiranje po dvije dimenzije.
Jedan od velikih nedostataka matrica ugraenih u C++ jezik je to to dimenzije matrice
moraju biti konstante poznate prilikom prevoenja. Program mora alocirati memorijski
prostor za najveu oekivanu matricu, to rezultira neefikasnim koritenjem memorije.

316

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);
}
};
Matrica::Matrica(int red, int stu) : redaka(red),
stupaca(stu), mat(new float[red * stu]) {}

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 );

Primjerice, moemo operator () preopteretiti tako da vraa podniz niza znakova.


Operator e imati dva parametra: cjelobrojnu varijablu start koja e pokazivati od
kojeg znaka je potrebno poeti uzimati podniz (nula oznaava prvi znak), te varijablu
duljina koja e pokazivati koliko je znakova potrebno uzeti. Povratna vrijednost e
biti objekt ZnakovniNiz koji e sadravati traeni podniz:
class ZnakovniNiz {
// ...
public:
ZnakovniNiz operator()(int start, int duljina);
};
ZnakovniNiz ZnakovniNiz::operator()(int start,
int duljina) {
char pomocni[200], *pok1 = pomocni;
char *pok2 = pokNiz + start;
while (duljina-- && *pok2) *(pok1++) = *(pok2++);
*pok1 = 0;
return ZnakovniNiz(pomocni);
}

Sada je mogue pozvati operator na sljedei nain:


int main() {
ZnakovniNiz a = "Kramer protiv Kramera";
cout << (const char *)a(2, 17) << endl;
return 0;
}

Program nakon izvoenja ispisuje


amer protiv Krame

Primijetite u gornjem primjeru korisniki definiranu konverziju privremenog objekta


koji se vraa iz operatora () u tip const char *. Vaan je modifikator const, jer je
operator << za ispis pomou tokova definiran tako da u sluaju ispisa znakovnih nizova
za parametar uzima const char *.
10.3.4. Operator ->

Ovaj operator se uvijek preoptereuje kao unarni i mora biti definiran kao funkcijski
lan. Kao rezultat on mora vratiti ili pokaziva na objekt, referencu na objekt ili sam
objekt neke klase. Izraz
x->m

319

interpretira se tako da se ponajprije promotri tip objekta x. Ako je x pokaziva na objekt


neke klase, izraz ima znaenje klasinog pristupa lanu m. Ako je x objekt ili referenca
na objekt, izraz se interpretira kao
(x.operator->())->m

Klasa objekta x se pretrai za preoptereenu verziju operatora ->. Ako operator ne


postoji, prijavljuje se pogreka prilikom prevoenja. U suprotnom se poziva operatorska
funkcija za objekt x. Operatorska funkcija mora vratiti pokaziva, referencu ili sam
objekt. Ako se vrati pokaziva, tada se na njega primijeni klasini operator -> za pristup
elementima klase. Ako se vrati objekt ili referenca, ponavlja se postupak, to jest trai se
operator -> u klasi objekta koji je vraen te se izvrava. Pojasnimo to primjerom:
#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;
}

Nakon izvoenja se dobiva slijedei ispis:


Pozvan operator -> od X
Pozvan operator -> od Y
lan: 1

Pojasnimo kako se izvodi navedeni primjer. Definirana su tri objekta klasa X, Y i Z.


Objekt objX ima referencu na objekt objY koji ima referencu na objZ. Prilikom
nailaska na objX->clan, prevoditelj ponajprije provjerava je li objX pokaziva. Kako
je to objekt, trai se operator -> u klasi X. Operator postoji te se izvodi; ako klasa X ne
bi sadravala operator, prijavila bi se poruka o pogreki. Operator ispisuje prvu poruku
te vraa referencu na objekt objY. Na njega se ponovo primjenjuje operator ->, kao da
pie objY->clan. Kako je vraena referenca, a ne pokaziva, postupak se ponavlja.
Trai se operator -> u klasi Y. On postoji te se izvodi ispisuje se druga poruka te se
vraa pokaziva na Z. Kako je sada rezultat pokaziva, preko njega se pristupa lanu
clan. Ispisuje se vrijednost 1, to nam pokazuje da se pristupa lanu objekta objZ, iako
i objX i objY imaju lan naziva clan.
Pomou operatora -> se mogu izgraditi klase koje slue kao pametni pokazivai
(engl. smart pointers). Pamet pokazivaa se sastoji u tome to se prilikom pristupa
objektu mogu obaviti dodatne akcije. Zamislimo da elimo izgraditi napredan sustav
upravljanja memorijom koji e omoguiti koritenje virtualne memorije na disku
raunala. Kako je osnovna memorija raunala esto nedovoljnog kapaciteta, esto je
potrebno nekoritene podatke prebaciti na diskovni sustav te ih pozivati po potrebi.
Poseban sustav za skupljanje smea (engl. garbage collection) moe u odreenim
vremenskim intervalima dijelove memorije koji se ne koriste jednostavno snimiti na
disk i osloboditi glavnu memoriju.
Upravljanje virtualnom memorijom moe biti dosta sloeno. Osnovna ideja je da se
za svaku klasu XX napravi i klasa PokXX pokazivaa na objekte klase XX. Klasa PokXX
mora sadravati operator -> kojim se pristupa objektu na kojeg se pokazuje, te podatke
o trenutnom smjetaju samog objekta (je li on trenutno u glavnoj memoriji; ako nije,
onda na kojem se disku nalazi, ili ak na kojem raunalu u mrei). Svaki pristup objektu
se obavezno mora obaviti preko odgovarajueg pokazivaa. Operator -> klase
pokazivaa e biti zaduen za to da dovede objekt u memoriju te vrati pokaziva na
njega. Pristup lanovima objekta na koji se pokazuje na taj e nain biti potpuno
neovisan o nainu smjetanja objekta. Programer ne mora voditi rauna o virtualnoj
memoriji, jedini ustupak je to se za pokazivae moraju koristiti objekti posebne klase.
U nastavku e biti dana samo skica rjeenja. Rjeenje koje bi u potpunosti funkcioniralo
je presloeno te je ovisno o operativnom sustavu na kojem se implementira.
Napominjemo da je klasa pokazivaa na objekte idealan kandidat da se napie pomou
predloaka koji e biti objanjeni u jednom od sljedeih poglavlja.

321

class XX {
// klasa XX je sam objekt koji ima svoje lanove
// ovisno o samom problemu koji se rjeava
};
class PokXX {
private:
int uMemoriji;
XX *pokXX;

//
//
//
//
//

postavljen na 1 pokazuje da
je objekt trenutno u memoriji
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;

Operator ->* za pristup lanovima preko pokazivaa na lanove se


preoptereuje kao klasian binarni operator te za njega ne vrijede ovdje
iznesena pravila.
}

Dakle, izraz
a->*b

se interpretira kao
a.operator->*(b)

ili, ako je operator deklariran izvan klase, kao


operator->*(a, b)

10.3.5. Prefiks i postfiks operatori ++ i --

U C++ jeziku postoje dvije verzije operatora ++ i --. Prefiks verzija se pie ispred
operanda, dok se postfiks verzija navodi iza njega. U konanoj varijanti standarda jezika
mogue je posebno preopteretiti obje verzije operatora.

322

Prilikom preoptereenja je potrebno da svaka od preoptereenih funkcija ima


jedinstven potpis kako bi se mogle razlikovati. Prefiks varijanta operatora ++ izgleda
ovako:
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
};
// deklaracije izvan klase
void operator--(Primjer &obj) {
// prefiks verzija
}
void operator--(Primjer &obj, int) {
// postfiks verzija
}

Prilikom pozivanja postfiks verzije operatora cjelobrojni parametar ima neku


podrazumijevanu vrijednost koju odreuje prevoditelj. Operator se moe i pozvati
navodei puno ime operatorske funkcije te se tada moe i eksplicitno navesti cjelobrojni
parametar, kao u sljedeem primjeru:
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

Programer ima potpunu slobodu u odabiru operacija koje e se obavljati u prefiks i


postfiks verzijama operatora. Izbor povratne vrijednosti je takoer dan programeru na
volju. Tako moemo primjerice preopteretiti operator -- da ispisuje odreene podatke
klase. Takav stil programiranja nije preporuljiv jer se time samo poveava neitkost
programa, no vano je razumjeti da programer ima odrijeene ruke prilikom odreivanja
to e njegov operator raditi.
10.3.6. Operatori new i delete

Operatori new i delete su zadueni za alokaciju i dealokaciju memorijskog prostora.


Kao i veina drugih operatora, i oni mogu biti preoptereeni. C++ jezik definira
globalne new i delete operatore koji su zadueni za alokaciju i dealokaciju. Svaka
klasa moe definirati svoje operatore koji se tada koriste za alokaciju memorije objekata
samo tih klasa.
U biti postoje po dvije varijante operatora new i delete: operator new za alociranje
jednog objekta i new [] za alokaciju polja objekata te operator delete za dealokaciju
jednog i delete [] za dealokaciju polja objekata. Posebno se mogu preopteretiti
varijante operatora za jedan, a posebno za polje objekata.
Kada se operatori new i delete deklariraju unutar klase, oni su automatski
statiki, te to nije potrebno posebno naznaivati.
Razlog za to je posve jasan: objekt jo ne postoji kada se poziva operator new te bi
pokuaj pristupa lanovima objekta rezultirao pristupom neinicijaliziranoj memoriji.
Operator delete se poziva nakon to je destruktor unitio element, pa bi opet pokuaj
pristupa lanovima rezultirao pristupom neincijaliziranoj memoriji.
Prilikom alokacije objekta neke klase, prvo se pokua pronai preoptereena verzija
operatora new u klasi koju se eli alocirati. Ako se eli alocirati jedan objekt, trai se
operator new, a u sluaju da se alocira niz objekata trai se operator new []. Ako se
operator ne nae, poziva se pripadajui globalni operator.
Operatori new i new [] mogu uzimati proizvoljan broj parametara, no prvi
parametar mora biti tipa size_t (taj tip je definiran je u biblioteci stddef.h).
Preostali parametri se mogu navesti eksplicitno prilikom pozivanja operatora u okruglim
zagradama odmah iza kljune rijei new. Povratna vrijednost mora biti void *. Evo
primjera:
void *operator new(size_t velicina, int br, char *naz);
// 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 []:
void *operator new[](size_t velicina, int poz);
// primjer poziva
Vektor *pok2 = new (49) Vektor[4];

Podjednako se deklariraju operatori kao funkcijski lanovi.


Operatori delete i delete [] moraju imati kao povratnu vrijednost void, dok im
prvi parametar mora biti void *. Taj parametar nosi pokaziva na segment memorije
koji se treba osloboditi. Ako se operator deklarira kao lan klase, moe imati drugi
parametar tipa size_t koji oznaava veliinu bloka koji se treba osloboditi. Nije
dozvoljeno deklarirati druge parametre tih operatora. Evo primjera deklaracije:
void operator delete(void *pok);
void operator delete[](void *pok);
// 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

Praktina primjena preoptereenja operatora new i delete moe biti poboljanje


sustava alokacije memorije koji je bio izloen u poglavlju o ugnijeenim klasama.
Svaka alokacija e se biljeiti u posebnu listu koja e se ispisati na kraju programa.
Alokacije e se pratiti u listi struktura koje e biti alocirane pomou funkcije malloc()
(definirane u standardnoj datoteci zaglavlja stdlib.h) jer se unutar operatora new ne
moe pozivati operator new (to bi rezultiralo rekurzijom). Memorijski blokovi za
objekte e se takoer alocirati funkcijom malloc(), a oslobaati funkcijom free().
Operatori new i new[] e imati dva opcijska parametra koji e pamtiti liniju kda i
naziv datoteke u kojoj je alokacija obavljena.
#include
#include
#include
#include

<stddef.h>
<malloc.h>
<string.h>
<iostream.h>

struct Alokacija {
Alokacija *sljeceda;
void *mjesto;
int linija;
size_t velicina;
char datoteka[80];
};
Alokacija *Prva = NULL, *Zadnja = NULL;
void DodajAlokaciju(int lin, size_t vel, char *dat,
void *mj) {
Alokacija *pom = (Alokacija*)malloc(sizeof(Alokacija));
pom->sljeceda = NULL;
pom->linija = lin;
pom->velicina = vel;
pom->mjesto = mj;
if (dat)
strcpy(pom->datoteka, dat);
else
pom->datoteka[0] = 0;
if (Zadnja)
Zadnja->sljeceda = pom;
else
Prva = pom;
Zadnja = pom;
}
void UkloniAlokaciju(void *mj) {
Alokacija *pom = Prva, *pom1 = NULL;
while (pom) {
if (pom->mjesto == mj) {
if (pom1)
pom1->sljeceda = pom->sljeceda;
else

326

Prva = pom->sljeceda;
if (!pom->sljeceda) Zadnja = pom1;
free(pom);
break;
}
pom1 = pom;
pom = pom->sljeceda;
}
}
void *operator new(size_t vel, int lin, char *dat) {
void *alo = malloc(vel);
DodajAlokaciju(lin, vel, dat, alo);
return alo;
}
void *operator new[](size_t vel, int lin, char *dat) {
void *alo = malloc(vel);
DodajAlokaciju(lin, vel, dat, alo);
return alo;
}
void operator delete(void *mjesto) {
UkloniAlokaciju(mjesto);
}
void operator delete[](void *mjesto) {
UkloniAlokaciju(mjesto);
}
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

Vektor *v = new (_ _LINE_ _, _ _FILE_ _) Vektor(4.0, 9.0);


// radi neki posao
delete [] p;
delete v;
// provjeri ima li alokacija koje nisu osloboene
IspisiListu();
return 0;
}

Zadatak. Za opisivanje polinoma u nekom programu koristit emo klasu Polinom


deklariranu na sljedei nain:
class Polinom {
private:
int stupanj;
float *koef;
public:
Polinom(int pocetniStupanj);
};

Napiite konstruktor koji e inicijalizirati polinom na stupanj zadan parametrom,


alocirati polje za koeficijente polja (oprez: broj koeficijenata je stupanj + 1), te
postaviti sve koeficijente na nulu. Takoer, dodajte konstruktor kopije koji e ispravno
obaviti kopiranje polinoma. Za ispravno unitavanje polinoma dodajte destruktor koji
e osloboditi alociranu memoriju.
Zadatak. Omoguite pristup pojedinom koeficijentu polinoma tako da preopteretite
operator []. Pri tome nula kao parametar oznaava slobodni lan polinoma. Ako
korisnik trai lan vei od stupnja polinoma, vraa se nula (jer su svi koeficijenti vei od
stupnja polinoma uistinu nula).
Zadatak. Kako klasa Polinom koristi dinamiku alokaciju memorije, podrazumijevani
operator dodjele nee biti ispravan. Napiite vlastiti operator dodjele koji e to
ispraviti.
Zadatak. Polinome je mogue zbrajati, oduzimati i mnoiti. Dodajte operatore +, - i *
koji e to omoguavati. Takoer, dodajte unarne + i - operatore za odreivanje
predznaka. Uputa: rezultat svih tih funkcija mora biti tipa Polinom, a ne Polinom &.
Drugim rijeima, elimo omoguiti pisanje izraza pomou polinoma.
Zadatak. Zbog potpunosti, pravilo dobrog programiranja nalae da ako za klasu postoji
neki aritmetiki operator, primjerice +, onda treba definirati i odgovarajui operator
obnavljajueg pridruivanja +=. Dodajte, stoga, klasi Polinom operatore +=, -= i *=.
Zadatak. Izraunavanje polinoma p u toki x u standardnoj matematikoj notaciji ima
oznaku p(x). Omoguite takvu notaciju i u C++ programima tako da preopteretite
operator (). Za izraunavanje polinoma koristite Hornerov algoritam:

328

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.

10.4. Ope napomene o preoptereenju operatora


Preoptereenje operatora je nesumnjivo vrlo moan alat za razvoj aplikacija visokog
stupnja sloenosti no postoje neka pravila koje je dobro imati na umu. Programeri koji
se po prvi put susreu s preoptereenjem operatora skloni su pretjerivanju u upotrebi
tog svojstva jezika. Prvo to se je potrebno upitati prilikom razvoja klase jest kojim je
operatorima prikladno opremiti klasu.
Za svaku klasu su definirani operatori = (pridruivanje) i & (uzimanje adrese). Svi
ostali operatori dobivaju svoje znaenje tek ako se definiraju za odreenu klasu.
Podrazumijevani operator pridruivanja radi tako da se svaki lan objekta s desne strane
jednostavno dodjeli istom lanu objekta s lijeve strane. Prvenstveno je potrebno obratiti
panju na to da li podrazumijevani operator pridruivanja obavlja operaciju koju mi
elimo.
Prilikom odreivanja skupa potrebnih operatora dobro je poeti od analize javnog
suelja klase. Mnogi funkcijski lanovi e biti daleko jednostavniji za upotrebu ako se
definiraju kao operatori. Zato je dobro prvo odrediti skup potrebnih funkcijskih lanova,
a potom iz njega izvesti skup potrebnih operatora.
Iako je gotovo sve operatore mogue redefinirati za sasvim proizvoljne funkcije, svi
operatori asociraju programera na neku odreenu operaciju. Operator = ima smisao
pridruivanja, dok operator + ima smisao zbrajanja. Iako je mogue zamijeniti uloge tih
dvaju operatora, dobiveni kd je potpuno neitljiv i nerazumljiv, da ne spominjemo
probleme koji nastaju u vezi s hijerarhijom operacija. Uputno je definirati + operator za
klase Vektor i Kompleksni u svrhu zbrajanje dvaju vektora odnosno kompleksna
broja te ne dovoditi itatelja programa u nedoumicu. Prirodno znaenje operatora + za
klasu ZnakovniNiz je nadovezivanje desnog operanda na kraj lijevog. Ako operator
nema prirodno asocijativno znaenje za odreenu klasu, bolje ga je uope ne definirati.
esto se sljedei funkcijski lanovi, koji su dijelovi javnog suelja klase, mogu
jednostavnije realizirati operatorima:

JeLiPrazan() postaje logiki operator negacije, operator!


JeLiJednak() postaje operator jednakosti, operator==
Kopiraj() postaje operator pridruivanja, operator=
Ako je definiran operator pridruivanja = i neki od binarnih operatora, primjerice +, tada
je uputno definirati i operator obnovljenog pridruivanja += kako se ne bi naruila
intuitivnost jezika.

329

Upotreba operatora u izrazima moe znatno degradirati performanse programa zbog


niza akcija kopiranja koje se primjenjuju prilikom prosljeivanja parametara i rezultata
operatorskim funkcijama. Promotrimo to se sve dogaa u naoko bezazlenom dijelu
programa (definicija operatora zbrajanja je dana u uvodnom dijelu ovog poglavlja):
Kompleksni a(5.0, 2.0), b(2.0, 6.0), c;
c = a + b;

Prilikom izvoenja operacije zbrajanja prosljeuju se reference na objekte a i b


operatorskoj funkciji. Izraunava se rezultat te se stvara privremeni objekt koji se vraa.
Zatim se taj privremeni objekt kopira u objekt c pomou operatora pridruivanja te se
unitava.
U ovoj raunskoj operaciji smo za prijenos rezultata koristili privremeni objekt koji
smo na kraju morali kopirati. Mnogo bolje rezultate prilikom izvoenja postigli bismo
da smo napisali funkciju kojoj prosljeujemo reference na tri objekta, dva parametra i
jedan objekt koji e uvati rezultat. Rezultirajuu vrijednost mogli bismo direktno
dodijeliti rezultirajuem objektu. Time bismo izbjegli potrebu za stvaranjem
privremenog objekta, njegovim kopiranjem i unitavanjem.
U sluaju klase Kompleksni dobici na brzini ne moraju biti spektakularni jer
objekt nije dugaak te se njegovo stvaranje, kopiranje i unitavanje moe obaviti u
razmjerno kratkom vremenu. No zamislimo da radimo s klasom Matrica koja ima
100100 elemenata. Prikladno je definirati operatore za zbrajanje i pridruivanje
objekata te klase. Sada potrebna kopiranja, alokacija i dealokacija memorije nisu
zanemarivi. tovie, vrijeme potrebno za alokaciju, kopiranje i unitavanje matrice
moe nadmaiti vrijeme potrebno za izraunavanje zbroja. Zato je prikladnije zaobii
upotrebu operatora te napraviti funkcijski lan koji e kao trei parametar dobiti
referencu na objekt u koji se smjeta rezultat. Time se izbjegavaju nepotrebna kopiranja.
Pouka ovog razmatranja jest da su operatori vrlo moan i praktian alat, no
prilikom koritenja potrebno je dobro odvagnuti prednosti i gubitke koji se
pojavljuju njihovom primjenom.

330

11. Nasljeivanje i hijerarhija klasa


Neka nepoznata gospoa obratila se George
Bernard Shawu: Vi imate najpametniji mozak
na svijetu, a ja imam najljepe tijelo; mi bismo
stoga proizveli savreno dijete. Gospodin Shaw
je odvratio: No to ako dijete naslijedi moje
tijelo, a Va mozak?
Hesketh Pearson: Bernard Shaw (1942)

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.

11.1. Ima li klasa bogatog strica u Ameriki?


elimo li napraviti dobar program za vektorsko crtanje, bit e potrebno omoguiti unos
razliitih grafikih elemenata na radnu plohu. Radi jednostavnosti izostavimo za
poetak vrlo korisne no pomalo ezoterine objekte kao to su Bezierove krivulje i
ograniimo skup objekata na linije, elipsine isjeke, krugove, poligone i pravokutnike.
Ako se detaljnije razmotri dani skup objekata, moe se ustanoviti da svi oni imaju niz
zajednikih svojstava: svaki objekt se moe nacrtati na eljenom podruju ekrana, moe
ga se translatirati za neki vektor te rotirati oko neke toke za eljeni kut. Takoer, svaki
objekt moe biti nacrtan u odreenoj boji koja se po elji moe mijenjati.
No svaki od tih objekata ima i svoja specifina svojstva koja u potpunosti opisuju
upravo njega. Primjerice, linija ima poetnu i zavrnu toku. Elipsin isjeak ima sredite
elipse, veliku i malu poluos te poetni i zavrni kut isjeka. Poligon ima niz toaka koje
odreuju njegove vrhove. Operacija crtanja e za svaki od tih objekata biti obavljena na
razliit nain. Isto vrijedi i za operacije translacije i rotacije.
Prirodno se namee implementacija navedenih objekata pomou hijerarhijskog
stabla. Osnovna klasa Grafobjekt definirat e zajednika svojstva svih grafikih
objekta bez navoenja kako se pojedina operacija mora obaviti. Takva klasa koja postoji
samo zato da bi ju se moglo naslijediti zove se apstraktna klasa (engl. abstract class).

331

Preostale klase e naslijediti tu klasu te e uvesti nova svojstva potrebna za opisivanje


objekata i redefinirati postojea u skladu sa samim objektom.
Razmotrimo kako e se izgraditi takvo stablo te pokuajmo razluiti koja svojstva
svaka od klasa u hijerarhiji mora definirati. Klasa GrafObjekt mora definirati
funkcijske lanove Crtaj(), Translatiraj() i Rotiraj() za crtanje, translaciju
odnosno rotaciju. Ti funkcijski lanovi ne mogu biti definirani jer ta klasa opisuje opi
grafiki objekt i definira njegova opa svojstva. Translatiraj() ima dva cjelobrojna
parametra koji e pokazivati komponente vektora za koji se translacija obavlja.
Rotiraj() ima tri brojana parametra; prva dva oznaavaju koordinate centra rotacije,
dok trei daje kut rotacije. Takoer, GrafObjekt mora sadravati podatkovni lan
Boja za pamenje boje objekta. Pretpostavit emo da se boja objekta moe zapamtiti
pomou cjelobrojnog podatkovnog lana. Kako bi se ostvarila konzistentnost javnog
suelja, taj lan e biti privatan, a pristupat e mu se pomou javnih funkcijskih lanova
PostaviBoju() i CitajBoju(). Ti lanovi mogu biti u potpunosti definirani jer se
boja postavlja i ita na nain jednak za sve objekte. Pretpostavit emo da moemo sve
koordinate pamtiti pomou cjelobrojnog pravokutnog koordinatnog sustava. Evo
deklaracije klase GrafObjekt:
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) {}
};

Kako funkcijski lanovi Translatiraj() i Rotiraj() ne mogu biti definirani, nije


specificirano njihovo tijelo. Parametri su namjerno navedeni bez imena, jer bi se u
suprotnom prilikom prevoenja dobilo upozorenje o tome da parametri nisu iskoriteni
unutar tijela lana. Istina, kd e se ispravno prevesti, no bolje je ne ignorirati
upozorenja i modificirati kd tako da se upozorenje izbjegne. Ovako se prevoditelju
eksplicitno daje na znanje da parametar nee biti koriten.
Liniju e opisivati klasa Linija koja mora uvesti nova svojstva potrebna za
ostvarenje njene funkcije. Ta nova svojstva su koordinate poetne i zavrne toke.
Zajednika svojstva, kao to su boja i rutine za njeno itanje i postavljanje, se
zadravaju nepromijenjenima. Iako bi bilo mogue definirati klasu Linija tako da se
zajedniki elementi jednostavno ponove, to je vrlo nepraktino. Takvih elemenata moe
biti znatno vie nego to je novih elemenata. Praktinije je uzeti postojei objekt i
jednostavno ga obogatiti novim elementima.
Drugo mogue rjeenje je u klasu Linija ukljuiti objekt klase GrafObjekt kao
podatkovni lan. Klasa bi tada izgledala ovako:

332

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);

Takav pristup ozbiljno naruava pravila skrivanja informacija. Programer mora


poznavati implementaciju klase Linija kako bi ju direktno koristio. Sljedei je
nedostatak to su neki lanovi, primjerice Crtaj(), redefinirani u novoj klasi. Tako
pozivi
l.Crtaj();
l.osnovna.Crtaj();

predstavljaju pozive razliitih funkcija. Programer mora paziti da izmijenjene funkcije


poziva direktno na samom objektu, dok se neizmijenjene funkcije moraju pozivati
pomou objekta osnovne klase. Takvo maltretiranje programera e sigurno prije ili
kasnije rezultirati pogrekama. Mogue je rjeenje ponoviti sve nepromijenjene
funkcije i u izvedenoj klasi kao u sljedeem primjeru:
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:
class Linija : public GrafObjekt {
// ...
};

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

klasi Poligon u potpunosti odgovaraju. Cjelokupno hijerarhijsko stablo prikazano je na


slici 11.1.
GrafObjekt

ElipsinIsj

Linija

Krug

Poligon

Pravokutnik

Slika 11.1. Hijerarhijsko stablo grafikih elemenata

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

ElipsinIsj

Linija

Krug

Poligon

Pravokutnik

TKrug

TekstObj

TPravokutnik

Slika 11.2. Hijerarhijsko stablo tekstualnih grafikih elemenata

335

Ovakav tip nasljeivanja se zove viestruko nasljeivanje (engl. multiple inheritance).


Jedna izvedena klasa sada ima vie osnovnih klasa.

11.2. Specificiranje nasljeivanja


Nasljeivanje se specificira tako da se iza naziva klase umetne lista osnovnih klasa. Ta
lista se sastoji od dvotoke te od naziva osnovnih klasa. Ispred svakog naziva se moe
navesti jedna od kljunih rijei public, private ili protected kojom e se opisati
tip nasljeivanja. Evo primjera:
// osnovne klase
class GrafObjekt {};
class TekstObj {};
// izvedene klase
class Poligon : public GrafObjekt {};
class Pravokutnik : private Poligon {};
class TPravokutnik : Pravokutnik, public TekstObj {};

Klase GrafObjekt i TekstObj su u ovom primjeru osnovne klase, odnosno klase od


kojih se nasljeuje. Klase Poligon, Pravokutnik i TPravokutnik su izvedene klase,
odnosno klase koje nasljeuju. Kako je prilikom deklaracije klase Poligon navedeno
javno nasljeivanje, klasa GrafObjekt se naziva javnom osnovnom klasom klase
Poligon. Naprotiv, klasa Poligon je privatna osnovna klasa za klasu Pravokutnik,
to je navedeno privatnim nasljeivanjem. Takoer moe postojati i zatiena osnovna
klasa u sluaju zatienog nasljeivanja.
Ako se prilikom nasljeivanja klasa izostavi tip nasljeivanja,
podrazumijeva se privatno nasljeivanje. Prilikom nasljeivanja struktura
podrazumijeva se javno nasljeivanje.
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

class GrafObjekt {
// osnovna klasa
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 : public GrafObjekt {
// izvedena klasa
private:
int x1, y1, x2, y2;
public:
void Crtaj();
void Translatiraj(int vx, int vy);
void Rotiraj(int cx, int cy, int kut);
};

Sada moemo deklarirati objekt ln klase Linija te pristupati lanovima linije, no


takoer moemo pristupati i lanovima klase GrafObjekt, i to na isti nain kao da su ti
lanovi navedeni u klasi Linija (to je jasno ako se razumije da svaka izvedena klasa
sadri sve lanove osnovnih klasa):
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

Slika 11.3. Struktura dvostruko vezane liste

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

Slika 11.4. Umetanje novog lana u dvostruko vezanu listu

postavljanje i itanje pokazivaa. Ta klasa definira opa svojstva i mehanizme elementa


u listi.
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);
};
void Lista::UgurajClan(Atom *pok, Atom *izaKojeg) {
// da li se dodaje na poetak?
if (izaKojeg != NULL) {
// ne dodaje se na poetak

339

// 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);
}
void Lista::GoniClan(Atom *pok) {
if (pok->Sljedeci() != NULL)
pok->Sljedeci()->StaviPrethodni(pok->Prethodni());
else
rep = pok->Prethodni();
if (pok->Prethodni() != NULL)
pok->Prethodni()->StaviSljedeci(pok->Sljedeci());
else
glava = pok->Sljedeci();
}

Funkcijski lan UgurajClan() e umetnuti lan u listu. Pokaziva pok e pokazivati


na lan koji se umee, dok e pokaziva izaKojeg pokazivati na lan koji je ve u
listi iza kojeg elimo ugurati novi lan. Ako lan elimo dodati na poetak, izaKojeg
mora biti jednak NULL. Funkcijski lan GoniClan() e izbaciti lan iz liste. Pri tome
e pokaziva pok pokazivati na lan kojeg elimo izbrisati.
Klase ije objekte elimo smjestiti u listu moraju naslijediti klasu Atom. Na primjer,
klasa LVektor e definirati vektor koji se moe smjestiti u listu:
class LVektor : public Atom {
private:
float ax, ay;
public:
LVektor(float a = 0, float b = 0) : ax(a), ay(b) {}
void PostaviXY(float x, float y) { ax = x; ay = y; }
float DajX() { return ax; }

340

float DajY() { return ay; }


};

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()) {
// ...
}

Podatkovnim lanovima pokSljedeci i pokPrethodni se ne moe pristupiti niti iz


klase LVektor niti izvana zato jer su definirani kao privatni. Da su definirani kao javni,
mogue bi im bilo pristupiti na klasian nain.
Ako je klasa izvedena od vie osnovnih klasa, govori se o viestrukom
nasljeivanju. Tada svaki od objekata iz osnovne klase postoji kao podobjekt u
izvedenoj klasi. Klasu LVektor mogue je dobiti iz klasa Atom i Vektor pomou
viestrukog nasljeivanja na sljedei nain:
class LVektor : public Atom, public Vektor {
public:
LVektor(float a = 0, float b = 0) : Vektor(a, b) {}
};

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

Slika 11.5. Grafiki prikaz klase LVektor

Evo kako se objekti mogu smjetati u listu i itati iz nje:


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();
}

Primijetite operatore za dodjelu tipa koji se koriste za dobivanje pokazivaa na ispravan


tip. Oni ine kd neitljivijim te su est uzrok pogrekama, ali su u naem primjeru
neophodni. Naime, klasa Lista vraa pokaziva na Atom ona ne zna za tip LVektor.
Kako bi se dobilo na sigurnosti programa, umjesto statikih dodjela tipa bolje bi bilo
koristiti dinamike dodjele, koje e biti opisane u poglavlju 13. Te dodjele bi se mogle
eliminirati upotrebom predloaka. No ako bismo u listi htjeli drati objekte razliitih
klasa, ovo nam je jedino rjeenje.
Zadatak. Klasi Lista dodajte operacije za dodavanje lana na poetak i kraj liste.
Takoer, dodajte lan UbaciListu() koji e kao parametar imati referencu na objekt
klase Lista. Taj lan e sve objekte iz liste navedene u parametru ubaciti u listu
pridruenu objektu iji je lan pozvan.

11.3. Pristup naslijeenim lanovima


Naslijeenim lanovima se moe pristupati jednostavno navoenjem njihovog imena.
Iako se tako ini da dotini lanovi pripadaju izvedenim klasama, oni ostaju u podruju
osnovne klase. Svakom lanu se moe pristupiti i preko osnovne klase pomou
operatora :: za odreivanje podruja. Na primjer:
LVektor lv;
lv.Atom::StaviSljedeci(NULL);
lv.Vektor::PostaviXY(5, 8);

U mnogim sluajevima je odreivanje podruja nepotrebno jer prevoditelj moe


jednoznano odrediti lan bez dodatnog navoenja. No u dva posebna sluaja je
potrebno dodatno odrediti lan:

kada se u izvedenoj klasi deklarira istoimeni lan,


kada u osnovnim klasama postoje dva ili vie lanova istog naziva.
lan u izvedenoj klasi skriva istoimeni lan u osnovnoj klasi. No lanu osnovne klase se
moe pristupiti tako da se navede podruje kojemu lan pripada pomou operatora za
odreivanje podruja. Ta je situacija slina onoj kada lokalni identifikator skriva
globalni identifikator. U oba sluaja navedeni identifikator oznaava entitet iz
neposrednog podruja. Na primjer:

342

#include <iostream.h>
class Osnovna {
public:
int i;
void Var() { cout << "Osnovna::i " << i << endl; }
};
class Izvedena : public Osnovna {
public:
int i;
// prekriva Osnovna::i !!!
void Int() { cout << "Izvedena::i " << i << endl; }
};

Identifikator i u klasi Izvedena prekriva identifikator i u klasi Osnovna.


Pridruivanje lanu bez navoenja podruja ima smisao pridruivanja lanu klase
Izvedena. lanu iz klase Osnovna pristupa se pomou navoenja podruja. To
ilustrira sljedei programski kd:
int main() {
Izvedena izv;
izv.i = 9;
// pristupa
izv.Osnovna::i = 20; // pristupa
izv.Var();
// ispisuje
izv.Int();
// ispisuje
return 0;
}

se Izvedena::i
se Osnovna::i
'Osnovna::i 20'
'Izvedena::i 9'

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.
Upotreba operatora za odreivanje podruja je obavezna kako bi se jednoznano
odredio lan kojem se pristupa. Na primjer, za deklaracije
class A {
public:
void Opis() { cout << "Ovo je klasa A" << endl; }
};
class B {
public:
void Opis() { cout << "Ovo je klasa B" << endl; }
};
class D : public A, public B { };

343

sljedei poziv je dvosmislen:


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();
obj.B::Opis();

// ispisuje 'Ovo je klasa A'


// ispisuje 'Ovo je klasa B'

Ovakav nain odreivanja podruja ima dva vrlo nezgodna nedostatka:


1. Onemoguen je mehanizam pozivanja virtualnih funkcijskih lanova. Naime, kada se
navede podruje funkcijskog lana pomou operatora za odreivanje podruja,
onemoguava se virtualan poziv funkcije te se poziv vee statiki (smisao i znaenje
virtualnih funkcija e biti kasnije objanjen).
2. Navedena nejednoznanost se protee u daljnjim nasljeivanjima. Pravilo dobrog
objektno orijentiranog dizajna kae da izvedene klase ne bi smjele uvesti dodatne
implementacijske detalje izvan suelja definiranog osnovnom klasom.
esto je prikladno stoga u izvedenoj klasi definirati istoimeni funkcijski lan koji e
objediniti funkcionalnost oba podlana. Na primjer:
class D : public A, public B {
public:
void Opis() { cout << "Ovo je klasa D" << endl; }
};

Time se jednostavno uklanja svaka mogua nejednoznanost. Sada je poziv


D obj;
obj.Opis(); // ispisuje 'Ovo je klasa D'

potpuno ispravan. Naravno da je jo je uvijek mogue pristupiti lanovima iz klase A i


B, kao na primjer:
obj.A::Opis(); // ispisuje 'Ovo je klasa A'

11.4. Nasljeivanje i prava pristupa


Ve je objanjeno da postoje tri tipa prava pristupa: javni, privatni i zatieni.
lanovima s javnim pristupom se moe pristupiti i izvan klase, dok se elementima s
privatnim i zatienim pristupom moe pristupiti iskljuivo unutar klase. Dodjeljujui

344

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.
11.4.1. Zatieno pravo pristupa

Osim ve poznatih javnih i privatnih prava pristupa, postoji i zatieno (engl.


protected) pravo pristupa koje primarno slui stvaranju suelja za nasljeivanje. Ono se
specificira pomou kljune rijei protected:
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:
class Linija : public GrafObjekt {
private:
int x1, y1, x2, y2;
public:
void Crtaj();
void Translatiraj(int vx, int vy);
void Rotiraj(int cx, int cy, int kut);
};
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.
Zatieni lanovi nisu javno dostupni, ali su dostupni iz naslijeenih klasa i
ine suelje za nasljeivanje.
Na kraju je jo potrebno napomenuti da pravo pristupa lanovima osnovne klase u
izvedenoj klasi ovisi o tipu nasljeivanja, o emu e biti vie rijei u narednim
odjeljcima.
11.4.2. Javne osnovne klase

Odreena klasa je javna osnovna klasa (engl. public base class) ako je u listi prilikom
nasljeivanja navedena pomou kljune rijei public. Svi elementi osnovne klase bit
e ukljueni u izvedenu klasu u kojoj e zadrati svoje originalno pravo pristupa.
Privatni lanovi nee biti dostupni iz izvedenih klasa niti iz preostalog programa.
Zatienim lanovima moi e se pristupiti iz izvedene klase, ali ne i iz glavnog
programa. Javni lanovi ostat e dostupni i iz glavnog programa i iz izvedene klase. To
se moe pokazati sljedeim primjerom:
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; }
};
class Linija : public GrafObjekt {
private:
int x1, y1, x2, y2;
public:
void Crtaj();
void Translatiraj(int vx, int vy);

346

void Rotiraj(int cx, int cy, int kut);


};

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
// ...
}
}

lan Brojac je na taj nain iskljuen iz suelja predvienog za nasljeivanje. Mogue


mu je pristupiti iz same klase GrafObjekt ili iz klase proglaene prijateljem klase
GrafObjekt. lan Boja je, naprotiv, deklariran kao zatieni lan. Izvan klase mu nije
mogue pristupiti, no kako je GrafObjekt javna osnovna klasa za klasu Linija,
mogue mu je pristupiti iz klase Linija. Na primjer:
#define PLAVA
#define BIJELA

35
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;
}

To znai da su zatieni lanovi iskljueni iz javnog suelja klase, ali su ukljueni u


suelje namijenjeno nasljeivanju. Javni lanovi osnovne klase su dostupni i izvan
programa i u izvedenoj klasi. Oni su dio i javnog suelja i suelja namijenjenog
nasljeivanju.
Javne osnovne klase se razlikuju od privatnih i zatienih osnovnih klasa po
vanom svojstvu:
Mogua je implicitna pretvorba pokazivaa i referenci izvedene klase u
njenu javnu osnovnu klasu.

347

Na primjer, pokaziva na objekt Linija moe biti implicitno pretvoren u pokaziva na


objekt GrafObjekt. To je dozvoljeno, zato jer svaki objekt klase Linija u sebi sadri
jedan podobjekt klase GrafObjekt. Prilikom pretvorbe e se vrijednost pokazivaa na
objekt Linija promijeniti tako da se dobije pokaziva na podobjekt GrafObjekt. Na
primjer:
Linija *pokLinija = new Linija;
GrafObjekt *pokObjekt = pokLinija; // implicitna pretvorba
pokObjekt->PostaviBoju(PLAVA);

Moe se postaviti pitanje zato je implicitna pretvorba pokazivaa i referenci ograniena


na javne osnovne klase. Da bi se to razjasnilo, potrebno je uoiti da je prilikom javnog
nasljeivanja cjelokupno javno suelje osnovne klase ukljueno u izvedenu klasu. To
znai da prilikom pretvorbe pokazivaa ne postoji opasnost da se time dozvoli pristup
nekom lanu koji se ne nalazi u javnom suelju izvedene klase.
11.4.3. Privatne osnovne klase

Klasa je privatna osnovna klasa (engl. private base class) ako se u listi nasljeivanja
navede s kljunom rijei private. Takoer, ako se izostavi kljuna rije koja odreuje
tip nasljeivanja, podrazumijeva se privatno nasljeivanje.
Prilikom privatnog nasljeivanja, privatni lanovi osnovne klase nisu dostupni
izvedenoj klasi, dok javni i zatieni lanovi osnovne klase postaju privatni lanovi
izvedene klase. Zamislimo da elimo na to jednostavniji nain od klase Vektor
nainiti klasu Kompleksni. Iako bi sa stanovita principa ispravnog objektno
orijentiranog dizajna bilo ispravnije napisati klasu Kompleksni neovisno od klase
Vektor, na prvi pogled se ini da vektori i kompleksni brojevi imaju mnogo
zajednikih svojstava: i jedni i drugi imaju dva realna lana koji pamte x i y vrijednosti,
odnosno realni i imaginarni dio. Na jednak se nain zbrajaju i oduzimaju, pa ak i
mnoe sa skalarom. Razlika je primjerice u tome to ne postoji skalarni produkt
kompleksnih brojeva; produkt kompleksnih brojeva se rauna po drugim pravilima.
Zato se moe initi prikladnim da se klasa Kompleksni jednostavno dobije
nasljeivanjem klase Vektor. Pretpostavimo za poetak da koristimo javno
nasljeivanje:
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

class Kompleksni : public Vektor {


public:
Kompleksni(float a = 0, float b = 0) : Vektor(a, b) {}
void MnoziSa(Kompleksni &broj);
};

Nedostatak ovakve hijerarhije klasa je u tome to je cjelokupno javno suelje klase


Vektor ukljueno u javno suelje klase Kompleksni. To znai da e i funkcijski lan
MnoziSkalarnoSa() biti dostupan u klasi Kompleksni. Time je omogueno
vektorsko mnoenje kompleksnih brojeva koje nema smisla i ne smije biti dozvoljeno:
Kompleksni z1, z2;
z1.MnoziSkalarnoSa(z2); // logika pogreka: operacija
// nema smisla za kompleksne
// brojeve

Bolje je rjeenje prilikom izvoenja klase Kompleksni koristiti privatno nasljeivanje.


Time svi javni i zatieni lanovi osnovne klase postaju privatni lanovi izvedene klase.
Javno suelje osnovne klase ne ukljuuje se u javno suelje izvedene klase; izvedena
klasa mora sama definirati svoje suelje. Definicija klase Kompleksni u tom sluaju
izgledala bi ovako:
class Kompleksni : private Vektor {
public:
Kompleksni(float a = 0, float b = 0) : Vektor(a, b) {}
float Realni() { return DajX(); }
float Imaginarni() { return DajY(); }
void MnoziSa(Kompleksni &broj);
};

Sada nije vie mogue pozvati primjerice lan MnoziVektorskiSa() na objektu klase
Kompleksni.
Privatno nasljeivanje se koristi u sluajevima kada izvedena klasa nije
podvrsta osnovne klase nego osnovna i izvedena klasa samo dijele
implementacijske detalje.
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:
Kompleksni *pokKompl = new Kompleksni(4.0, 6.0);
Vektor *pokVekt = pokKompl;

// ne smije biti dozvoljeno!

// pokVekt zapravo pokazuje na kompleksni broj koji se ne

349

// moe mnoiti skalarno s vektorom


pokVekt->MnoziSkalarnoSa(Vektor(7.0, 8.0)); // besmislica

Ovako je omogueno koritenje objekata na nepredvien nain te je uinjena ista


pogreka kao i prilikom javnog nasljeivanja. Zbog toga e prevoditelj dojaviti
pogreku prilikom prevoenja gornjeg programa.
Ako je apsolutno sigurno da je potrebna gornja konverzija, mogue ju je provesti
tako da se navede eksplicitna pretvorba:
pokVekt = (Vektor *)pokKompl;

Sada programer sam odgovara za eventualne neeljene efekte prilikom koritenja


konvertiranog pokazivaa.
11.4.4. Zatiene osnovne klase

Zatieno nasljeivanje se specificira tako da se prije naziva osnovne klase navede


kljuna rije protected. Time se svi javni i zatieni lanovi zatiene osnovne klase
(engl. protected base class) prenose kao zatieni u izvedenu klasu. Cjelokupno javno
suelje osnovne klase se prenosi kao suelje za nasljeivanje u izvedenu klasu. Sve
izvedene klase mogu imati pristup lanovima osnovne klase, a ostatak programa tim
lanovima ne moe pristupiti. Na primjer:
class Osnovna {
public:
int i;
protected:
int j;
private:
int k;
};
class Izvedena : protected Osnovna {
public:
void Pristupi();
};
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;
}

Ne postoji standardna konverzija pokazivaa na izvedenu klasu u pokaziva na osnovnu


klasu, osim u podruju izvedene klase. Na primjer:
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();
}

Razlozi za onemoguavanje ugraene pretvorbe izvan klase su identini onima zbog


kojih nije omoguena pretvorba u privatnu osnovnu klasu: smisao zatienog
nasljeivanja je skrivanje javnog suelja osnovne klase od vanjskog programa. Uz
dozvoljenu pretvorbu, javno suelje bi preutno postalo dostupno i vanjskom programu.
Drukije su okolnosti unutar klase: javno suelje osnovne klase je regularno dostupno i
unutar klase te zbog toga pretvorba ne moe naruiti princip skrivanja informacija. Ako
je izriito potrebna pretvorba izvan klase, mogue ju je postii pomou operatora za
dodjelu tipa.
U tablici Error! Reference source not found. pregledno su dane sve mogue
kombinacije prava pristupa prilikom nasljeivanja. U gornjem redu tablice nalazi se
pravo pristupa lana u osnovnoj klasi, a s lijeve strane popisani su tipovi navoenja.
Tablica 11.1. Pregled tipova nasljeivanja i prava
pristupa

Tip
nasljeivanja

Pravo pristupa u osnovnoj klasi


public

protected

private

public

public

protected

private

protected

protected

protected

private

private

private

private

private

11.4.5. Posebne napomene o pravima pristupa

U prethodnim odsjecima navedeno je da izvedena klasa ima u sluaju javnog ili


zatienog nasljeivanja pristup naslijeenim javnim i zatienim lanovima. Na prvi

351

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
}

Klasa Linija dobivena je javnim nasljeivanjem:


class Linija : public GrafObjekt {
private:
int x1, y1, x2, y2;
public:
void Crtaj();
void Translatiraj(int vx, int vy);
void Rotiraj(int cx, int cy, int kut);
};

Kako je svaka Linija ujedno i GrafObjekt, funkcija AnimirajBoju() imat e


dozvoljen pristup samo lanovima klase Linija naslijeenim od klase GrafObjekt.
Na primjer:
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; //
go.Boja = CRNA; //
//
ln.Boja = CRNA; //
//
}

OK: pristupa se naslijeenom lanu


pogreka: nema prava pristupa
objektima osnovnih klasa
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:
// funkcijski lanovi su nepromijenjeni
};
class Linija : public GrafObjekt {
friend void AnimirajBoju();
private:
int x1, y1, x2, y2;
public:
// funkcijski lanovi su nepromijenjeni
};
void AnimirajBoju() {
Linija ln;
GrafObjekt go;
ln.Boja = BIJELA;
go.Boja = BIJELA;
ln.Brojac = CRNA;
}

// OK
// pogreka
// 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.
11.4.6. Iskljuivanje lanova

Prilikom privatnog ili zatienog nasljeivanja ponekad moe biti prikladno ostaviti
originalno pravo pristupa pojedinom lanu osnovne klase. Na primjer, prilikom
privatnog izvoenja klase Kompleksni iz klase Vektor, javni lanovi DajX() i
DajY() automatski postaju skriveni. No kompleksni brojevi imaju svoj realni i
imaginarni dio kojima bi se takoer moglo pristupiti pomou tih lanova. Iako je
mogue u klasi Kompleksni ponoviti te lanove tako da se oni jednostavno pozovu
lanove osnovne klase, to samo dodatno unosi probleme u stablo nasljeivanja. Zbog
toga je mogue iskljuiti pojedini lan iz privatnog nasljeivanja te mu ostaviti njegovo
osnovno pravo pristupa:
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);
};
class Kompleksni : public Vektor {
public:
Kompleksni(float a = 0, float b = 0) : Vektor(a, b) {}
void MnoziSa(Kompleksni &broj);
Vektor::DajX;
// iskljuuje DajX iz mehanizma
// privatnog nasljeivanja
Vektor::DajY;
// iskljuuje DajY iz mehanizma
// privatnog nasljeivanja
};

Iskljuivanje se provodi tako da se u odgovarajuem dijelu izvedene klase navede


identifikator lana osnovne klase s punim imenom. Pri tome se ne navodi tip lana
ukoliko se radi o podatkovnom lanu, odnosno ne navodi se povratna vrijednost i
parametri ako se radi o funkcijskom lanu.
Ovim nainom nije mogue promijeniti pravo pristupa lana iz osnovne klase, ve
samo zadrati osnovno pravo pristupa. To znai da nije bilo mogue iskljuivanje
lanova DajX() i DajY() provesti u zatienoj ili privatnoj sekciji klase Kompleksni
jer bi se time mijenjalo pravo pristupa odreeno u osnovnoj klasi.

354

U zavrnu varijantu jezika C++ uvedena je deklaracija using (engl. using


declaration) koja omoguava bolje iskljuivanje pojedinih lanova iz nasljeivanja.
Mogue je da e se iskljuivanje opisano u ovom odsjeku nakon nekog vremena
iskljuiti iz jezika.
Zadatak. Implementirajte gore navedene klase grafikih objekata s obzirom na grafike
mogunosti vaeg raunala. Dodajte novi grafiki objekt PuniPravokutnik koji e
opisivati pravokutnik ispunjen nekom bojom.
Zadatak. Pomou klase Tablica definirane u poglavlju 6 o klasama, potrebno je
ostvariti klasu Stog. Ta klasa je slina klasi Tablica utoliko to se lanovi pamte u
nizu. Razlika je u tome to se novi lanovi mogu dodavati iskljuivo na vrh stoga, te se
mogu skidati samo s vrha stoga. Klasa Stog mora imati lanove Stavi() i Uzmi() za
stavljanje broja na stog te za skidanje broja sa stoga. Kako Stog nije Tablica, javno
suelje tablice nije poeljno ukljuiti u javno suelje stoga, pa zato koristite privatno
nasljeivanje.

11.5. Nasljeivanje i pripadnost


Vano je razumjeti osnovni smisao nasljeivanja. Ono opisuje relaciju biti izmeu
objekata izvedene klase i objekata osnovne klase. Svaki Pravokutnik jest ujedno i
Poligon, dok svaki Poligon jest ujedno i GrafObjekt. Stoga svaka operacija
primijenjena na GrafObjekt ima smisla ako je primijenjena na Pravokutnik ili
Poligon.
Nasljeivanjem se svi lanovi osnovne klase ukljuuju u objekt izvedene klase.
Svaki objekt izvedene klase sadrava podobjekt osnovne klase. Zbog te injenice
mogue je nasljeivanje upotrijebiti na krivi nain tako da se tim mehanizmom pokua
opisati relacija sadri.
Na primjer, poligon sadri etiri linije. Neiskusan programer bi mogao ustanoviti da
bi moda bilo pogodno da se klasa Pravokutnik realizira tako da se naslijedi klasa
Linija ime se pojednostavljuje problem: jedna linija je prisutna kao podobjekt
osnovne klase:
class Pravokutnik : public Linija {
// ...
};

No to znatno naruava koncept objektno orijentiranog programiranja i nasljeivanja.


Pravokutnik nije Linija. Klasa Linija moe primjerice sadravati funkcijski lan
koji vraa duinu linije. Duina je atribut koji nema smisla za poligon; tamo je mogue
uvesti funkciju koja izraunava povrinu pravokutnika. Time se naruava princip po
kojem svaka operacija koja ima smisla za izvedenu klasu mora imati smisla i za
osnovnu klasu.

355

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 Pravokutnik : public GrafObjekt {
private:
Linija L1, L2, L3, L4;
public:
// funkcijski lanovi
};

11.6. Inicijalizacija i unitavanje izvedenih klasa


Uvoenjem nasljeivanja potrebno je proiriti mehanizam konstruktora kako bi se
omoguila ispravna inicijalizacija izvedenih klasa. Konstruktor izvedene klase zaduen
je za inicijalizaciju iskljuivo lanova definiranih u pripadnoj klasi; lanovi osnovnih
klasa se moraju inicijalizirati u konstruktoru osnovne klase. Kako bi se odredio nain na
koji se inicijalizira objekt osnovne klase, konstruktor osnovne klase se moe navesti u
inicijalizacijskoj listi konstruktora izvedene klase ispisivanjem naziva osnovne klase i
navoenjem parametara konstruktora.
Modificirat emo klase koje definiraju grafike objekte tako da im dodamo
konstruktore. Klasa GrafObjekt sadravat e konstruktor koji e postaviti boju na
vrijednost zadanu parametrom. Konstruktor klase Linija imat e pet parametra: boju i
etiri koordinate koje opisuju poetnu i zavrnu toku linije. Evo kda koji to opisuje:
class GrafObjekt {
private:
int Brojac;
protected:
int Boja;
public:
GrafObjekt(int b = CRNA) : Boja(b) {}
// ostali lanovi su nepromijenjeni
};
class Linija : public GrafObjekt {
private:
int x1, y1, x2, y2;
public:
Linija(int b, int px, int py, int zx, int zy) :
GrafObjekt(b), x1(px), y1(py), x2(zx), y2(zy) {}
// ostali lanovi su nepromijenjeni

356

};

Konstruktori svih osnovnih klasa se mogu navesti u inicijalizacijskoj listi izvedene


klase. Ako se neka od osnovnih klasa ne navede u inicijalizacijskoj listi, prevoditelj e
automatski umetnuti kd koji poziva podrazumijevani konstruktor osnovne klase. U
sluaju klase Linija, izostavljanje konstruktora GrafObjekt iz inicijalizacijske liste
rezultiralo bi postavljanjem poetne boje na crnu (jer upravo to radi podrazumijevani
konstruktor klase).
Ako izvedena klasa nema konstruktor, prevoditelj e automatski generirati
podrazumijevani konstruktor koji e pozivati podrazumijevane konstruktore
osnovnih klasa (koji u tom sluaju moraju postojati).
Prilikom inicijalizacije objekta konstruktori osnovnih klasa izvode se prije nego to se
pone s izvoenjem konstruktora izvedene klase. Kako bi se jednoznano definirao
nain na koji se inicijaliziraju objekti klase, ustanovljen je obavezan redoslijed
inicijalizacije klase:
1. Konstruktori osnovnih klasa izvode se prema redoslijedu navedenom u listi
izvoenja klasa. Vano je uoiti da se ne potuje redoslijed navoenja u
inicijalizacijskoj listi (kao to se i podatkovni lanovi inicijaliziraju po redoslijedu
navoenja u deklaraciji klase, a ne po redoslijedu navoenja u inicijalizacijskoj listi).
2. Svaki se konstruktor lana izvodi u redoslijedu navoenja objekata (a ne u
redoslijedu navoenja u inicijalizacijskoj listi).
3. Na kraju se izvodi konstruktor izvedene klase.
Redoslijed pozivanja konstruktora je uvijek ovakav te nije podloan implementacijskim
zavisnostima.
Redoslijed pozivanja konstruktora osnovne klase definiran je redoslijedom
navoenja osnovnih klasa u listi nasljeivanja. Konstruktori objekata
lanova se izvode prema redoslijedu deklaracije objekata u klasi.
Za ispravno unitavanje klase koriste se destruktori. Prilikom unitavanja objekta poziva
se destruktor klase koji unitava samo dio objekta koji je definiran izvedenom klasom.
Automatski e se pozvati destruktor osnovnih klasa ime e se osloboditi dijelovi
objekta koji su definirani osnovnim klasama. Redoslijed pozivanja destruktora je tono
obrnut od redoslijeda pozivanja konstruktora.
Zadatak. Objasnite zato navedene deklaracije klasa ne daju oekivane rezultate:
class Prva {
public:
Prva(int);
};
class Druga {

357

public:
Druga(Prva &);
};
class Treca : public Druga, public Prva {
public:
Treca(int a) : Prva(a), Druga(*this) {}
};

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.

11.7. Standardne pretvorbe i nasljeivanje


Postoje etiri standardne pretvorbe koje se mogu provesti izmeu izvedene klase i
osnovnih klasa:
1. Objekt izvedene klase moe se implicitno pretvoriti u objekt javne osnovne klase.
2. Referenca na objekt izvedene klase moe se implicitno pretvoriti u referencu na
objekt javne osnovne klase.
3. Pokaziva na objekt izvedene klase moe se implicitno pretvoriti u pokaziva na
objekt javne osnovne klase.
4. Pokaziva na lan izvedene klase moe se implicitno pretvoriti u pokaziva na lan
javne osnovne klase.
Dodatno, pokaziva na bilo koju klasu moe se implicitno pretvoriti u pokaziva na tip
void *. Pokaziva na tip void * se moe jedino eksplicitnom dodjelom tipa pretvoriti u

pokaziva na bilo koji drugi tip.


Svaki objekt izvedene klase sadrava po jedan podobjekt svake osnovne klase.
Zbog toga je pretvorba izvedene klase u osnovnu sigurna. Obrnuto ne vrijedi. Uzmimo
za primjer stablo nasljeivanja koje je ve bilo navedeno u prethodnim poglavljima
(klasa LVektor proirena lanom redBroj koji sadri redni broj lana u listi):
class Atom {
private:
Atom *pokSljedeci, *pokPrethodni;
public:
// funkcijski lanovi nisu bitni
};
class Vektor {
private:
float ax, ay;
public:
// funkcijski lanovi nisu bitni
};
class LVektor : public Atom, public Vektor {
public:

358

int redBroj;
// funkcijski lanovi nisu bitni
};

Objekt klase LVektor se moe prikazati kao na slici 11.6.


pokaziva na LVektor
pokaziva na Vektor

pokSljedeci
pokPrethodni
ax
ay
redBroj

dio od klase Atom


dio od klase Vektor
dio od klase LVektor

Slika 11.6. Struktura objekta LVektor

Pokaziva na LVektor pokazivat e na poetak objekta. Prilikom pretvorbe u recimo


pokaziva na Vektor, bit e ga potrebno uveati tako da pokazuje na poetak
podobjekta Vektor.
Ako je potrebno pretvoriti pokaziva na objekt osnovne klase u pokaziva na objekt
izvedene klase, potrebno je koristiti eksplicitnu pretvorbu. Razmotrimo okolnosti zbog
kojih je to tako:
// pv se inicijalizira tako da pokazuje u biti na objekt
// klase LVektor
Vektor *pv = new LVektor;
// plv se inicijalizira tako da se pv eksplicitno pretvori
// u pokaziva na LVektor; to je OK, jer pv pokazuje u
// biti na objekt klase LVektor
LVektor *plv = (LVektor *)pv;
// dodjela je u redu, jer plv pokazuje na LVektor
plv->redBroj = 5;

U gornjem primjeru pokaziva pv na objekt klase Vektor se pretvara u pokaziva plv


na objekt klase LVektor. Takva konverzija se ne moe obaviti automatski, jer svaki
objekt klase Vektor ne sadrava podobjekt LVektor. Prevoditelj ne zna u trenutku
prevoenja da li pv pokazuje na objekt LVektor te ima li stoga takva konverzija smisla.
Zbog toga programer daje eksplicitnom dodjelom tipa prevoditelju na znanje da se
dodjela smije obaviti te da sve eventualne nepoeljne posljedice pretvorbe snosi sam
programer. U ovom sluaju sve je u redu, no evo primjera gdje koritenje takve
pretvorbe moe prouzroiti probleme:
// pv se inicijalizira tako da pokazuje u biti na objekt
// klase Vektor
Vektor *pv = new Vektor;

359

// plv se inicijalizira tako da se pv eksplicitno pretvori


// u pokaziva na LVektor; to je pogreno, jer pv pokazuje
// na objekt klase Vektor
LVektor *plv = (LVektor *)pv;
// dodjela e prouzroiti probleme, jer plv pokazuje na
// objekt Vektor, te e se broj 5 dodijeliti u nepoznato
// podruje memorije
plv->redBroj = 5;

U ovom sluaju programer je natjerao prevoditelja da pretvori pokaziva na objekt


Vektor u pokaziva na objekt LVektor. Dodjela lanu redBroj promijenit e sada
neko nepoznato memorijsko podruje. Velika je vjerojatnost da e se promijeniti neki
lan nekog sasvim drugog objekta koji je smjeten u memoriji na tom mjestu. Zbog toga
ovakav program ima velike anse zablokirati raunalo.
Pokazivai na lanove klasa se ponaaju obrnuto od obinih pokazivaa. Kako
svaka izvedena klasa sadri sve lanove osnovne klase, dozvoljeno je provesti
implicitnu konverziju pokazivaa na lan osnovne klase u pokaziva na lan izvedene
klase:
void Linija::Crtaj() {
int Linija::*pokClan;
// dozvoljena implicitna konverzija jer je Boja
// prisutna i u osnovnoj i u izvedenoj klasi
pokClan = &GrafObjekt::Boja;
}

Obrnuta konverzija, to jest konverzija pokazivaa na lan izvedene klase u pokaziva na


lan osnovne klase nije dozvoljena, jer osnovna klasa ne sadrava sve lanove koji su
prisutni u izvedenoj klasi:
void Linija::Crtaj() {
int GrafObjekt::*pokClan;
// pogreka: za implicitnu konverziju je potrebna
// eksplicitna dodjela tipa
pokClan = &Linija::x1;
// ovo je dozvoljeno, ali nije sigurno
pokClan = (int GrafObjekt::*)&Linija::x1;
// evo zato je gornja dodjela opasna: pokClan sada
// pokazuje na podruje izvan objekta GrafObjekt te
// e sljedea dodjela lanu objekta izazvati
// upisivanje u memoriju izvan objekta
GrafObjekt go;
go.*pokClan = 8;

360

// sljedea dodjela je u redu, jer se provodi dodjela


// objektu klase Linija
this->*pokClan = 9;
}

U gornjem primjeru, nakon eksplicitne dodjele tipa pokaziva pokClan se postavlja


tako da pokazuje na lan klase Linija. Kako taj lan ne postoji u klasi GrafObjekt,
pokClan e pokazivati na lan izvan objekta GrafObjekt. Pridruivanje broja 8
pomou varijable pokClan e rezultirati pridruivanjem u memorijsko podruje izvan
objekta ime e se vjerojatno unititi neki drugi objekt u raunalu. Naprotiv,
pridruivanje broja 9 pomou pokazivaa this nije opasna jer pokaziva this
pokazuje na objekt klase Linija u kojoj postoji lan na koji pokaziva pokazuje.

11.8. Podruje klase i nasljeivanje


Svaka izvedena klasa definira svoje podruje identifikatora u koje se smjetaju nazivi
deklarirani unutar klase. Kako se svaka izvedena klasa bazira na nekoj postojeoj
osnovnoj klasi, potrebno je definirati odnose izmeu podruja izvedene i osnovne klase.
Nasljeivanje rezultira ugnjeivanjem podruja. Moe se zamisliti da je podruje
izvedene klase okrueno podrujem osnovne klase. Kada se pokuava pronai neki
identifikator, pretrauje se prvo podruje klase kojoj objekt pripada. Ako se identifikator
ne moe pronai u tom podruju, pretrauju se podruja osnovnih klasa sve dok se ne
doe do datotenog podruja.
U sluaju viestrukog nasljeivanja pretrauju se podruja svih osnovnih klasa. Na
primjer, neka je potrebno pronai identifikator pokSljedeci u funkcijskom lanu
DajSljedeci():
class Atom {
protected:
Atom *pokSljedeci;
// ...
};
class Vektor {
// ...
};
class LVektor : public Atom, public Vektor {
// ...
Atom *DajSljedeci() { return pokSljedeci; }
};

Prvo se pretrauje lokalno podruje funkcijskog lana. Kako ne postoji identifikator s


tim nazivom u tom podruju, pretrauje se nadreeno podruje, a to je podruje klase
LVektor. Kako takav lan ne postoji niti u podruju klase kojoj funkcijski lan pripada,

361

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.
11.8.1. Razlika nasljeivanja i preoptereenja

Valja dobro uoiti i razumjeti razliku izmeu preoptereenja funkcijskih lanova i


nasljeivanja. Promotrimo nain na koji prevoditelj razlikuje lanove po potpisu (na
osnovu parametara navedenih u pozivu) u sluaju kada su pojedini identifikatori
navedeni u razliitim podrujima:
class Osnovna {
public:
void funkcija();
};
class Izvedena : public Osnovna {
public:
void funkcija(int);
};
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;
}

Nasljeivanje nije vrsta preoptereenja. Funkcijski lan u izvedenoj klasi prekrit e


istoimeni lan u osnovnoj klasi iako se oni razlikuju po potpisu. Sve funkcije nekog
skupa preoptereenih funkcija moraju biti definirane u istom podruju. Ako bismo
eljeli zadrati varijantu lana funkcija() s jednim parametrom i u klasi Izvedena,
morali bismo ponoviti njegovu definiciju:
class Izvedena : public Osnovna {
public:
void funkcija(int);
void funkcija() { Osnovna::funkcija(); }
};

362

11.8.2. Ugnijeeni tipovi i nasljeivanje

Na ugnijeene tipove se takoer primjenjuju pravila pristupa i nasljeivanja kao i za


obine podatkovne i funkcijske lanove. Na primjer, pretpostavimo da elimo razviti
klasu koja opisuje objekt koji pamti skup nizova znakova. Neka ta klasa podrava niz
operacija kojima se moe dodati element u skup, provjeriti da li neki element ve
postoji u skupu te po redu proi kroz sve elemente skupa. Uzmimo da elimo realizirati
skup pomou vezane liste kako se ne bi postavljalo ogranienje na broj elemenata liste.
Za takvu realizaciju potrebne su nam dvije klase: SkupNizova i Elem. Prva klasa
realizira sam skup, dok druga klasa realizira pojedini element skupa. Kako koritenje
klase Elem nema smisla izvan konteksta klase SkupNizova, eljeli bismo onemoguiti
stvaranje objekata klase izvan samog skupa. To moemo postii primjerice tako da
konstruktor klase Elem postavimo privatnim ili zatienim. Da bi se omoguilo
stvaranje elemenata unutar klase SkupNizova, potrebno je tu klasu uiniti prijateljem
klase Elem ime joj se stavlja na raspolaganje konstruktor klase. To izgleda ovako:
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);
};
};
class NaslijediOd : public SkupNizova::Elem {
// ...
};

11.9. Klase kao argumenti funkcija


Kako objekti mogu biti proslijeeni funkcijama preko parametara, potrebno je dodatno
proiriti mehanizme preoptereenja funkcija ime bi se omoguilo jednoznano
pronalaenje eljene funkcije.
Ako funkcija ima za parametre ugraene tipove, a u pozivu nije naveden isti tip (na
primjer parametar kae da se oekuje int, a u pozivu je stavljen long), tada se koriste
pravila standardnih konverzija kojima se navedeni parametar pretvara u oekivani.
Slino se moe dogoditi da skup preoptereenih funkcija oekuje jednu klasu, a poziv u

364

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.
11.9.1. Tono podudaranje tipova

Ako je objekt naveden u pozivu funkcije primjerak klase koja je navedena u


parametrima, onda se ta dva tipa tono podudaraju te je poziv preoptereene funkcije
time odreen. Ovo pravilo jo ukljuuje pretvorbu objekta u referencu na tip objekta
pomou trivijalne konverzije. Na primjer:
void Rastegni(GrafObjekt &obj);
void Rastegni(Linija &obj);
Linija lin;
// zbog tonog podudaranja donji poziv e pozvati
// Rastegni(Linija&)
Rastegni(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:
// pogreka: nije mogue razlikovati objekt i referencu na
// objekt
void Rastegni(Linija &obj);
void Rastegni(Linija obj);

11.9.2. Standardne konverzije

Ako ne postoji tono podudaranje tipova, prevoditelj e pokuati svesti tip stvarnog
argumenta na tip naveden u parametrima funkcije pomou standardne konverzije.
Objekt izvedene klase, referenca ili pokaziva e se implicitno pretvoriti u
odgovarajui tip javne osnovne klase. Na primjer:
void Deformiraj(GrafObjekt &obj);
Linija lin;
// OK: lin se konvertira u GrafObjekt&
Deformiraj(lin);

Pokaziva na bilo koju klasu se implicitno konvertira u tip void *, na primjer:

365

void PopuniNulama(void *pok);


// OK: provodi se konverzija u void *
PopuniNulama(&lin);

Vano je uoiti da ne postoji standardna konverzija u izvedenu klasu (razlozi za to su


dani u odjeljku 11.7), pa e prilikom prevoenja donjeg kda prevoditelj javiti
pogreku:
void ProduljiLiniju(Linija &obj);
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:
void Obradi(Atom &obj);
void Obradi(Vektor &obj);
LVektor lv;
//pogreka: postoje funkcije s obje javne osnovne klase
Obradi(lv);
// programer odreuje poziv funkcije Obradi(Vektor&) pomou
// eksplicitne dodjele tipa
Obradi((Vektor&)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 { };
void funkcija(A &obj);
void funkcija(B &obj);
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.
11.9.3. Korisniki definirane pretvorbe

Ako prevoditelj ne uspije pomou prethodna dva pravila pronai traenu


preoptereenu funkciju, pokuat e primijeniti korisniki definiranu konverziju kako bi
sveo navedeni tip na neki od ugraenih tipova.
Ako primjerice radimo program koji rauna s kompleksnim brojevima, razvit emo
klasu Kompleksni. No svi realni brojevi su ujedno i kompleksni brojevi kojima je
imaginarni dio nula te bi bilo pogodno da se, na mjestu gdje se oekuje kompleksni broj,
moe prihvatiti i realan broj koji se zatim interpretira kao kompleksan. Tada bismo
mogli napraviti funkcije Korijen1() i Korijen2() koje izraunavaju prvu i drugu
vrijednost korijena iz kompleksnog broja te bismo ih mogli koristiti i za obine realne
brojeve. Konverziju realnog broja u kompleksni moemo obaviti konstruktorom:
class Kompleksni {
private:
double cx, cy;
public:
Kompleksni(double a = 0, double b = 0)
: cx(a), cy(b) {}
// ...
};

Funkcije Korijen1() i Korijen2() imale bi sljedeu deklaraciju:


Kompleksni Korijen1(const Kompleksni &kompl) {
// ...
}
Kompleksni Korijen2(const Kompleksni &kompl) {
// ...
}

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:
Kompleksni kor = Korijen1(3.9);

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

Kompleksni kor = Korijen1(12);

rezultirat e proirivanjem cijelog broja 12 na tip double te e se zatim pozvati


odgovarajui konstruktor za konverziju. Konverzija tipa se provodi uvijek u jednom
koraku, to znai da prevoditelj nee uzastopno pozivati konverziju u neki tip koji e se
zatim konvertirati u neki drugi tip kako bi se postigao eljeni tip prilikom poziva
funkcije. Korisniki definirane konverzije e se koristiti samo ako se ne uspije pronai
funkcija za poziv koje su dovoljne trivijalne i standardne pretvorbe. Mogli bismo
funkcije preopteretiti i tako da bre rade za realne brojeve (njihovi korijeni se lake
izraunavaju nego korijeni iz openitih kompleksnih brojeva):
Kompleksni Korijen1(double broj) {
// ...
}
Kompleksni Korijen2(double broj) {
// ...
}

Poziv koji navodi tip za koji se ne mora koristiti korisnika konverzija


Kompleksni kompl = Korijen1(5.6);

rezultirat e pozivom funkcije Korijen1(double), a ne konverzijom u kompleksni


broj i pozivom funkcije Korijen1(Kompleksni &).
Prevoditelj e primijeniti standardnu konverziju na rezultat korisniki definirane
konverzije ako se time moe postii podudaranje parametara. To se esto moe koristiti
u kombinaciji sa standardnim konverzijama u osnovnu klasu. Zamislimo da elimo u
sklopu neke matematike aplikacije prikazivati vektore kao linije koje izlaze iz
ishodita. U prethodnim poglavljima razvili smo klasu Linija koja sadri sve potrebne
podatkovne i funkcijske lanove za opis linije. Svaki vektor se moe pretvoriti u liniju
tako da se prva koordinata linije postavi u ishodite, a druga se postavi u toku do koje
vektor see. Deklaracija klase Vektor s takvom konverzijom izgleda ovako:
class Vektor {
// ...
operator Linija();
};

Ako postoji funkcija koja kao parametar oekuje objekt klase GrafObjekt, mogue joj
je kao parametar navesti objekt klase Vektor:
void NestoRadi(GrafObjekt &go) {
// ...
}
Vektor v;

368

NestoRadi((Linija)v);
// rezultira konverzijom objekta klase Vektor u objekt
// klase Linija koja se ugraenom konverzijom moe
// svesti na klasu GrafObjekt

Operatori konverzije nasljeuju se po identinim pravilima po kojima se nasljeuju


obine funkcije.
Postoje li dvije korisnike konverzije koje se mogu ravnopravno primijeniti,
prevoditelj e prilikom prevoenja dojaviti pogreku o nejasnom pozivu. Na primjer,
ako bismo omoguili konverziju neke klase A i u klasu Kompleksni i u tip double,
poziv funkciji Korijen1() s objektom klase A kao parametrom bio bi nejasan:
class A {
public:
// ...
operator double();
operator Kompleksni();
};
A obj_a;
Korijen1(obj_a);

// pogreka prilikom prevoenja: nije


// jasno pretvara li se obj_a u
// double ili u Kompleksni

11.10. Nasljeivanje preoptereenih operatora


U poglavlju o preoptereenju operatora objanjeno je kako su preoptereeni operatori u
sutini zapravo samo posebni funkcijski lanovi. Prilikom nasljeivanja operatorskih
funkcija vrijede pravila iznesena za nasljeivanje obinih funkcija, no u nekim
sluajevima njihova primjena nije oita i sama po sebi razumljiva. Zbog toga emo
pogledati neke od tipinih sluajeva.
Jedno od osnovnih pravila nasljeivanja kae da nasljeivanje ima drukija svojstva
od preoptereenja, o emu ak i iskusniji korisnici C++ jezika esto ne vode rauna.
Preoptereene funkcije uvijek moraju biti navedene u istom podruju imena,
dok funkcije u naslijeenoj klasi skrivaju istoimene funkcije osnovne klase.
To pravilo podjednako vrijedi i za operatorske funkcije.
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);
};
class Izvedena : public Osnovna {
public:
// ...
int operator==(char *niz);
};
Izvedena obj;
if (obj == "ab") cout << "Jednako!" << endl;
if (obj == 1) cout << "Podjednako!" << endl;

// OK
// pogreka

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:
class Izvedena : public Osnovna {
// ...
public:
int operator==(char *niz);
int operator==(int i) {
return Osnovna::operator==(i);
}
};

Pravilo koje se takoer vrlo esto zaboravlja jest da se operator


pridruivanja ne nasljeuje.
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

Osnovna& operator=(int a);


};
class Izvedena : public Osnovna {
public:
Izvedena();
// ...
};
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.
Ako bi kojim sluajem postojao konstruktor s jednim cjelobrojnim
parametrom, operacija dodjeljivanja bi se preutno i bez upozorenja provela
kao kopiranje bit po bit privremenog objekta nastalog kao rezultat
konverzije.
U sluaju koritenja dinamike dodjele memorije unutar klase Izvedena ovakvo
pridruivanje e vrlo vjerojatno rezultirati pogrekom prilikom izvoenja programa. Da
bi se takve situacije izbjegle, potrebno je biti vrlo oprezan prilikom nasljeivanja klasa s
definiranim operatorom pridruivanja.
Takoer, zbog toga to se konstruktori ne nasljeuju, konverzije konstruktorom
takoer nee biti naslijeene. Ako je neka konverzija izriito potrebna, neophodno je u
izvedenoj klasi ponoviti konstruktor koji e ju obaviti. Na primjer, klasa Kompleksni
sadrava konstruktor kojim se realni brojevi mogu konvertirati u kompleksne. Ako
bismo napravili klasu LKompleksni koja bi nasljeivala i klasu Kompleksni i klasu
Atom te bi opisivala kompleksni broj u vezanoj listi, konstruktor konverzije bi trebalo
ponoviti:
class LKompleksni : public Kompleksni, public Atom {
public:
LKompleksni(double a = 0, double b = 0) :
Kompleksni(a, b) {}
// ...
};

371

Kako se konstruktori ne nasljeuju, ne nasljeuju se niti konverzije


konstruktorom.
Nasljeivanje operatora new i delete moe prouzroiti velike probleme u radu
programa ako programer nije posebno paljiv. Naime, ti operatori kao jedan od
argumenata imaju parametar tipa size_t koji oznaava koliko se memorijsko podruje
zauzima. Ako implementacija operatora ignorira tu vrijednost te primjerice uvijek
dodjeljuje memorijsko podruje veliine klase u kojoj je operator definiran, tada taj
operator nee funkcionirati ispravno za objekte izvedene klase. Objekti izvedenih klasa
mogu imati dodatne podatkovne lanove te e zbog toga za njih biti potrebno zauzeti
vee memorijsko podruje. Operator new koji uvijek zauzima fiksni komad memorije
nee zauzeti potrebnu memoriju te e dio objekta biti smjeten na memorijskom
podruju koje mu nije dodijeljeno. Takav program e vjerojatno pogreno funkcionirati.
Slino je i s operatorom delete; on mora osloboditi tonu koliinu memorije koja
je odreena parametrom tipa size_t, u suprotnom se moe dogoditi da se neka
memorija nikada ne oslobodi te se koliina slobodne memorije postupno smanjuje.
Program e funkcionirati sve dok se ne zauzme cjelokupna memorija, a tada e
jednostavno prijaviti nedovoljno slobodne memorije.

11.11. Principi polimorfizma


Do sada smo upoznali dvije osnovne znaajke koje svrstavaju C++ jezik u objektno
orijentiranu domenu. Prvo, to je enkapsulacija: svojstvo koje oznaava objedinjavanje
podataka i funkcija koje podacima manipuliraju. Umjesto da baratamo s pasivnim
strukturama podataka i aktivnim funkcijama, program se sastoji od objekata koji
meusobno stupaju u interakciju preko njihovog javnog suelja.
Sljedee vano svojstvo objektno orijentiranih jezika je mogunost stvaranja
hijerarhije klasa. Objekti se mogu izvoditi postepeno tako da se prvo definiraju opa
svojstva objekta koja se zatim detaljno opisuju u naslijeenim klasama. To je oito na
primjeru klasa koje opisuju grafike objekte: klasa GrafObjekt definira opa svojstva
grafikih objekata koja posjeduje svaki objekt, dok klase Linija i Poligon dodaju
svojstva koja su specifina za poligone i linije.
Polimorfizam je tree vano svojstvo koje svaki ozbiljniji objektno orijentirani
jezik mora podravati. Ono omoguava definiranje operacija koje su ovisne o tipu. U
prethodnim poglavljima opisano je kako se svaki objekt izvedene klase moe promatrati
i kao objekt bilo koje javne osnovne klase. Time se ne naruava integritet objekta jer su
svi lanovi osnovne klase prisutni i u objektima izvedene klase pa im se bez problema
moe i pristupiti. Postoje standardne konverzije opisane u prethodnim poglavljima koje
pretvaraju pokazivae, reference i objekte izvedenih klasa u pokazivae, reference i
objekte osnovnih klasa.
Vano je uoiti da se, iako je moda pokaziva na objekt klase Linija pretvoren u
pokaziva na objekt klase GrafObjekt, sam objekt na koji se pokazuje nije promijenio.

372

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() {}
};
class Linija : public GrafObjekt {
public:
// ...
void Crtaj();
// ovaj funkcijski lan e stvarno
// nacrtati liniju
};
Linija *pokLinija = new Linija;
GrafObjekt *pokGO = pokLinija;

Ako se pozove funkcijski lan Crtaj() preko pokazivaa pokLinija, prevoditelj e


pozvati funkcijski lan Crtaj() iz klase Linija zato jer je pokLinija definiran kao
pokaziva na objekt klase Linija:
pokLinija->Crtaj();

// poziva Linija::Crtaj()

Pokaziva pokGO je inicijaliziran tako to mu je dodijeljena vrijednost pokazivaa


pokLinija. No on i dalje u biti pokazuje na objekt Linija, te bi operacija crtanja i
dalje trebala nacrtati crtu. Meutim, poziv
pokGO->Crtaj();

// poziva GrafObjekt::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

zove? na jednak nain, bez obzira na to da li na njega gledamo kao na crnca,


pripadnika plemena Zulu ili kao na tono odreenog pojedinca. Odgovor na to pitanje
nije odreen okolnim uvjetima, nego samim objektom.
Navedeno ponaanje dolazi do izraaja u sluaju da bismo eljeli sve grafike
objekte nekog programa uvati u jednom polju. Ono bi se sastojalo od pokazivaa na tip
GrafObjekt koji bi po potrebi pokazivao na objekte izvedenih klasa. Crtanje svih
objekata se tada moe jednostavno provesti na sljedei nain:
int const MAX_ELT = 10;
GrafObjekt *nizObj[MAX_ELT];
// ...
nizObj[0] = new Linija;
nizObj[1] = new Poligon;
nizObj[2] = new ElipsinIsj;
// na isti nain se mogu popuniti i preostali lanovi polja
// ...
// ovo bi bio praktian nain crtanja svih lanova polja
for (int i = 0; i <= MAX_ELT; i++)
nizObj[i]->Crtaj();

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:
enum TipObj {GRAFOBJEKT, LINIJA, POLIGON, PRAVOKUTNIK,
ELIPSINISJ, KRUG};

374

class GrafObjekt {
// ...
public:
TipObj tip;
void Crtaj() {}
};
// deklaracije ostalih klasa se ne mijenjaju
int const MAX_ELT = 10;
GrafObjekt *nizObj[MAX_ELT];
// niz nizObj se popunjava po istom principu
// crtanje sada izgleda ovako:
for (int i = 0; i < MAX_ELT; i++)
switch (nizObj[i]->tip) {
case GRAFOBJEKT:
nizObj[i]->Crtaj();
break;
case LINIJA:
((Linija*)(nizObj[i]))->Crtaj();
break;
case POLIGON:
((Poligon*)(nizObj[i]))->Crtaj();
break;
// po istom principu se navode svi ostali tipovi
}

Predloeno rjeenje unato ispravnom funkcioniranju ima niz nedostataka. Oito je da je


na svakom mjestu u programu na kojem elimo pozvati neki funkcijski lan potrebno
ispisati cijeli switch blok, to moe biti vrlo nepraktino i zamorno. Takoer, ako se
kasnije doda novi grafiki objekt, potrebno je proi kroz cijeli program te dodati nove
instrukcije za pozivanje. Ponekad to moe biti i nemogue, u sluaju da izvorni kd
programa nije dostupan (na primjer, ako se radi proirenje neke postojee komercijalno
dostupne biblioteke klasa).
11.11.1. Virtualni funkcijski lanovi

C++ nudi vrlo elegantno i uinkovito rjeenje gore navedenog problema u obliku
virtualnih funkcijskih lanova. Osnovna ideja je u tome da se funkcijski lanovi za koje
elimo dinamiko povezivanje oznae prilikom deklaracije klase. To se ini
navoenjem kljune rijei virtual, a takvi lanovi se nazivaju virtualnim funkcijskim
lanovima (engl. virtual function members). Prevoditelj automatski odrava tablicu
virtualnih lanova koja se pohranjuje u memoriju zajedno sa samim objektom. Prilikom
poziva lana, prevoditelj e potraiti adresu lana u tablici koja je privezana uz objekt
te na taj nain pozvati lan.

375

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) {}
};
class Linija : public GrafObjekt {
private:
int x1, y1, x2, y2;
public:
Linija(int lx1, int ly1, int lx2, int ly2) :
x1(lx1), y1(ly1), x2(lx2), y2(ly2) {}
virtual void Crtaj();
virtual void Translatiraj(int vx, int vy);
virtual void Rotiraj(int cx, int cy, int kut);
};

Funkcijski lanovi Crtaj(), Translatiraj() i Rotiraj() deklarirani su kao


virtualni.
Virtualni lan se deklarira tako da se ispred povratnog tipa lana navede
kljuna rije virtual. Prilikom definicije funkcije izvan klase ta se kljuna
rije ne smije ponoviti.
Na primjer, definicija lana Crtaj() izgledala bi ovako:
void Linija::Crtaj() {
// virtual se ne ponavlja u definiciji
// ...
}

Prevoditelj e sve virtualne funkcijske lanove pohraniti u tablicu virtualnih lanova.


Ona se u literaturi esto oznaava skraenicom vtable. Toj tablici se ne moe direktno
pristupiti. Svaki objekt sadravat e skriveni pokaziva vptr na virtualnu tablicu.
Takav poziv se zove dinamiki poziv (engl. dynamic call) ili virtualni poziv (engl.
virtual call). To se moe prikazati kao na slici 11.7.

376

vptr
Boja
x1
y1
x2
y2
vptr
Boja
x1
y1
x2
y2

vtable
Crtaj()
Translatiraj()
Rotiraj()

Slika 11.7. Mehanizam virtualnih


funkcija

Prilikom poziva funkcijskog lana prevoditelj e generirati kd koji e pomou


pokazivaa vptr pronai tablicu virtualnih funkcija, a zatim e u njoj pronai tonu
adresu funkcije koju treba pozvati. Vidi se da je odluka o pozivu odgoena za trenutak
izvoenja programa te rezultat ovisi o stvarnom objektu za koji se lan poziva.
Osim konstruktora, sve ostale funkcijske lanove moemo uiniti i
virtualnima.
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();
pokB->func();
return 0;
}

// ispisuje A::func()
// ispisuje C::func()

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

Kada je neki funkcijski lan deklariran kao virtualan, on je automatski


virtualan i u svim naslijeenim klasama.
poziva pomou virtualne tablice. To rezultira pozivom funkcijskog lana samog
objekta, a ne lana klase B. Zato se u stvari poziva lan C::func().
Ako bi se ispred deklaracije lana func() u klasi C i izostavila kljuna rije virtual,
funkcija bi bila virtualna jer je ve uinjena virtualnom u klasi B. Funkcijski lan koji je
jednom deklariran virtualnim, ne moe se u izvedenoj klasi pretvoriti u lan sa statikim
pozivom.

378

11.11.2. Poziv virtualnih funkcijskih lanova

Virtualni funkcijski lanovi mogu se pozvati pomou objekta klase, preko pokazivaa
ili reference na neki objekt te pozivom unutar klase. C++ jezik definira skup pravila
pomou kojih se moe odrediti kako e se pozvati neki funkcijski lan.
Modificirat emo hijerarhiju grafikih objekata tako to emo dodati mogunost
brisanja objekta s ekrana te mogunost premjetanja objekta s jednog mjesta na drugo.
Kako ne postoje funkcije za crtanje koje bi se na isti nain izvodile na svim raunalnim
platformama, radi openitosti kda stavit emo da pojedini funkcijski lan umjesto
stvarnog crtanja na ekran ispisuje poruku o tome koja je funkcija pozvana. Valja uoiti
da se postupak premjetanja objekta sastoji iz tri osnovne operacije: brisanja objekta s
ekrana, promjene koordinata te njegovog ponovnog crtanja. Svaki objekt mora definirati
kako se za njega obavlja pojedini od tih koraka, odnosno kako e se objekt nacrtati,
kako e se izbrisati te kako e mu se promijeniti koordinate. Operaciju crtanja obavit e
funkcijski lan Crtaj(), operaciju brisanja lan Brisi(), a operaciju pomaka lan
Translatiraj(). Funkcijski lan koji e objedinjavati te tri operacije nazvat emo
Pomakni(), a kao parametre imat e dva broja koji e definirati vektor za koji se
pomak obavlja. Taj lan e se definirati samo jednom te e pomou mehanizma
virtualnih funkcija pozivati ispravnu funkciju za brisanje, pomak i crtanje.
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);
};
void GrafObjekt::Pomakni(int px, int py) {
Brisi();
Translatiraj(px, py);
Crtaj();
}

Iz gornjeg primjera je vidljivo da se pozivi funkcijskih lanova iz drugih lanova klase


tretiraju kao virtualni. Takoer, poziv preko pokazivaa ili reference na objekt bit e
virtualan:
Linija duzina;
GrafObjekt *go1 = &duzina, &go2 = duzina;
go1->Crtaj();
go2.Crtaj();

// virtualan poziv
// ponovo virtualan poziv

379

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:
duzina.Crtaj();
(*go1).Crtaj();

// statiki poziv
// virtualan poziv

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:
duzina.GrafObjekt::Crtaj();
go1->GrafObjekt::Crtaj();

//
//
//
//

statiki poziva
GrafObjekt::Crtaj()
statiki poziva
GrafObjekt::Crtaj()

Neispravno je pomou pokazivaa na neku klasu pozivati funkcijski lan iz izvedene


klase, 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);

lan Pomakni() se poziva statiki, no lanovi Brisi(), Translatiraj() i Crtaj()


koji se pozivaju iz lana Pomakni() se i dalje pozivaju virtualno.
Trei sluaj u kojem se zaobilazi mehanizam virtualnih poziva je poziv iz
konstruktora ili destruktora. Razlog tome je to poziv virtualne funkcije u konstruktoru
moe rezultirati pristupom jo neinicijaliziranom dijelu objekta, dok poziv virtualne
funkcije u destruktoru moe rezultirati pristupom ve unitenom dijelu objekta.

380

11.11.3. iste virtualne funkcije

Funkcijski lanovi Crtaj(), Translatiraj() i Rotiraj() u klasi GrafObjekt


nemaju neko stvarno znaenje. Njihova je jedina zadaa definiranje javnog suelja koje
e biti detaljno specificirano tek u izvedenim klasama. Takoer, nema smisla stvarati
objekt klase GrafObjekt ta klasa zapravo slui kao temelj za nasljeivanje. Takve
klase se u C++ terminologiji zovu apstraktne klase (engl. abstract classes), dok se
funkcijski lanovi koji slue samo za definiciju javnog suelja nazivaju isti virtualni
funkcijski lanovi (engl. pure virtual function members).
Virtualni funkcijski lan se proglaava istim virtualnim tako da se iza
njegove deklaracije stavi oznaka =0. Klasa je apstraktna ako sadri barem
jedan isti virtualni funkcijski lan.
Klasa GrafObjekt redeklarirana kao apstraktna klasa izgleda ovako:
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);
};

Ovakvom

deklaracijom se naznaava da lanovi Crtaj(), Brisi() i


Translatiraj() nemaju definiciju na nivou klase GrafObjekt te e se njihova
definicija dati tek u izvedenim klasama. Time je klasa GrafObjekt pretvorena u
apstraktnu klasu. Vie nije mogue deklarirati objekt te klase svaki takav pokuaj
rezultirat e pogrekom prilikom prevoenja.
Nije mogue deklarirati objekte apstraktnih klasa. Mogue je, naprotiv,
deklarirati pokazivae i reference na te klase.
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.
11.11.4. Virtualni destruktori

Poput svih drugih funkcijskih lanova, i destruktor moe biti virtualan. Razmotrimo
razloge zbog kojih bi nam to moglo biti od koristi.

381

Destruktor se poziva prilikom brisanja objekta iz memorije raunala operatorom


delete. U sljedeem primjeru stvorit emo polje pokazivaa na tri razliita grafika

objekta:
int const MAX_ELT = 3;
GrafObjekt *nizObj[MAX_ELT];
nizObj[0] = new Linija;
nizObj[1] = new Poligon;
nizObj[2] = new ElipsinIsj;

Polje sadri pokazivae na objekte klase GrafObjekt. Za pohranjivanje pojedinih


objekata u polje koriste se principi polimorfizma: pokazivai na pojedine objekte se
konvertiraju u pokazivae na osnovne javne klase. Mehanizam virtualnih funkcija
osigurava da poziv
nizObj[1]->Crtaj();

zaista i rezultira crtanjem poligona. Funkcijski lan Crtaj() je deklariran kao


virtualan, pa iako se u pozivu koristi pokaziva na osnovnu klasu, poziva se u stvari
funkcijski lan iz odgovarajue izvedene klase. Meutim, promotrimo to se dogaa
prilikom brisanja niza operatorom delete:
for (int i = 0; i <= MAX_ELT; i++)
delete nizObj[i];

Operator delete poziva destruktor za objekt na koji pokazuje pokaziva. No pokaziva


pokazuje na objekt klase GrafObjekt, a destruktor je funkcijski lan sa statikim
pozivom. Takvi se lanovi pozivaju iz klase ijeg je tipa pokaziva. To znai da bi u
ovom sluaju dolo do poziva destruktora za klasu GrafObjekt, umjesto destruktora za
odgovarajue objekte na koje pokazivai iz niza stvarno pokazuju.
Taj problem se moe rijeiti tako da se destruktori svih klasa u hijerarhiji uine
virtualnima. Na primjer:
class GrafObjekt {
// ...
public:
virtual ~GrafObjekt();
};

Iako destruktori nemaju zajedniko ime, oni mogu biti virtualni.


Sve klase koje nasljeuju klasu s virtualnim destruktorom takoer e imati
virtualne destruktore te nije to potrebno eksplicitno specificirati kljunom
rijei virtual.
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.

11.12. Virtualne osnovne klase


Postoje sluajevi kada se neka osnovna klasa moe proslijediti izvedenoj klasi vie no
jednom. Ilustrirat emo to na sljedeem problemu.
esti element u raznim programima koji barataju grafikom je tekst. U hijerarhiju
grafikih objekata dodat emo stoga novu klasu Tekst koja e opisivati tekst na bilo
kojem dijelu ekrana u bilo kojem tipu pisma.
class Tekst : public GrafObjekt {
private:
char *pokTekst;

383

public:
virtual
virtual
virtual
virtual
virtual
};

void PostaviTekst(char *niz);


void Crtaj();
void Brisi();
void Translatiraj(int px, int py);
~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:
class UokvireniTekst : public Pravokutnik, public Tekst {
// ...
};

Ovo rjeenje je vrlo elegantno, no pogledajmo to smo zapravo dobili. Klase


Pravokutnik i Tekst nasljeuju od klase GrafObjekt, to znai da sadravaju po
jedan podobjekt GrafObjekt. Rezultirajui objekt UokvireniTekst imat e dva
objekta klase GrafObjekt: jedan dobiven preko klase Pravokutnik i jedan dobiven
preko klase Tekst. Hijerarhija nasljeivanja prikazana je na slici 11.8. Na slici 11.9 se
vidi struktura samog objekta: postoje dva podobjekta klase GrafObjekt.
Ovakva struktura objekta nije poeljna. Dobiveni objekt ima niz nedostataka. Na
primjer, nije mogua ugraena konverzija u pokaziva na GrafObjekt. To je
razumljivo, jer nije jasno u koji se GrafObjekt treba konverzija obaviti. Nadalje, svi
lanovi klase GrafObjekt prisutni su dva puta te prilikom pristupa nije jasno kojem se
lanu pristupa:
UokvireniTekst ut;
ut.PostaviBoju(CRVENA);

// pogreka: nejasan pristup

384

GrafObjekt

GrafObjekt

GrafObjekt
Pravokutnik

Poligon
GrafObjekt

Pravokutnik

Tekst

Tekst

UokvireniTekst

UokvireniTekst

Slika 11.8. Viestruko nasljeivanje

Slika 11.9. Struktura objekta klase


UokvireniTekst

lan PostaviBoju() postoji u oba podobjekta GrafObjekt tako da u gornjem


primjeru nije jasno kojem se lanu zapravo pristupa. Da bi se tono odredilo kojem se
lanu pristupa, potrebno je koristiti operator za odreivanje podruja. No poziv
ut.GrafObjekt::PostaviBoju(CRVENA); // pogreka: nejasno

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.
11.12.1. Deklaracija virtualnih osnovnih klasa

Neka osnovna klasa moe se uiniti osnovnom virtualnom klasom tako da se prilikom
nasljeivanja u listi nasljeivanja ispred naziva klase umetne kljuna rije virtual.
Redoslijed kljunih rijei public, private, protected i virtual pritom nije bitan.
Klasa se ne definira virtualnom prilikom njene deklaracije, nego prilikom nasljeivanja.

385

GrafObjekt

Poligon
Tekst
Pravokutnik

UokvireniTekst

Slika 11.10. Primjena virtualne osnovne klase

To znai da se deklaracija klase GrafObjekt ne mijenja. tovie, ona moe jednom biti
upotrijebljena kao virtualna, a drugi put kao nevirtualna:
class Poligon : public virtual GrafObjekt {
// ...
};
class Pravokutnik : public Poligon {
// ...
};
class Tekst : public virtual GrafObjekt {
// ...
};
class UokvireniTekst : public Pravokutnik, public Tekst {
// ...
};

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.
11.12.2. Pristup lanovima virtualnih osnovnih klasa

lanovima virtualnih osnovnih klasa moe se pristupiti bez opasnosti od nejasnoa


prilikom pristupa, jer sada postoji samo jedan podobjekt unutar izvedenog objekta. To
se postie razlikom u alokaciji osnovnih virtualnih klasa. Dijelovi objekta izvedene

386

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:
class Poligon : private virtual GrafObjekt {
// ...
};
class Pravokutnik : public Poligon {
// ...
};
class Tekst : public virtual GrafObjekt {
// ...
};
class UokvireniTekst : public Pravokutnik, public Tekst {
// ...

Pravokutnik
GrafObjekt

Tekst
GrafObjekt

Pravokutnik
GrafObjekt

Tekst

UokvireniTekst

Slika 11.11. Struktura objekata s virtualnim osnovnim


klasama

387

};

Sada se funkcijski lan PostaviBoju() nalazi na dvije staze nasljeivanja: jednom


kao privatni lan naslijeen preko klase Pravokutnik, a jednom kao javni lan
naslijeen preko klase Tekst. Pravilo kae da javni put do nekog lana uvijek
prevladava, odnosno da e u klasi UokvireniTekst taj lan imati javni pristup. Stoga
e sljedei poziv biti ispravan:
UokvireniTekst ut;
ut.PostaviBoju(CRNA);

// OK

Posebno pravilo dominacije (engl. dominance) se primjenjuje u situacijama kada u


izvedenim klasama postoje lanovi koji imaju iste nazive kao i lanovi osnovnih klasa.
Tada se prilikom pristupa lanu esto moe postaviti pitanje kojem se lanu zapravo
pristupa. Na primjer, moemo u klasi Tekst definirati novi funkcijski lan
PostaviBoju() koji e obaviti zadatak postavljanje boje na nain specifian za tu
klasu. Deklaracije tada izgledaju ovako:
class Tekst : public virtual GrafObjekt {
public:
void PostaviBoju(int b);
// ...
};

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)

11.12.3. Inicijalizacija osnovnih virtualnih klasa

U obinim sluajevima nasljeivanja svaki konstruktor u svojoj inicijalizacijskoj listi


moe inicijalizirati samo neposredno prethodee klase. Na primjer, ako ne bismo
koristili virtualne osnovne klase, klasa Pravokutnik ne bi mogla inicijalizirati klasu
GrafObjekt, budui da se u stablu nasljeivanja izmeu njih nalazi klasa Poligon.
Virtualne osnovne klase predstavljaju izuzetak u ovom pravilu. Evo i razloga.

388

Klasa UokvireniTekst nasljeuje od klasa Tekst i od klasa Pravokutnik. Obje


klase inicijaliziraju svoj GrafObjekt podobjekt. No objekt klase UokvireniTekst
sadrava samo jedan GrafObjekt. Ako bi se ta klasa inicijalizirala iskljuivo u
neposredno izvedenim klasama, tada bi Pravokutnik i Tekst pokuali inicijalizirati
isti GrafObjekt dva puta, vrlo vjerojatno na dva razliita naina. Na primjer, uzmimo
da klasa GrafObjekt sadrava konstruktor kojemu se prosljeuje poetna boja objekta:
class GrafObjekt {
public:
GrafObjekt(int b);
// ...
};
class Poligon : virtual public GrafObjekt {
public:
Poligon() : GrafObjekt(BIJELA) {}
// ...
};
class Tekst : virtual public GrafObjekt {
public:
Tekst() : GrafObjekt(CRNA) {}
// ...
};
class Pravokutnik : public Poligon {
// ...
};
class UokvireniTekst : public Pravokutnik, public Tekst {
public:
UokvireniTekst() : GrafObjekt(ZELENA) {}
// ...
};

Klasa Poligon (pa tako i klasa Pravokutnik) inicijalizirat e klasu GrafObjekt na


bijelu boju, dok e klasa Tekst postaviti GrafObjekt na crno. Kako klasa
UokvireniTekst sadrava samo jedan GrafObjekt podobjekt, on moe biti samo
jednom inicijaliziran. Zbog toga je uvedeno pravilo koje kae da se sve virtualne
osnovne klase inicijaliziraju konstruktorom iji je poziv naveden u najdalje izvedenoj
(engl. most derived) klasi. To je u ovom sluaju klasa UokvireniTekst. Pozivi
konstruktorima za GrafObjekt u inicijalizacijskim listama klasa Poligon i Tekst se
tada ignoriraju, a konstruktori se ne izvode. Kao rezultat, GrafObjekt se postavlja na
zeleno.
Virtualne osnovne klase se uvijek inicijaliziraju u najdalje izvedenoj klasi.
Konstruktori u lancu nasljeivanja prije najdalje izvedene klase se ne
izvode.

389

Postoje promjene i u redoslijedu izvoenja konstruktora i destruktora pojedinih klasa.


Pravilo kae da se sve virtualne osnovne klase inicijaliziraju uvijek prije nevirtualnih
osnovnih klasa, neovisno o njihovoj poziciji u hijerarhijskom stablu ili u listi
nasljeivanja.
Najprije se inicijaliziraju neposredne virtualne klase. Ako postoji nekoliko
neposrednih osnovnih virtualnih klasa, one se inicijaliziraju ovisno o redoslijedu u
inicijalizacijskoj listi. Potom se inicijaliziraju sve virtualne klase u hijerarhijskom stablu
ispod trenutne klase. Na kraju se pozivaju konstruktori za preostale nevirtualne osnovne
klase.
Redoslijed pozivanja destruktora je time automatski definiran; on je uvijek obrnut
od redoslijeda pozivanja konstruktora.
Mogue su situacije u kojima se jedna klasa prosljeuje izvedenoj klasi i kao
virtualna i kao nevirtualna. U tom sluaju objekt izvedene klase sadravat e jedan
virtualni podobjekt i potreban broj nevirtualnih objekata. No takav pristup
programiranju je bolje izbjegavati. Ponovo se javlja problem nemogunosti razluivanja
imena lanova. Ako i sam programer jest u stanju pratiti stablo izvoenja i na ispravan
nain pozivati pojedine lanove, vrlo je velika vjerojatnost da to osoba koja e itati
programski kd nee moi uiniti.

390

12. Predloci funkcija i klasa


Nakon to je Platon definirao ovjeka kao
dvononu ivotinju bez perja, Diogen oerupa
pijetla te ga donese u Akademiju uz rijei: Ovo
je Platonov ovjek. Na osnovu toga definicija
bijae proirena: Sa irokim i ravnim noktima
Diogen iz Sinope, oko 350. p.n.e.

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.

12.1. Uporabna vrijednost predloaka


Ako ste ikad bili u situaciji da morate razviti iole kompleksniji programski paket, tada
ste vjerojatno imali prilike primijetiti da postoje algoritmi koji se mogu podjednako
primijeniti na vie razliitih tipova podataka. Jedan od tipinih primjera je sortiranje
podataka. Razvijeno je mnogo razliitih algoritama za sortiranje: bubble sort, quick sort,
heap sort i slini. Svaki od njih koristi odreeni princip sortiranja neovisan o tipu
podataka koji se sortira. Vano je samo da postoji mogunost usporeivanja podataka i
njihovog premjetanja u nizu. Postupak se ne razlikuje sortiramo li nizove znakova,
cijele brojeve ili neke korisnike objekte koji se mogu usporeivati.
Iako se sam algoritam u sutini ne razlikuje za razliite tipove podataka koji se
sortiraju, programer do sada nije bio u mogunosti napisati jednu funkciju koja e
definirati opi algoritam sortiranja primjenjiv neovisno o tipu koji se sortira. Problem
lei u tomu to je C++ jezik, kao i mnogi drugi srodni jezici (Pascal, Modula 2, Ada)
strogo tipiziran. To znai da se prilikom prevoenja mora tono navesti tip podataka s
kojima se radi. Dakle, ako elimo napisati funkciju bubble_sort() koja e sortirati
niz podataka koji joj se prosljeuje kao parametar, potrebno je u zaglavlju funkcije
navesti tip niza koji joj se prosljeuje. Zbog toga je programer esto prisiljen kopirati
kd funkcije i samo promijeniti deklaracije. Ovakav pristup ima dva oita nedostatka:

391

izvorni kd programa buja preslikavanjem slinih funkcija, a ako se primijeti


pogreka u algoritmu, ispravak treba unijeti u sve preslikane verzije funkcija.
Jo jednostavniji ali i znatno opasniji primjer je odreivanje minimuma dvaju
elemenata. Htjeli bismo napisati funkciju koja e vratiti manji od dva elementa. Problem
je u tome to prilikom deklaracije parametara funkcije obavezno moramo navesti tipove
podataka koji se prosljeuju. Privlana, ali zato suptilno opasna alternativa definiciji
funkcije je koritenje makro funkcija (o njima e jo biti govora u poglavlju 14):
#define manji(a, b)

((a) < (b) ? (a) : (b))

Ovakvo rjeenje e savreno funkcionirati za jednostavne primjere kao


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

U gornjem primjeru varijabla j e nakon poziva makro naredbe manji() umjesto


oekivane vrijednosti 5 sadravati broj 6. To se vidi ako se runo zamijeni definicija
makro funkcije stvarnim naredbama, kako bi to uinio pretprocesor:
j = ((++i) < (10) ? (++i) : (10));

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.

12.2. Predloci funkcija


Problem odreivanja minimuma dvaju objekata se moe elegantno rijeiti predlokom
funkcije. Definirat e se funkcija iji e parametri biti openitog tipa. Prilikom
prevoenja prevoditelj e sam generirati definiciju funkcije na osnovu stvarnih tipova
parametara navedenih u pozivu funkcije.

392

12.2.1. Definicija predloka funkcije

Definicija predloka funkcije zapoinje se kljunom rijei template. Svaki predloak


ima listu formalnih parametara koja se navodi izmeu znakova < (manje od) i > (vee
od). Lista formalnih parametara ne moe biti prazna.
Svaki formalni parametar se sastoji od kljune rijei class iza koje se navodi
identifikator. Taj identifikator predstavlja neki potencijalni ugraeni ili korisniki tip
koji e se navesti prilikom poziva funkcije. Funkcija manji() se pomou predloaka
moe ovako napisati:
template <class NekiTip>
NekiTip manji(NekiTip a, NekiTip b) {
return a < b ? a : b;
}

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
int j = manji(++i, 10);

// sada je OK

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:
template <class T, class T>
void func(T a) {
// ...
}

// pogreka

U gornjem primjeru u definiciji predloka je parametar T upotrijebljen dva puta.


Meutim, isti identifikator se moe koristiti bez ogranienja u razliitim definicijama
predloaka, na primjer:
template <class Tip>
Tip manji(Tip a, Tip b);
template <class Tip>
Tip max(Tip a, Tip b);

393

Svaki formalni parametar mora biti oznaen kljunom rijei class. Sljedea definicija
je neispravna:
// pogreka: mora biti <class Elem, class Zbroji>
template <class Elem, Zbroji>
void Zbroji (Elem *niz, Zbroji zbr);

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:
template <class Elem>
Elem &manji_u_nizu(Elem *niz, int brElem) {
Elem *pokNajmanji;
// lokalna varijabla
pokNajmanji = niz;
for (int i = 1; i < brElem; i++)
if (niz[i] < *pokNajmanji) pokNajmanji = &niz[i];
return *pokNajmanji;
}

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.
Dozvoljeno je koristiti modifikatore * i & u kombinaciji s identifikatorom
Elem kako bi se oznaili pokazivai i reference na tip.
Prilikom poziva funkcije, prevoditelj e identifikator Elem zamijeniti stvarnim tipom
navedenim u pozivu te e tako odrediti stvarni tip parametara funkcije i lokalne

394

varijable. Taj postupak zamjene se naziva instanciranje predloka funkcije (engl.


function template instantiation).
Identifikator formalnog parametra nalazi se u podruju imena koje se protee do
kraja definicije funkcije. Isto ime iz globalnog podruja imena je skriveno te mu se
moe pristupiti samo pomou operatora za odreivanje podruja.
Predloak funkcije moe biti deklariran kao umetnut, eksterni ili statiki, ba kao i
kod obinih funkcija. Kljune rijei inline, extern i static se tada stavljaju iza liste
parametara predloka:
template <class Tip>
inline Tip manji(Tip a, Tip b) {
return a < b ? a : b;
}
template <class Tip>
static void sortiraj(Tip *niz, int duljina);

Deklaracija predloka funkcije moe biti odvojena od definicije. U gornjem primjeru je


funkcija manji() istovremeno deklarirana i definirana, dok je funkcija sortiraj()
samo deklarirana, a njena definicija se u tom sluaju mora dati kasnije.
12.2.2. Parametri predloka funkcije

Poseban skup pravila definira pravila kojima se podvrgavaju parametri predloka. Ta


pravila su se znaajno promijenila prilikom razvoja i napretka C++ jezika, pa je
prilikom koritenja predloaka potrebno o tome voditi dosta rauna. Mnogi prevoditelji
jo ne podravaju novosti u C++ standardu koje je komitet za standardizaciju izglasao
1994. godine, pa je potrebno informirati se o tome kako va prevoditelj podrava
predloke.
Vano pravilo za deklaraciju parametara predloaka po staroj verziji C++ standarda
jest bilo da svaki formalni parametar obavezno mora biti iskoriten barem jednom u listi
parametara funkcije. On se mogao pojaviti u njoj i vie puta, ali je morao biti iskoriten
barem jednom. Razlog tome ogranienju bio je u tome to je prevoditelj pomou tipova
argumenata funkcije navedenih u pozivu odreivao o kojem se predloku radi. Nije
postojao mehanizam kojim bi se parametri, koji nisu bili spomenuti u listi argumenata
funkcije, mogli dodatno specificirati.
Takoer, formalni parametar nije mogao biti iskoriten samo za specificiranje tipa
povratne vrijednosti, na primjer:
// pogreka u okviru starog C++ standarda: Rez se koristi samo
// za tip povratne vrijednosti
template <class Rez, class Elem>
Rez prosjek(Elem *niz, int brElem);
// pogreka u okviru starog C++ standarda: Rez se ne spominje
// u potpisu funkcije

395

template <class Rez, class Elem>


void prosjek(Elem *niz, int brElem);

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);

Znak < za specifikaciju parametara predloka se obavezno mora pisati


neposredno uz naziv funkcije. U suprotnom e se on interpretirati kao
operator usporedbe manje-od.
Parametar predloka funkcije koji nije iskoriten u listi argumenata moe se koristiti za,
primjerice, podeavanje preciznosti raunanja:
template <class Rez, class Elem, class Preciznost>
Rez prosjek(Elem *niz, int brElem);

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:
template <typename T>
void funkcija() {
T::A *pok1;
typename T::A *pok2;
}

// T oznaava tip
// ove dvije deklaracije imaju
// razliito znaenje

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

ona se interpretira tako da se unutar tipa T pristupi identifikatoru A, no dobiveni rezultat


se ne tretira kao tip koji se moe nai u deklaraciji, nego kao: uzmi lan A iz tipa T te ga
pomnoi s pok1. Oito je to pogreka.
U drugom sluaju kljuna rije typename oznaava prevoditelju da T::A
predstavlja tip prilikom instantacije funkcije specificirat e se stvarni tip te e se
pristupiti tipu A koji se nalazi u navedenom tipu. pok2 se stoga deklarira kao pokaziva
na taj tip. Ako je odreeni tip potrebno esto spominjati unutar predloka, mogue je
deklarirati sinonim za tip pomou deklaracije typedef:
typedef typename T::A T_ov_A;

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:
template <class TipPolja, int brElem>
void ADCUcitajPolje(TipPolja *polje) {
TipPolja pomocnoPolje[brElem];
// ...
}

U gornjem primjeru je deklariran predloak funkcije koji je parametriziran tipom


podataka koji se uitava s pretvaraa, ali i s brojem elemenata koji se uitavaju. Taj broj
e se navesti prilikom instantacije predloka, a u tijelu funkcije se koristi ba kao da je
naveden u listi parametara funkcije. Razlika je u tome to e se taj parametar prilikom
instantacije funkcije zamijeniti navedenim parametrom, a nee se prenijeti funkciji
prilikom
poziva.
Tako
e
se,
primjerice,
generirati
funkcija
ADCUcitajPolje<Vektor, 10>() koja e uitavati polje vektora od deset elemenata.
Za uitavanje polja vektora od dvadeset lanova koristit emo funkciju
ADCUcitajPolje<Vektor, 20>(). Prilikom generiranja kda obiju funkcija
prevoditelj e zamijeniti pozive brElem unutar funkcije sa stvarnom konstantom. Time
smo dobili mogunost deklaracije lokalnog polja pomocnoPolje s promjenjivim
brojem elemenata: kako je vrijednost brElem uvijek poznata prilikom prevoenja,
dozvoljeno je koristiti parametar predloka za deklaraciju polja. No takvu pogodnost
plaamo time to za svaku duljinu za koju pozovemo funkciju dobijemo posebnu
verziju generirane funkcije. U sluaju intenzivnijeg koritenja predloka to moe dovesti
do prekomjernog bujanja kda (engl. code bloat). Kako u prvobitnoj verziji C++
standarda nije bilo mogue navesti eksplicitno parametre predloka prilikom
instantacije, mogunost konstantnih parametara takoer nije postojala.
Slino podrazumijevanim parametrima funkcija, nova verzija C++ standarda
omoguava koritenje podrazumijevanih parametara predloaka. Ako, na primjer, esto

397

koristimo predloak kojemu poseban tip specificira preciznost rauna, i taj tip u veini
sluajeva iznosi double, predloak moemo definirati ovako:
template <class T, class Preciznost = double>
void Inverz(T[100][100]);

Time emo si utedjeti na tipkanju prilikom poziva predloka: umjesto navoenja


Inverz<double>() dovoljno je navesti Inverz<>().
Prilikom koritenja podrazumijevanih parametara predloka ako je lista
parametara predloka prazna potrebno je iza naziva funkcije navesti prazan
par znakova <>.

12.2.3. Instanciranje predloka funkcije

Definicija predloka daje prevoditelju samo opi algoritam koji se koristi za rjeenje
nekog problema. No prevoditelj ne moe stvoriti kd same funkcije prije nego to
stvarno dozna na koji je nain potrebno zamijeniti formalne parametre stvarnima. Zbog
toga e prevoditelj prilikom prevoenja kda preskoiti definiciju predloka: ona moe
sadravati i grube pogreke, ali je sva prilika da one na tom mjestu uope nee biti
prepoznate.
Podaci o aktualnim tipovima postaju dostupni tek kada se funkcija pozove. Tada se
obavlja postupak instantacije. Prevoditelj provodi zamjenu tipova, prevodi funkciju i
generira njen izvedbeni kd. Na primjer:
template <class Tip>
Tip manji(Tip a, Tip b) {
return a < b ? a : b;
}
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

int manji(int a, int b) {


return a < b ? a : b;
}
char manji(char a, char b) {
return a < b ? a : b;
}

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:
template <class lokalniPodaci>
int Usporedi(char *niz1, char *niz2);

U gornjem primjeru deklarirali smo predloak funkcije koja e obavljati usporedbu


dvaju znakovnih nizova, s time da e uvaavati set znakova definiran u abecedi
korisnika programa. U tom sluaju mogue je definirati klasu za svaki pojedini jezik
koja e definirati nain usporedbe (primjerice, posjedovat e odreenu statiku
funkciju koja e obavljati usporedbu). Ta klasa se navodi prilikom poziva predloka. Na
primjer:
char *s1, *s2;
int usp1 = Usporedi<Hrvatski>(s1, s2);
int usp2 = Usporedi<English>(s1, s2);

Ako se argument predloka ne navede eksplicitno, prevoditelj e ga pokuati razluiti


pomou liste argumenata navedenih u pozivu funkcije. Mogu je kombinirani poziv:
neki parametri se navedu eksplicitno, a preostali se zakljue iz liste. Na primjer:
template <class Preciznost, class Tip>
Preciznost Prosjek(Tip *polje, int brElem);

Predloak se moe pozvati tako da se eksplicitno navede samo vrijednost parametra


Preciznost, dok e se vrijednost parametra Tip zakljuiti iz poziva:
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:
template <class Tip, class Preciznost>
Preciznost Prosjek(Tip *polje, int brElem);
float polje[100];
double pros = prosjek<double>(polje, 100);

// pogreno

U gornjem primjeru prevoditelj e rezonirati ovako: prvi navedeni argument je double,


to e se dodijeliti parametru Tip, dok se drugi argument Preciznost ne moe
zakljuiti iz poziva (nije naveden u listi parametara funkcije) pa je poziv neispravan.
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.
Algoritam za pronalaenje parametara predloka koji nisu eksplicitno navedeni
usporeuje potpis predloka funkcije s pozivom funkcije. Da bi instanciranje uspjelo,
mora se moi postii slaganje stvarnih i formalnih parametara. Na primjer, funkcija
manji_u_nizu()za prvi argument ima pokaziva na tip, pa stoga poziv mora kao prvi
argument takoer imati pokaziva:
template <class Elem>
Elem &manji_u_nizu(Elem *niz, int brElem);
// pogreka: prvi argument mora biti pokaziva
int a, b;
b = manji_u_nizu(a, b);
// OK: formalni i stvarni tipovi se slau
int niz[] = {1, 4, 7, 2, 4, 1, 3};
int i = manji_u_nizu(niz, 7);

Takoer, neophodno je potivati i const modifikator. Na primjer, ope je pravilo da se


pokaziva na konstantu ne moe pridodijeliti pokazivau na promjenjivu vrijednost:
// pogreka: niz je pokaziva na const
const int niz[] = {1, 7, 3, 2};
int k = manji_u_nizu(niz, 4);

Postupak usporeivanja formalnih i stvarnih parametara prilikom poziva predloka


funkcije tee po sljedeem redoslijedu:
1. Svaki formalni parametar predloka funkcije se ispituje sadri li neki formalni tip
predloka.

400

2. Ako je pronaen formalni tip predloka, ustanovljava se tip odgovarajueg stvarnog


parametra navedenog u pozivu.
3. Tipovi formalnog i stvarnog parametra se tada uparuju tako da se uklone svi dodatni
modifikatori tipa.
Na primjer, ako bismo funkciju manji_u_nizu() pozvali kao u sljedeem primjeru,
formalni parametar Elem bi predstavljao tip int *:
int **i;
manji_u_nizu(i, 1);

Ako se formalni parametar predloka pojavljuje u listi parametara funkcije


vie puta, prilikom poziva funkcije na svakom njegovom mjestu u listi
parametara mora se navesti doslovno isti tip. Pritom se ne provode nikakve
konverzije tipa.
Na primjer:
// pogreka: prvi parametar je unsigned int, a drugi je int
unsigned int i = 5, j;
j = manji(i, -5);

U gornjem primjeru dobit emo pogreku prilikom prevoenja predloak funkcije


manji() specificira da e prvi i drugi parametri biti istog tipa, to ovdje nije sluaj
(prvi parametar je unsigned int, a drugi je int).
Niti na argumente funkcije iji tip nije definiran formalnim parametrom
predloka se ne primjenjuju pravila konverzije.
Na primjer:
// pogreka: drugi parametar ne odgovara po tipu
int niz[] = {3, 5, 6, 2, 4};
long dulj = 5;
int m = manji_u_nizu(niz, dulj);

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);

Nakon uparivanja formalnih i stvarnih argumenata prevoditelj e generirati verziju


funkcije za dane tipove te e se tada provjeriti sintaksa samog predloka. Prilikom

401

prevoenja samog predloka prevoditelj zapravo uope ne analizira napisani kd; on


moe biti i potpuno sintaktiki neispravan. Tek kada se prilikom instanciranja funkcije
doznaju tipovi parametara, prevoditelj ima sve podatke potrebne da instancira
predloak.
Zbog toga prevoenje predloka jednom moe uspjeno proi, dok e za neke druge
formalne tipove prevoditelj pronai niz pogreaka. To najee ovisi o svojstvima
samog tipa koji se navodi kao stvarni parametar. Na primjer, naa funkcija
manji_u_nizu() moe funkcionirati ispravno ako je za tip koji joj je proslijeen
definirana operacija usporedbe. Kako se ugraeni cjelobrojni tip moe usporeivati,
poziv
int niz[] = {5, 2, 7, 3};
int m = manji_u_nizu(niz, 4);

se moe prevesti bez problema. Prevoditelj ima sve to mu je potrebno da generira


ispravnu verziju funkcije. Naprotiv, ako pozovemo funkciju za tip za koji nema
definirane operacije usporedbe, dobit emo pogreku prilikom prevoenja. Na primjer:
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

12.2.4. Eksplicitna instantacija predloka

U prethodnom odsjeku objanjena je implicitna instantacija predloka prilikom


poziva funkcije prevoditelj sam zakljuuje da funkcija nije obina funkcija, ve se radi
o predloku te generira eljenu varijantu funkcije. To je vrlo praktino, jer se time posao
s programera u cijelosti prebacuje na prevoditelj.
No postoji nekoliko problema na koje e svaki ozbiljniji C++ programer
neminovno naii. Osnovno je to to da bi predloak bio uspjeno instanciran, prevoditelj
mora imati dostupan izvorni kd predloka. To moe biti dosta nezgodno ako elimo
napraviti biblioteku funkcija definiranih pomou predloka, te biblioteku elimo dalje
distribuirati zasigurno ne bismo htjeli pokazati konkurenciji izvorni kd. Osim toga,
ak i da predloke koristimo iskljuivo za vlastite potrebe, naii emo na probleme ako
isti predloak koristimo u vie datoteka izvornog kda. Naime, prevoditelj e u svaku
datoteku umetnuti po jednu verziju funkcije predloka. Prilikom povezivanja emo ili
dobiti pogreku o tome da je pojedina funkcija deklarirana viestruko ili e poveziva
biti prisiljen izbaciti viestruke definicije predloka i koristiti samo jednu.
Svi navedeni problemi se mogu izbjei tako da se u jednu datoteku smjeste svi
predloci koji se koriste u programu u druge datoteke neemo morati ukljuiti izvorni
kd predloka, ve emo funkciju samo deklarirati, a prilikom povezivanja e se ti
pozivi povezati s definicijom iz druge datoteke. Time predloci zaista postaju slini
obinim funkcijama mogue je ak napraviti biblioteku predloaka koju samo
prilikom povezivanja ukljuimo. Dodue, ta biblioteka mora sadravati instancirane
predloke za sve mogue tipove koje korisnik poeli koristiti u predloku. To ponekad
nije mogue (na primjer, za predloak funkcije koja ispisuje polje objekata na ekran
pomou operatora >> taj je operator mogue preopteretiti za bilo koji tip podataka i
tako iskoristiti predloak za bilo koji tip) te je u tom sluaju potrebno distribuirati
izvorni kd predloka. No za neke vrste predloaka to nije ogranienje.
Kako bi se omoguilo elegantno rjeenje gornjih problema, u posljednju varijantu
C++ jezika je ubaena mogunost eksplicitne instantacije predloka. Mnogi prevoditelji
to jo ne podravaju, ali vjerujemo da e ta mogunost biti uskoro dodana veini
prevoditelja jer je esto vrlo bitna za uspjenu primjenu.
Eksplicitna instantacija se provodi tako da se iza kljune rijei template navede
naziv predloka s listom parametara predloka i listom parametara funkcije:
template <class Preciznost, class Tip>
// deklaracija
Preciznost Prosjek(Tip *polje, int brElem);
// eksplicitne instantacije
template double Prosjek<double, float>(float *, int);
template long Prosjek<long, int>(int *, int);

U gornjem primjeru stvorit e se dvije funkcije instancirane s razliitim setom


parametara: <double, float> i <long, int>. Prilikom eksplicitne instantacije
potrebno je navesti puni potpis funkcije time je omoguena instantacija samo eljene
funkcije od ukupnog skupa preoptereenih funkcija predloaka.

403

Kao i prilikom implicitne instantacije, pojedini tip se moe izostaviti ako se njegova
vrijednost moe odrediti pomou liste argumenata:
// argument Tip e se postaviti na float:
template Prosjek<double>(float *, int);

12.2.5. Preoptereivanje predloaka funkcija

Predloak funkcije moe biti preoptereen proizvoljan broj puta pod uvjetom da se
potpisi funkcija razlikuju po tipu i/ili broju argumenata. Na primjer, moemo definirati
funkciju zbroji() koja zbraja objekte:
// zbrajanje dvaju objekata
template <class Tip>
Tip zbroji(Tip a, Tip b);
// zbrajanje triju objekata
template <class Tip>
Tip zbroji(Tip a, Tip b, Tip c);
// zbrajanje dvaju nizova
template <class Tip>
void zbroji(Tip *niza, Tip *nizb, Tip *rez, int brElem);

Nije mogue preopteretiti funkcije tako da se razlikuju samo u povratnom tipu:


// pogreka: funkcija se razlikuje samo u povratnom tipu
template <class Tip>
int zbroji(Tip a, Tip b);

Prilikom poziva preoptereenih predloaka funkcija potrebno je biti vrlo paljiv.


Naime, reeno je da ako se formalni argument predloka pojavljuje nekoliko puta u listi
parametara funkcije, onda se prilikom poziva on mora svaki put zamijeniti istim tipom.
To znai da sljedei poziv nee uspjeti:
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:
template <class Tip1, class Tip2>
Tip1 zbroji(Tip1 a, Tip2 b);

404

No ovakvo rjeenje dodatno komplicira problem: to je s pozivom kada su oba


argumenta istog tipa?
int i = 5, j = 7, k;
k = zbroji(i, j);
// koji se poziva: zbroji(Tip, Tip) ili
// zbroji(Tip1, Tip2) ?

Sada se ravnopravno mogu pozvati dvije varijante funkcije zbroji(), pa e


prevoditelj javiti pogreku prilikom prevoenja. Na prvi pogled se ini da bismo mogli
jednostavno izbaciti prvu definiciju s jednim tipom. To je donekle i tono: poziv kada su
oba argumenta istog tipa se uvijek moe obuhvatiti varijantom funkcije koja prima
razliite tipove za parametre. No problem je koji od tipova Tip1 i Tip2 treba staviti kao
rezultat?
Odgovor glasi da zapravo nema odgovora. Rezultat mora ponekad biti tipa Tip1, a
drugi put tipa Tip2, ovisno o redoslijedu navoenja stvarnih parametara prilikom
poziva funkcije. Zbog toga bi bilo poeljno dodati trei parametar predloku koji e
specificirati povratni tip. U najnovijoj verziji C++ jezika to se moe uiniti prilino
jednostavno, na sljedei nain:
template <class Rez, class Tip1, class Tip2>
Rez zbroji(Tip1 a, Tip2 b);
// OK u posljednjoj verziji

Argument Rez je potrebno navesti prilikom instantacije predloka:


float a;
double b;
double rez = zbroji<double>(a, b);

Ako posjedujete prevoditelj koji ne podrava eksplicitno navoenje parametara, ovakvo


rjeenje nije mogue. Tada programeri obino pribjegavaju lukavom triku: funkciji se
doda trei parametar koji nee prenositi nikakav konkretan sadraj, osim to e
saopiti prevoditelju povratni tip:
template <class Rez, class Tip1, class Tip2>
Rez zbroji(Tip1 a, Tip2 b, Rez);

Trei argument nema ime, ime se signalizira prevoditelju da se on u funkciji ne koristi


i da nije potrebno generirati upozorenje o suvinom parametru koji se ne koristi u
funkciji. Prilikom instantacije predloka potrebno je navesti neku vrijednost kako bi
prevoditelj doznao povratni tip:
float a;
double b, samoZaPovratniTip;
double rez = zbroji(a, b, samoZaPovratniTip);

405

12.2.6. Specijalizacije predloaka funkcija

Postoje situacije u kojima openiti algoritam predloka moe biti neodgovarajui ili
neefikasan za pojedine tipove podataka. Na primjer, naa funkcija manji() nee
ispravno usporediti dva znakovna niza, zato jer se operator < ne interpretira kao
usporedba nizova, nego kao usporedba pokazivaa. U takvim sluajevima programer
moe napisati specijalizaciju predloka funkcije (engl. function template specialization)
koja se koristi samo za taj tip podataka.
Specijalizacija se definira tako da se bez koritenja kljune rijei template
jednostavno napie eljena funkcija. Na primjeru funkcije manji() to izgleda ovako:
#include <string.h>
// predloak za openite tipove
template <class Tip>
Tip manji(Tip a, Tip b) {
return a < b ? a : b;
}
// specijalizacija za char sluaj
char *manji(char *a, char *b) {
return strcmp(a, b) < 0 ? a : b;
}

Za sve pozive funkcije manji() koristi se odgovarajua instantacija predloka, osim u


sluaju poziva kada se trai minimum niza znakova. Tada se poziva specijalizacija:
// 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:
char *manji<char *>(char *a, char *b);

Lista parametara predloka moe biti i prazna, ako se oni mogu zakljuiti iz liste
argumenta funkcije:
char *manji<>(char *a, char *b);

Kod predloaka s vie parametara mogue je napraviti i takozvanu djelominu


specijalizaciju (engl. partial specialization): specijalizira se samo dio parametara, dok
se ostali ostave opima:
template <class T1, class T2>
void f();
// originalni predloak

406

template <class T>


f<T, char>();

// djelomina specijalizacija

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
char
kao
drugi
argument:
f<long, char>(),
kojih
navedemo
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.
Zadatak. Napiite specijalizaciju funkcije binarno_pretrazi() iz zadatka na stranici
401 tako da ona ispravno radi za znakovne nizove.
12.2.7. Primjer predloka funkcije za bubble sort

Na kraju odsjeka o predlocima funkcija, evo konkretnog primjera njihovog koritenja.


Dana je funkcija bubble_sort() koja je u stanju sortirati niz bilo kojih tipova
elemenata za koje postoji operacija usporeivanja (konkretnije operator <). Takoer,
zadani tip mora imati i definiran operator pridruivanja. Parametri su niz i duljina niza.
Funkcija bubble_sort() takoer poziva funkciju zamijeni() koja provodi zamjenu
dva elementa niza.
Evo kratkog objanjenja kako radi navedena metoda sortiranja. Prolazi se kroz niz
te se usporeuju susjedni elementi. Ako je neki element vei od sljedeeg, potrebno ih je
zamijeniti kako bi se dobio eljeni rastui poredak. Nakon prvog prolaska kroz niz
najvei element e se nai na kraju niza, ime je on dospio na svoje odredite. Zatim se
postupak ponavlja za preostale elemente niza, s time da oito nije potrebno ii do kraja
niza. Postupak sortiranja za niz od etiri broja je prikazan na slici 12.1.
Evo kda funkcija zamijeni() i bubble_sort():

407

prvi prolaz

drugi prolaz

trei prolaz

Slika 12.1. Prikaz bubble sort algoritma za niz od etiri broja

template <class Elem>


void zamijeni(Elem& a, Elem& b) {
Elem priv = a;
a = b;
b = priv;
}

template <class Elem>


void bubble_sort(Elem *niz, int brElem) {
int ok;
for (int i = brElem - 1; i > 0; i--) {
ok = 1;
for (int j = 0; j < i; j++)
if (niz[j + 1] < niz[j]) {
zamijeni(niz[j], niz[j + 1]);
ok = 0;
}
if (ok) break;
}
}

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>
template <class Elem>
void pisi_niz(Elem *niz, int brElem) {
cout << "{ ";
for (int i = 0; i < brElem; i++) {
cout << niz[i];

408

//zarez se ne tampa nakon zadnjeg elementa:


if (i < brElem - 1) cout << ", ";
}
cout << "}" << endl;
}

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;
}

12.3. Predloci klasa


Slino predlocima funkcija koji omoguavaju definiranje opih algoritama, koji se
zatim konkretiziraju stvarnim tipovima nad kojima djeluju, postoje i predloci klasa
(engl. class templates) za definiranje opih klasa koje se zatim konkretiziraju stvarnim
tipovima. Tipian primjer za takve klase su kontejnerske klase (engl. container classes).
To su klase koje manipuliraju skupom objekata neke druge klase te se esto koriste za
pohranjivanje nizova objekata. Na primjer, neki program moe sve svoje podatke drati
u obliku vezane liste. Lista se moe predoiti objektom, a klasa koja definira listu je
kontejnerska klasa jer ona obrauje skup podataka neke druge klase.
Vano je uoiti da pravila za odravanje liste ne ovise o stvarnim objektima koji se
smjetaju u listu. Postoje standardne operacije koje se mogu obavljati nad listom:
dodavanje elemenata na poetak, ubacivanje elemenata, brisanje elementa, pretraivanje
liste, unija dviju listi i slino. Te operacije se obavljaju na konceptualno jednak nain
bez obzira sadri li lista nizove znakova, cijele brojeve ili objekte korisnikih klasa.
Zbog toga je vrlo korisno definirati opi predloak klase koji definira ope algoritme za
odravanje liste. Kasnije, prilikom koritenja liste navodi se stvarni tip koji e se
pohraniti u listi. Prevoditelj e tada stvoriti kopiju klase koja e djelovati na tono
navedeni tip koji se sprema u listi.
Time se programer oslobaa dosadnog posla kopiranja i promjene zajednikog
kostura liste za svaki pojedini tip koji se uva u listi. Takoer, ako je mehanizam liste
potrebno proiriti novim svojstvima ili ispraviti pogreke u algoritmu, promjena e se
unijeti na jednom mjestu i automatski prenijeti na sve pozive liste. Velika je prednost i u
nainu imenovanja. Ta generika klasa se moe nazvati Lista i ona ne mijenja naziv

409

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.
12.3.1. Definicija predloka klase

Klasu Lista iz poglavlja 9 i realizirat emo pomou predloaka. Definirat emo


predloak koji e opisivati opu listu objekata proizvoljnog tipa. No prvo moramo
ustanoviti koje operacije elimo podrati:

void UgurajClan(element, izaKojeg) dodaje element iza lana na kojeg


pokazuje izaKojeg,
void GoniClan(element) izbacuje element na koji element pokazuje i
int prazna() vraa jedinicu ako je lista prazna.
Za realizaciju trebamo dvije klase:

klasa Lista e definirati opa svojstva liste, vodit e rauna o njenom poetnom i
zavrnom lanu te e definirati javno suelje liste i
klasa ElementListe koja e definirati objekt koji se smjeta u listu. Taj objekt e
sadravati pokazivae na prethodni i sljedei element te samu vrijednost objekta.
Definicija predloka klase se ne razlikuje znaajnije od definicije obinih klasa.
Deklaracija se zapoinje tako da se ispred kljune rijei class umetne kljuna rije
template iza koje se unutar znakova < i > (znakovi manje-od i vee-od) navode
argumenti predloka. Na primjer:
template <class Tip>
class Lista;

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:
template <class T1, class T2, class T3>
class NekaKlasa;

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:
template <class Tip, int maxElem>
class OgranicenaLista;

410

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 kompleksnih brojeva se oznaava s


Lista<Kompleksni>

Puni naziv liste kompleksnih brojeva ograniene na dvadeset elemenata je


OgranicenaLista<Kompleksni, 20>

Evo primjera definicije klase ElementListe:


template <class Tip>
class ElementListe {
private:
Tip vrij;
ElementListe *prethodni, *sljedeci;
public:
ElementListe *Prethodni() { return prethodni; }
ElementListe *Sljedeci() { return sljedeci; }
void PostaviPrethodni(ElementListe *pret)
{ prethodni = pret; }
void PostaviSljedeci(ElementListe *sljed)
{ sljedeci = sljed; }
ElementListe(const Tip &elem);
ElementListe() {}
Tip &DajVrijednost();
};

Unutar definicije predloka identifikator Tip se koristi za oznaavanje tipa koji e se


navesti prilikom poziva klase. Pomou tog identifikatora moemo raditi sve to
moemo raditi i sa svim drugim tipovima: stvarati objekte (kao na primjer objekt vrij)
i prosljeivati ih kroz parametre (kao u sluaju konstruktora). Unutar klase se
identifikator ElementListe koristi bez parametara, to je i logino: parametri su
navedeni u template deklaracije ispred poetka deklaracije klase. Naprotiv, izvan
deklaracije klase, pored identifikatora klase se uvijek moraju navesti parametri.
Unutar definicije klase naziv klase se moe navesti bez parametara.
Promotrimo kako to izgleda u naem primjeru prilikom definicije konstruktora i
funkcijskog lana DajVrijednost(). Konstruktor emo realizirati kao inline:

411

template <class Tip>


inline ElementListe<Tip>::ElementListe(const Tip &elem) :
vrij(elem), prethodni(NULL), sljedeci(NULL) {}
template <class Tip>
inline Tip &ElementListe<Tip>::DajVrijednost() {
return vrij;
}

Obratimo jo panju i na nain na koji je definirana klasa ElementListe. Naime,


prilikom izrade kontejnerske klase programer je esto suoen s problemom da li e
klasa pamtiti objekte koji se stvaraju i unitavaju u samoj klasi, ili e se pamtiti samo
pokazivai odnosno reference na objekte koji su stvoreni izvan klase. U naem sluaju
smo izabrali pristup kada lista stvara kopiju vanjskog objekta. Lokalni objekt koji uva
vrijednost elementa liste je deklariran s
Tip vrij;

ime se zapravo specificira da e svaki ElementListe sadravati kopiju vanjskog


objekta. Ako bismo htjeli u listi drati samo reference na objekte koji su stvoreni izvan
liste, tada bismo promijenili gornju deklaraciju u
Tip &vrij;

Time bi svaki ElementListe sadravao samo referencu na vanjski objekt. Okolni


program e biti odgovoran za njegovo stvaranje i unitavanje.
Nema jednoznanog odgovora na pitanje koji je pristup bolji i ispravniji. Izabrano
rjeenje e ovisiti o naim potrebama. Prvo rjeenje ima prednost u tome to je lista
potpuno izolirana od vanjskog utjecaja: osim ako ne primjenjujemo prljave trikove
neemo nehotice unititi sadraj liste. Promjena u vanjskom objektu nee utjecati na
promjenu objekta u listi. No zato e stvaranje svakog elementa liste biti popraeno
stvaranjem lokalne kopije vanjskog objekta koji se smjeta u listu. Takoer, kada se
unitava lista, unitavaju se i svi elementi koje ona sadrava. Drugi pristup je bri:
objekt se stvori jednom i zatim se samo pokaziva (odnosno referenca) na njega ubaci u
listu.
Takoer, u prvom sluaju jedan objekt se ne moe nai istodobno u dvije razliite
liste. Svaka lista e sadravati svoju kopiju, te promjena na objektu u jednoj listi nee
imati utjecaja na objekt u drugoj listi. U drugom sluaju direktno moemo pokaziva
(referencu) na isti objekt jednom ubaciti u jednu, a drugi put u drugu listu.
Prilikom graenja kontejnerskih klasa koje sadravaju pokazivae na objekte
postoji opasnost od viseih referenci (engl. dangling references). Naime, stvaranje i
unitavanje objekata je izvan nadlenosti kontejnera. Tako moemo u listu koja je
deklarirana globalno ubaciti pokaziva na lokalni objekt. Nakon to dotina funkcija
zavri, lokalni objekt e biti automatski uniten. Kontejner e tada sadravati visei
pokaziva: pokaziva je ostao u listi, a objekt je uniten. Taj pokaziva sada pokazuje na

412

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.
Zbog opasnosti od viseih referenci valja biti vrlo oprezan prilikom
koritenja kontejnera koji uvaju pokazivae na lanove.
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:
template <class Tip = char>
class NizZnakova;

Sada se niz znakova char moe deklarirati ovako:


NizZnakova<> niz;

// OK

Upotreba praznih znakova <> je obavezna: oni prevoditelju kazuju da je NizZnakova


predloak, bez obzira to mu je lista argumenata prazna. Deklaracija
NizZnakova niz;

// pogreno

bi prouzroila pogreku prilikom prevoenja.


12.3.2. Instanciranje predloaka klasa

Predloak klase se instancira tako da se iza naziva klase unutar znakova < i > navedu svi
stvarni parametri predloka. Na primjer:
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.
Puni naziv klase stvorene na osnovu predloka ukljuuje parametre koji se
navode unutar znakova < i >. Za stvaranje objekata klase potrebno je koristiti
njen puni naziv.
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:
extern ElementListe<int> vanjski;
ElementListe<Kompleksni> poljeZ[20];
const ElementListe<int> postojaniKositreniInt(0);
ElementListe<Kompleksni> *pok = poljeZ, &ref = poljeZ[0];

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

ako ne postoje definirani operatori konverzije ili pridruivanja). Dakle, uz gornje


deklaracije naredba
poljeZ[0] = vanjski;

// pogreka

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.
Pojedine instance neke klase definirane predlokom predstavljaju razliite
tipove te ih nije dozvoljeno mijeati.
Moe se dogoditi da se predloak treba instancirati unutar definicije drugog predloka.
Tada se parametar predloka koji se definira moe koristi kao stvarni parametar
predloka kojeg se instancira. Pretpostavimo da svakoj varijanti klase ElementListe
elimo pridruiti funkciju Ispisi() koja e ispisati sadraj elementa na ekran. Takva
funkcija e se morati definirati predlokom:
template <class Tip>
void Ispisi(ElementListe<Tip> &Elem) {
ElementListe<Tip> *pok = &Elem;
// ...
}

U gornjem primjeru parametar funkcije je zapravo instantacija klase ElementListe,


slino kao i deklaracija lokalne varijable pok. Pri tome se naziv ElementListe
parametrizira identifikatorom Tip.
Posebnu panju treba obratiti na instanciranje predloaka koji imaju izraze kao
parametre. Promotrimo to na primjeru klase OgranicenaLista koja moe sadravati
samo neki odreeni maksimalan broj lanova. Takva lista moe biti deklarirana
predlokom
template <class Tip, int maxElem>
class OgranicenaLista {
private:
Tip lista[maxElem];
int elem_u_listi;
public:
OgranicenaLista() : elem_u_listi(0) {}
// ...
};

Isti uinak moe se postii tako da se broj elemenata proslijedi konstruktoru


deklaracija bi onda izgledala ovako:

415

template <class Tip>


class OgrLista {
private:
Tip *lista;
int elem_u_listi;
int maxElem;
public:
OgrLista(int mel) : lista(new Tip[mel]),
maxElem(mel), elem_u_listi(0) {}
// ...
};

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
OgranicenaLista<int, 5> ol5;
OgranicenaLista<int, 6> ol6;

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

Zbog toga prevoditelj nee generirati lan za sortiranje ukoliko ga programer


eksplicitno ne pozove (a tada odgovornost za operator usporedbe pada na njega). Tako
je mogue koristiti Lista<Kompleksni> bez sortiranja.
Zadatak. Klasu Tablica iz poglavlja 6 o klasama prepravite pomou predloaka tako
ona ne sadrava samo cijele brojeve, nego i bilo koji drugi tip.
Zadatak. Klasi Tablica dodajte funkcijski lan Pretrazi(). Kao parametra taj lan
e uzimati referencu na tip koji se uva u tablici, a kao rezultat e vraati redni broj
elementa u tablici ili 1 ako parametar nije pronaen. Usporedba podataka e se
obaviti pomou operatora == definiranog u klasi koja opisuje tip koji se uva u tablici.
12.3.3. Eksplicitna instantacija predloaka klasa

U odsjeku 12.2.4 naveli smo razloge zbog kojih je esto potrebno eksplicitno
instancirati neki predloak funkcije Slino vrijedi i za predloke klasa: mogue je dati
prevoditelju zahtjev da instancira odreeni predloak za zadani skup parametara.
Cijela klasa se instancira tako da se iza kljunih rijei template class navede
naziv klase s parametrima:
template class Lista<Kompleksni>;
Ovime e se instancirati svi funkcijski lanovi klase Lista s
tipom Kompleksni kao parametrom. Osim instanciranja cijele klase,
mogue je instancirati samo eljeni funkcijski lan klase:
template void Lista<Vektor>::UgurajClan(Vektor, int);

12.3.4. Specijalizacije predloaka klasa

Poneki funkcijski lanovi predloka klase mogu biti neadekvatni za neke konkretne
tipove koji se mogu proslijediti predloku kao parametar. U tom sluaju mogue je
definirati specijalizaciju funkcijskog lana koja e precizno definirati nain na koji
dotini lan mora biti obraen.
Na primjer, ako bismo klasu ElementListe parametrizirali tipom char *,
konstruktor klase ne bi djelovao ispravno. Konstruktor izgleda ovako:
template <class Tip>
inline ElementListe<Tip>::ElementListe(const Tip &elem) :
vrij(elem), prethodni(NULL), sljedeci(NULL) {}

Za tip char * taj konstruktor e lokalni objekt vrij inicijalizirati pokazivaem na


znakovni niz, to nije ispravno. Ono to bismo htjeli jest alocirati zasebni memorijski
prostor za proslijeeni niz te kopirati proslijeeni niz u to mjesto. U takvom sluaju smo
prisiljeni napraviti specijalizaciju predloka klase.

417

Specijalizacija pojedinog funkcijskog lana predloka se definira tako da se iza


naziva lana umjesto formalnih navedu stvarni tipovi na koje se specijalizacija odnosi:
#include <string.h>
inline ElementListe<char *>::ElementListe(char * const & elem)
: vrij(new char[strlen(elem) + 1]), prethodni(NULL),
sljedeci(NULL) {
strcpy(vrij, elem);
}

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.
Ako dotina klasa uistinu predstavlja objekt koji nema slinosti s
predlokom, onda je ispravnije ne definirati ga kao specijalizaciju predloka
nego kao neku drugu klasu zasebnog imena.
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 *:
class ElementListe<char *> {
private:
char *vrij;
ElementListe *sljedeci, *prethodni;
public:
ElementListe(char *elem);
~ElementListe();
char *DajVrijednost() { return vrij; }

418

void Kopiraj(char *buff);


};
ElementListe<char *>::~ElementListe() {
delete [] vrij;
}

void ElementListe<char *>::Kopiraj(char *buff) {


strcpy(buff, vrij);
}
ElementListe<char *>::ElementListe(char *elem) :
vrij(new char[strlen(elem) + 1]),
sljedeci(NULL), prethodni(NULL) {
strcpy(vrij, elem);
}

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).
Specijalizacija predloka se moe navesti samo nakon navoenja opeg
predloka. Takoer, ako se specijalizirani primjerak klase instancira,
potrebno je definirati sve funkcijske lanove eksplicitno.
To znai da nije mogue u gornjem primjeru izostaviti definiciju funkcijskog lana
DajVrijednost() i oekivati da e prevoditelj primijeniti opu varijantu funkcije.

Specijalizacija mora biti deklarirana u cijelosti i to neovisno o opem predloku.


Slino predlocima funkcija, mogue je definirati i djelomine specijalizacije
predloka klase. Na primjer:
template <class T1, class T2>
class XX;

// opi predloak

template <class T>


class XX<T, char>;

// parcijalna specijalizacija

Zadatak. Napiite specijalizaciju klase Tablica iz zadatka na stranici Error!


Bookmark not defined. tako da omoguite stvaranje tablice znakovnih nizova. Za to je
potrebno definirati specijalizirani konstruktor kopije, destruktor, lanove za dodavanje i
pristup znakovnim nizovima te lan Pretrazi().

419

12.3.5. Predloci klasa sa statikim lanovima

Prilikom definiranja predloka klase mogue je navesti statike lanove. U tom sluaju
svaka instanca navedenog predloka e imati svoj zasebni skup statikih lanova. Svaki
od tih lanova se mora inicijalizirati zasebno.
Uzmimo na primjer da elimo brojati ukupan broj listi koje imamo u memoriji. To
se jednostavno moe uiniti tako da klasi Lista dodamo statiki lan. U konstruktoru
klase potrebno je poveati lan za jedan, a u destruktoru klase smanjiti za jedan. Statiki
lan se i prilikom koritenja predloaka klasa moe definirati na isti nain kao i kod
obinih klasa:
template <class T>
class Lista {
private:
ElementListe<T> *glava, *rep;
public:
static int brojLista;
Lista();
~Lista();
void Dodaj(T *elt);
void Brisi(T *elt);
ElementListe<T> *Pocetak() { return glava; }
ElementListe<T> *Kraj() { return rep; }
};

Konstruktor i destruktor klase e osim uobiajenih poslova inicijalizacije i


deinicijalizacije objekta obavljati posao auriranja brojaa brojLista:
template <class T>
Lista<T>::Lista() : glava(NULL), rep(NULL) {
brojLista++;
}
template <class T>
Lista<T>::~Lista() {
brojLista--;
}

Statiki lanovi se u zaglavlju klase samo deklariraju, no inicijalizirati se moraju izvan


klase. To se izvodi ovako:
template <class T>
int Lista<T>::brojLista = 0;

Smisao gornje naredbe je sljedei: Za svaku klasu napravljenu po predloku stvori


cjelobrojni statiki lan brojLista i inicijaliziraj ga na nulu. Mogue je takoer
definirati specijalizirane inicijalizacije statikih lanova. U sluaju gornje inicijalizacije

420

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;

Ako postoji, specijalizirana inicijalizacija se uvijek primjenjuje umjesto ope varijante.


Takoer, specijalizirana inicijalizacija nema izravne veze sa specijalizacijom predloka
sama klasa ne mora imati specijalizirane lanove, ali za neki tip moemo imati
specijaliziranu inicijalizaciju i obrnuto.
Specijalizirani predloak moe koristiti opu inicijalizaciju, a opi predloak
moe koristiti specijaliziranu inicijalizaciju.
Prilikom pristupa statikim lanovima klase definirane predlokom potrebno je navesti
puno ime klase. Naime, svaka varijanta klase Lista imat e svoj broja koji e brojati
samo objekte tog tipa pa je prilikom pristupa potrebno je naznaiti kojem se brojau
pristupa:
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
cout << Lista::brojLista << endl;

// pogreka

nije ispravan jer nije jasno kojem se brojau pristupa.


Zadatak. Klasu Tablica proirite statikim lanom koji e pamtiti ukupan broj
lanova u svim tablicama u programu. Uputa: za to je potrebno promijeniti i
konstruktor tablice i lanove za promjenu veliine.
12.3.6. Konstantni izrazi kao parametri predloaka

Formalni parametar predloka, osim tipa moe biti i konstantni izraz. Na primjer,
mogue je definirati klasu Spremnik koja definira memorijski meuspremnik za
uvanje podataka prilikom komunikacije s ureajima u raunalu, kao to su disk ili

421

komunikacijski prikljuak. Spremnik moe biti razliitih duljina, pa emo tu klasu


realizirati pomou predloka koji e kao parametar imati cjelobrojni izraz. Parametar
predloka e odreivati duljinu spremnika:
template <int duljina>
class Spremnik {
private:
char podrucje[duljina];
public:
void Puni(char *pok, int dulj);
void Citaj(char *pok, int dulji);
};

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;

Gornje instantacije su ispravne. Generiraju se dvije klase, Spremnik<20> i


Spremnik<30>. Prve tri deklaracije rezultiraju instantacijom samo jedne klase, jer
izrazi u parametru imaju istu vrijednost. Posljednja deklaracija stvara zasebnu klasu, jer
izraz u parametru ima razliitu vrijednost od prva tri.
Vano je razumjeti da, iako su klase Spremnik<20> i Spremnik<30> sline sa
stanovita implementacije, one predstavljaju dvije zasebne i odvojene klase. Ne postoji
nikakvo njihovo meusobno srodstvo. Pokaziva na objekt jedne klase se ne moe
implicitno pretvoriti u pokaziva na objekt druge klase (eksplicitna pretvorba uz dodjelu
tipa je mogua uvijek i izmeu potpuno nekompatibilnih tipova, ali na vlastitu
odgovornost). Takoer, svaka od tih dviju klasa ima zaseban skup funkcijskih lanova.
Ako je klasa dugaka, mnoge instantacije za razliite vrijednosti parametra duljina e
generirati mnogo funkcijskih lanova te e izvrni kd biti dugaak.
Donja instantacija nije ispravna, jer izraz koji se stavlja kao parametar nije poznat
prilikom prevoenja, nego tek samo prilikom izvoenja:
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:
Spremnik<6.7 * 6.4> bez_konverzije;

// pogreka

422

U gornjem primjeru prevoditelj e prijaviti pogreku, jer nema konverzije iz realnog u


cjelobrojni tip. To se moe urediti pomou eksplicitne dodjele tipa:
Spremnik<(int)(6.7 * 6.4)> sada_radi;

12.3.7. Predloci i ugnijeeni tipovi

Razmotrimo detaljnije implementaciju klasa ElementListe i Lista. Objekti klase


ElementListe nemaju smisla osim u kontekstu klase Lista. Teko je zamisliti neki
nain na koji bi glavni program mogao imati koristi od klase ElementListe, pa bi bilo
vrlo pogodno onemoguiti stvaranje objekata klase iz glavnog programa.
No u proiavanju strukture programa moe se ii i dalje. Bilo bi vrlo zgodno
ukloniti identifikator ElementListe iz javnog podruja imena. Ima smisla deklarirati
klasu ElementListe ugnijeenu u klasu Lista.
U staroj varijanti C++ jezika koju jo i danas mnogi prevoditelji podravaju to nije
bilo mogue predloci su mogli biti definirani samo u globalnom podruju, a ne unutar
klase. Na primjer, sljedea deklaracija je neispravna:
template <class Tip1>
class Lista {
private:
template <class Tip2>
class ElementListe {
// ...
};
};

// pogreka u starom C++ jeziku

No u naem sluaju to i ne bi predstavljao neki problem: klasa ElementListe mora


biti parametrizirana istim tipom kao i klasa Lista. Dakle, svaka Lista ima
odgovarajui ElementListe. Dotini problem se moe rijeiti na sljedei nain:
template <class Tip>
class Lista {
private:
class ElementListe {
public:
Tip vrij;
ElementListe *prethodni, *sljedeci;
};

ElementListe *glava, *rep;


};

423

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:
template <int duljina>
class Spremnik {
private:
char podrucje[duljina];
public:
enum {kriticna = (int)(0.75 * duljina),
zadnji = duljina - 1, maksimum = 1000};
void Puni(char *pok, int dulj);
void Citaj(char *pok, int dulji);
};

Sada je mogue iz vanjskog programa pristupati pobrojenjima, ali tako da se navede


ukupna staza do identifikatora:
cout << Spremnik<20>::kriticna << endl;
cout << Spremnik<99>::zadnji << endl;

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:
cout << Spremnik::maksimum << endl;
// pogreka
cout << Spremnik<20>::maksimum << endl; // ispravno

12.3.8. Ugnijeeni predloci

Posljednja verzija C++ jezika podrava ugnjeivanje predloaka. Mogue je definirati


predloak neke klase unutar klase ili unutar predloka klase:
template <class T1>
class X {
public:
template <class T2>
class Y {
void Funkcija();
T1 *pok;

424

};
};

Funkcijski lan Funkcija() je sada parametriziran s dva tipa: T1 i T2. Njegova


definicija izvan klase izgleda ovako:
template <T1> template <T2>
void X<T1>::Y<T2>::Funkcija() {
// ...
}

Jedna instanca klase Y ovisi i o parametru klase X i o parametru klase Y:


X<char>::Y<Kompleksni> obj;

Neuki programer bi mogao pomisliti zato je Y parametriziran s T1; na kraju, Y


predstavlja tip neovisan od X, s tom razlikom to je njegovo podruje ugnijeeno u
podruje klase X. No valja se prisjetiti da su formalni argumenti u definiciji predloka
vidljivi kroz cijelu definiciju do njenog kraja. To znai da je unutar definicije klase Y
dozvoljeno koristiti tip T1 na kraju, to je i uinjeno za lan pok. Stoga e izgled klase
Y definitivno, osim samo o T2, ovisiti i o T1.
Osim deklaracija predloaka klase unutar drugih klasa i predloaka klasa, veliku
primjenu ima i deklaracija predloaka funkcijskih lanova unutar klasa i predloaka
klasa. To svojstvo je vrlo vano za definiranje razliitih funkcija konverzije izmeu
klasa.
Preradit emo klasu Kompleksni tako da bude parametrizirana tipom koji odreuje
preciznost kompleksnog broja:
template <class Tip>
class Kompleksni {
private:
Tip x, y;
public:
void PostaviXY(Tip a, Tip b) { x = a; y = b; }
Tip DajX() { return x; }
Tip DajY() { return y; }
};

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>);

No to se deava ako elimo funkciji kao parametar proslijediti Kompleksni<float>?


To ne moemo uiniti direktno, jer su Kompleksni<float> i Kompleksni<double>
dva razliita tipa izmeu kojih nema ugraenih pravila konverzije. Konverziju moramo

425

sami dodati, i to tako da klasi dodamo predloak konstruktora. On e biti parametriziran


nekim drugim tipom Tip2 te e konvertirati objekt Kompleksni<T2> u objekt
Kompleksni<Tip>:
template <class Tip>
class Kompleksni {
// ...
public:
template <class Tip2>
Kompleksni(const Kompleksni<Tip2>& ref) :
x(ref.x), y(ref.y) {}
// ...
};

Taj konstruktor e omogui konverziju Kompleksni<Tip2> u Kompleksni<Tip>


samo ako postoji konverzija tipa Tip2 u Tip. Ako ne postoji, konstruktor nee biti
generiran ako ga korisnik ne pozove, te nee doi do pogreke prilikom prevoenja.
Postoji vano ogranienje na funkcijske lanove definirane predlokom: oni ne
mogu biti virtualni. Na primjer:
class X {
public:
template <class T>
virtual void f(T&) = 0;
};
class Y : public X {
public:
template <class T>
virtual void f(T&);
};

// neispravno

// neispravno

Problem je u tome to bi za svaki poziv funkcije f() s razliitim parametrom bilo


potrebno dodati stavku u virtualnu tabelu klase X. Kako se ti pozivi s razliitim
parametrima mogu protezati kroz vie datoteka izvornog kda, jedino poveziva moe
sagledati kompletnu situaciju. To unosi dodatne komplikacije, jer bi tada poveziva
trebao ponovo proi kroz cijeli kd i promijeniti adresiranje virtualne tabele. Realizirati
takav mehanizam bi bilo vrlo sloeno: jednostavnije je zabraniti predloke virtualnih
funkcijskih lanova. Meutim, to i nije neko veliko ogranienje, jer se predloci
funkcijskih lanova najee koriste za definiranje konverzije.
Predloci funkcijskih lanova ne mogu biti virtualni.
Nije dozvoljeno stvarati predloke lokalnih klasa (klasa deklariranih unutar funkcija ili
funkcijskih lanova).

426

12.3.9. Predloci i prijatelji klasa

Postoje tri naina na koji jedan predloak klase moemo uiniti prijateljem nekog
drugog predloka.
1. Predloak klase se deklarira kao prijatelj neke druge funkcije ili klase (ne predloka
funkcije ili klase). Takvom deklaracijom funkcija ili klasa postaje prijateljem svih
moguih klasa koje se dobiju iz predloka:
class A {
void FClan();
};

class B {
public:
void FClan();
};
void Funkcija();
template <class Tip>
class C {
friend class A;
friend void Funkcija();
friend void B::FClan();
// ...
};

Ovaj sluaj je razmjerno jednostavan: klasa A, funkcija Funkcija() i funkcijski lan


FClan() klase B e biti prijatelji svih klasa koje e nastati iz predloka klase C.
2. Vezano prijateljstvo (engl. bound template friendship) se ostvaruje kada se svaka
klasa koja proizlazi iz predloka vee s tono jednom klasom nekog drugog
predloka:
template <class Tip>
class Lista {
// ...
};
template <class Tip>
class Red {
public:
void Dodaj(Tip *);
};
template <class Tip>
void Ispisi(Tip *) {
// ...
}

427

template <class Tip>


class ElementListe {
friend class Lista<Tip>;
friend void Red<Tip>::Dodaj(Tip *);
friend void Ispisi(Tip*);
// ...
};

U ovom sluaju se svakoj klasi ElementListe dodjeljuju po jedna klasa Lista,


funkcijski lan Dodaj() klase Red i funkcija Ispisi() parametrizirani istim tipom
kojim je parametrizirana i klasa ElementListe. To znai da e Lista<char> imati
pristup svim privatnim i zatienim lanovima klase ElementListe<char>, ali ne i
lanovima klase ElementListe<int>.
3. Nevezano prijateljstvo (engl. unbound template friendship) se ostvaruje kada se svaka
klasa koja proizlazi iz predloka vee sa svim klasama nekog drugog predloka:
template <class Tip>
class ElementListe {
template <class T>
friend class Lista<T>;
template <class T>
friend void Red<T>::Dodaj(T *);
template <class T>
friend void Ispisi(T *);
};

U ovom e sluaju svaka klasa Lista imati pristup privatnim i zatienim


lanovima svih klasa ElementListe.
Zadatak. Napiite funkciju Zbroji() koja e kao parametar imati tablicu definiranu
klasom Tablica. Ta funkcija mora biti prijatelj klase Tablica te e direktnim
pristupom implementaciji klase zbrajati pojedine lanove tablice. Za zbrajanje koristite
operator + definiran u klasi koja opisuje tip koji se uva u tablici.
12.3.10. Predloci i nasljeivanje

Predloci klasa mogu se koristiti kao osnovne klase te za izgradnju parametrizirane


hijerarhije klasa. Postoje tri osnovna naina nasljeivanja.
Prvi je nain kada predloak klase slui kao osnovna klasa za izvoenje konkretne
klase koja nije predloak. U tom sluaju osnovna klasa mora biti parametrizirana
konkretnim tipovima, te se zapravo nasljeuje jedna konkretna instanca nekog
predloka.
Na primjer, definirajmo klasu SkupRijeci koja opisuje objekt koji uva niz rijei,
moe ga pretraivati i sortirati. Takav objekt bi bio od koristi u programu koji odrava
rjenik. Klasu SkupRijeci moemo izvesti nasljeivanjem iz klase Lista<char *>

428

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:
class SkupRijeci : Lista<char *> {
// ...
};

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;
};
template <class Tip>
class Lista : public Kontejner {
// ...
};

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:
template <class Tip>
class Stog : public Lista<Tip> {
// ...
};

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

kljune rijei template. U gornjem primjeru je klasa Lista parametrizirana opim


tipom Tip ime se prevoditelju daje na znanje da se izvoenje obavlja iz predloka. No
mogli bismo definirati i predloak klase Stog koja se izvodi iz tono odreene varijante
osnovne klase Lista, primjerice klase Lista<char>. U tom sluaju e se opi
parametri osnovne klase zamijeniti stvarnim:
template <class Tip>
class Stog : Lista<char> {
// ...
};

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.

12.4. Mjesto instantacije


Prilikom koritenja predloaka, bilo funkcija bilo klasa, postoje popratne pojave koje u
prvi mah nisu oite, a mogu prouzroiti velike glavobolje programerima koji ih ne
razumiju. Problem je definirati mjesto instantacije (engl. point of instantiation). Odmah
napominjemo da su primjeri navedeni u ovom odsjeku dosta nastrani, no to je stoga jer
se radi o primjerima sa samo nekoliko linija kda. U velikim programima vrlo je
mogue doi u situacije opisane u ovim poglavljima nehotice: perverzija tada prelazi u
realnost.
Promotrimo sljedei primjer deklaracije i koritenja predloka funkcije OpciExp()
za raunanje eksponencijalne funkcije preko reda potencija. Za one koji su markali taj
sat analize, eksponencijalna funkcija se moe razviti u sljedei red:

ex =
i =0

xi
x1 x 2 x 3
= 1+
+
+
+
i!
1! 2! 3!

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

template <class T>


T OpciExp(T x, int brKoraka) {
T rez = 0;
for (int i = 0; i <= brKoraka; i++)
rez += NaPotenciju(x, i) / faktorijel(i);
return rez;
}

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):
// Ovaj kd slijedi iza prethodne deklaracije
long NaPotenciju(int, int);
long faktorijel(int);
int main() {
double e = OpciExp(1, 20);
cout << e << endl;
// mogli bismo se iznenaditi
return 0;
}

U gornjem primjeru smo pozvali funkciju OpciExp() za najobiniji int, no uz vezanje


identifikatora na mjestu instantacije neemo dobiti ispisan broj e. Evo i zato: na mjestu
instantacije definirane su cjelobrojne funkcije faktorijel() i NaPotenciju(). U
samoj funkciji dijeljenje e se svesti na cjelobrojno dijeljenje te emo imati sasvim
pogrean raun (greka u raunu nastaje i zbog tipa pomone varijable rez i tipa
povratne vrijednosti, ali to bi se moglo ispraviti tako da se uvede dodatni tip predloka
koji e se odreivati sporni tip). Oito jest da nema idealnog rjeenja: imat emo
problema i ako se odluimo vezivati identifikatore na mjestu deklaracije i na mjestu
instantacije.
Kako bi se izbjegle ovakve nedoumice, C++ standard uvodi lukavo rjeenje. On
kae da e se identifikatori koji ne ovise o parametru predloka vezati na mjestu
deklaracije, dok e se identifikatori koji ovise o tipu predloka vezati na mjestu
instantacije.
To znai da e se, u gornjem sluaju, poziv funkcije faktorijel() uvijek vezati
za faktorijel(double), jer je samo ta verzija funkcije definirana na mjestu
deklaracije predloka. Funkcija faktorijel(int) je deklarirana iza, pa se niti ne

431

uzima u obzir. Time se spreava nehotino skupljanje smea iz okolnog podruja


prilikom instantacije.
Naprotiv, identifikatori koji su vezani za parametar predloka veu se na mjestu
instantacije. To znai da ako elimo pozvati OpciExp() za kompleksni broj, tada klasu
Kompleksni, operatore za zbrajanje i dijeljenje te funkciju za potenciranje
kompleksnog broja NaPotenciju(Kompleksni, int) moemo deklarirati bilo gdje u
kdu, ali obavezno prije koritenja funkcije OpciExp().
Standard jezika specificira tono to znai biti vezan za parametar predloka.
Primjerice, funkcija ovisi o parametru predloka ako njen parametar ovisi o parametru
predloka. Tako funkcija NaPotenciju() ovisi o parametru jer joj je argument x
zapravo tipa T. Prilikom koritenja predloaka, u normalnim programima korisnik u
principu o tome ne mora voditi rauna, jer se jezik ponaa intuitivno. No ako elite
profurati neki svoj hakeraj, tada ete se time itekako pozabaviti.

12.5. Realizacija klase Lista predlokom


I na kraju, evo kompletne realizacije klase Lista pomou predloaka. Implementacija
je slina onoj iz poglavlja o nasljeivanju, s neznatnim razlikama. Prvo i osnovno,
pojedini lanovi liste se ne identificiraju preko pokazivaa, nego preko cijelog broja.
Prvi lan (onaj na kojeg pokazuje glava) ima indeks 1. Takoer, za izravan pristup
pojedinom lanu liste dodan je preoptereeni operator [].
template <class Tip>
class Lista {
private:
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; }
};
ElementListe *glava, *rep;
public:
Lista() : glava(NULL), rep(NULL) {}
ElementListe *AmoGlavu() { return glava; }

432

ElementListe *AmoRep() { return rep; }


void UgurajClan(Tip pok, int redBr);
void GoniClan(int koga);
Tip &operator [](int ind);
};
template <class Tip>
void Lista<Tip>::UgurajClan(Tip pok, int redBr) {
ElementListe *elt = new ElementListe(pok);
ElementListe *izaKojeg = NULL;
if (redBr) {
izaKojeg = glava;
redBr--;
while (redBr) {
izaKojeg = izaKojeg->Sljedeci();
redBr--;
}
}
// da li se dodaje na poetak?
if (izaKojeg != NULL) {
// ne dodaje se na poetak.
// da li se dodaje na kraj?
if (izaKojeg->Sljedeci() != NULL)
// ne dodaje se na kraj
izaKojeg->Sljedeci()->StaviPrethodni(elt);
else
// dodaje se na kraj
rep = elt;
elt->StaviSljedeci(izaKojeg->Sljedeci());
izaKojeg->StaviSljedeci(elt);
}
else {
// dodaje se na poetak
elt->StaviSljedeci(glava);
if (glava != NULL)
// da li ve ima lanova u listi?
glava->StaviPrethodni(elt);
glava = elt;
}
elt->StaviPrethodni(izaKojeg);
}
template <class Tip>
void Lista<Tip>::GoniClan(int koga) {
ElementListe *pok = glava;
koga--;
while (koga) {
pok = pok->Sljedeci();
koga--;
}
if (pok->Sljedeci() != NULL)

433

pok->Sljedeci()->StaviPrethodni(pok->Prethodni());
else
rep = pok->Prethodni();
if (pok->Prethodni() != NULL)
pok->Prethodni()->StaviSljedeci(pok->Sljedeci());
else
glava = pok->Sljedeci();
delete pok;
}
template <class Tip>
Tip &Lista<Tip>::operator [](int ind) {
ElementListe *pok = glava;
ind--;
while (ind) {
pok = pok->Sljedeci();
ind--;
}
return pok->DajVrijednost();
}

434

13. Imenici
Slava i nitavilo imena.
Lord Byron (1788-1824)

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.

13.1. Problem podruja imena


Mnoga informatika poduzea u svijetu su se specijalizirala ne za izradu gotovih
programskih rjeenja, ve za pisanje biblioteka koje rjeavaju samo specifian segment
nekog problema. Te biblioteke zatim drugi programeri ugrauju u svoje programe. Kako
jedan program moe koristiti nekoliko razliitih biblioteka razliitih proizvoaa,
sasvim su mogue situacije gdje oba proizvoaa u svojim bibliotekama koristite iste
identifikatore za svoje funkcije i klase.
To je sluaj sa esto koritenim simbolima: na primjer, mnoge biblioteke koje
obavljaju poslove vezane s diskovnim sustavom raunala vrlo e vjerojatno deklarirati
funkciju Read() za itanje podataka. Obje biblioteke e imati svoje zasebne datoteke
zaglavlja, koje kad se ukljue u glavni program ne mogu funkcionirati ispravno: obje
deklariraju isti identifikator:
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.

13.2. Deklaracija imenika


Identifikatori klasa, funkcija i drugih elemenata C++ jezika se mogu smjestiti u imenike
te ih se na taj nain moe ukloniti iz globalnog podruja imena. Deklaracija imenika se
obavlja kljunom rijei namespace iza koje se navodi naziv imenika. U vitiastim
zagradama se navode deklaracije i definicije:
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;
}

Iza deklaracije imenika se ne stavlja toka-zarez.


Unutar deklaracije imenika mogue je deklarirati, ali i definirati identifikatore. U
gornjem primjeru funkcija Citaj() je samo deklarirana, dok je klasa Datoteka i
definirana. Funkcija Citaj() se moe definirati izvan imenika tako da se navede puni
naziv pomou operatora za razluivanje imena:
int RadiSDiskom::Citaj(char *buf, int dulj) {
// definicija
}

Klasa Podaci je unutar imenika samo deklarirana unaprijed, pa bi njena definicija


izgledala ovako:

436

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 {
// ...
};
}

Ovakva deklaracija se naziva proirenjem imenika (engl. namespace extension


definition). Dovitljivi itatelj e na osnovu toga primijetiti da se definicija lanova
imenika, primjerice funkcije Pisi(), moe obaviti i u sklopu proirenja:
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.
Evo primjera u kojem je unutar imenika ugnijeen drugi imenik:
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

namespace RSD = RadiSDiskom;

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;
}

Ovakav imenik e sadravati elemente jedinstvene za datoteku u kojoj je dana ovakva


deklaracija. Varijabla i i klasa X se ponaaju kao da su globalni identifikatori, no oni
nisu vidljivi izvan datoteke u kojoj su deklarirani. Na taj nain imenik bez naziva
omoguava deklaraciju statikih identifikatora vidljivih iskljuivo unutar datoteke
gdje su deklarirani.
ANSI standard C++ jezika preporuuje koritenje imenika bez naziva
umjesto koritenja statike smjetajne klase.

13.3. Pristup elementima imenika


Pojedini element imenika se moe pozvati tako da se navede puno ime elementa
pomou operatora :: za razluivanje podruja. Ako je potrebno deklarirati objekt klase
Datoteka, to se moe uiniti na ovakav nain:
// imenik RadiSDiskom je deklariran u prethodnom odsjeku
RadiSDiskom::Datoteka dat;

Funkcija Citaj() e se pozvati na sljedei nain:


if (!RadiSDiskom::Citaj(pok, 800)) // ...

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;
i = 0;

//
//
//
::i = 0;// OK:

OK: pristupa se elementu imenika


pogreka: nije navedeno je li to
globalni i ili i iz imenika
globalni i

Iz imenika nije potrebno navoditi naziv, jer se on podrazumijeva:


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

13.3.1. Deklaracija using

Identifikator iz nekog imenika je mogue uvesti u neko podruje imena tako da se iza
kljune rijei using navede puni naziv identifikatora. Na primjer, ovako emo u
funkciji UcitajDatoteku() iskoristiti funkciju Citaj() iz imenika RadiSDiskom:
void UcitajDatoteku() {
using RadiSDiskom::Citaj;
char buf[50];
if (Citaj(buf, 50)) // radi neto korisno, npr. okopaj vrt
}

U gornjem primjeru je pomou kljune rijei using u podruje imena funkcije


UcitajDatoteku() uveden identifikator Citaj() iz imenika RadiSDiskom. Sada se
Citaj() moe koristiti bez eksplicitnog navoenja imenika.
Deklaracija using se moe nai i unutar definicije imenika. U tom sluaju e
imenik u kojem je deklaracija navedena sadravati sinonim za lan iz nekog drugog
imenika. Takoer, mogue je uvesti i globalni lan, tako da se ispred operatora :: ne
stavi nikakvo ime.
int i = 0;
namespace X {
int j = 9;
using ::i;
}
namespace Y {
using X::i;
using X::j;
}

// sinonim za globalni i

// sinonim za globalni i
// sinonim za j iz imenika X

int main() {
using Y::j;
cout << Y::i << endl;
cout << j << endl;
return 0;
}

// ispisuje 0
// ispisuje 9

U deklaraciji using se ne navode tipovi lanova ili povratni tipovi i lista


parametara za funkcije. Ako neki naziv pripada preoptereenoj funkciji,
using uvodi sinonime za sve preoptereene funkcije.
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

U gornjem primjeru, na mjestu gdje je navedena using deklaracija imenik Ispis jo


ne sadri preoptereenu verziju Ispisi(Vektor &). Zbog toga poziv preoptereenog
lana nee uspjeti prevoditelj e javiti kako nije pronaen lan s odgovarajuim
parametrima. U dijelu kda koji slijedi nakon proirenja imenika gornji poziv bi bio
ispravan.
Deklaracija using se moe nai i na globalnom podruju te se tada stvara globalni
sinonim za lan nekog imenika:
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);
Ispisi("C++");

//
//
//
//
//

OK: using deklaracija je stvorila


globalni sinonim
pogreka: na mjestu using
deklaracije Ispisi(char *) jo nije
bio lan imenika

return 0;
}

Deklaracija using jest deklaracija: ona e na mjestu gdje je navedena deklarirati


lanove iz odreenog imenika, i to samo one lanove koji su u imeniku u tom trenutku.
Zbog toga, iako je nakon deklaracije using, a prije poziva imenik proiren funkcijom
Ispisi(char *), taj lan nee biti obuhvaen deklaracijom using, te mu se nee
moi pristupati bez eksplicitnog navoenja imenika. Ovakvo ponaanje je suprotno od
direktive using, koja je objanjena u sljedeem odjeljku.
Deklaracija using se moe nai i u deklaraciji klase. Pri tome ona moe
referencirati samo lan iz osnovne klase koji se tada uvodi u podruje izvedene klase.
Da bi se lan osnovne klase mogao ukljuiti u podruje izvedene klase, on mora biti
dostupan.
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:
using
using
using
};

public A {
B::k;
A::i;
A::j;

//
//
//
//

pogreka: B nije osnovna klasa od A


pogreka: i nije dostupan u klasi C
OK: j e u klasi C imati javni
pristup

442

Pravo pristupa sinonimu odreeno je mjestom na kojemu se nalazi using deklaracija.


To znai da e lan j u klasi C imati javni pristup, iako je u osnovnoj klasi bio
deklariran zatienim:
class C : public A {
public:
C() : A(5, 10) {}
using A::j;
};
int main() {
C obj;
cout << obj.j << endl;
return 0;
}

// ispisuje 10

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; }
};
class Izvedena : public Osnovna {
public:
void func(char *a) { cout << a << endl; }
};
int main() {
Izvedena obj;
obj.func(5);
return 0;
}

// pogreka

U gornjem primjeru func(char *) u izvedenoj klasi skriva func(int) iz osnovne


klase iako su potpisi lanova razliiti. Ako bismo prilikom nasljeivanja htjeli samo
preopteretiti neki funkcijski lan osnovne klase, do sada smo morali ponoviti deklaraciju
u izvedenoj klasi i eksplicitno pozvati lan osnovne klase. Pomou deklaracije using
to vie nije potrebno mogue je jednostavno ukljuiti sve dosadanje preoptereene
funkcije osnovne klase u podruje imena izvedene klase:
class Izvedena : public Osnovna {
public:
using Osnovna::func;
void func(char *a) { cout << a << endl; }
};

443

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 f(D* pd) {
pd->f(1);
pd->f('a');
pd->g(1);
pd->g('a');
}

//
//
//
//

poziva
poziva
poziva
poziva

D::f(int)
B::f(char)
B::g(int)
D::g(char)

13.3.2. Direktiva using

Vidjeli smo kako se pomou using deklaracije moe neki identifikator iz nekog
imenika uvesti u podruje imena. No ako neki imenik ima mnogo lanova koji se esto
ponavljaju, bit e vrlo zamorno navoditi posebice svako ime kada ga elimo koristiti.
Jednim potezom mogue je kompletan imenik uvesti u podruje imena, tako da se
nakon kljunih rijei using namespace navede naziv imenika:
void UcitajDatoteku() {
using namespace RadiSDiskom;
Datoteka dat;
// poziv klase iz imenika
char buf[50];
if (Citaj(buf, 50))
// poziv funkcije iz imenika
// ...
}

444

Direktiva using se moe navesti i u globalnom podruju, ime se svi identifikatori iz


imenika uvode u globalno podruje. Slino kao i kod using deklaracije, identifikator iz
imenika se ne moe koristiti prije nego to se uvede u imenik:
namespace Ispis {
void Ispisi(int);
void Ispisi(char);
void Ispisi(char *);
void Ispisi(Kompleksni&);
}
using namespace Ispis;
void f() {
Ispisi(5);
Ispisi(Vektor(5., 6.));

// OK
// 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;
cout << j << endl;
return 0;
}

// pogreka: koji i?
// OK: ispisuje 9

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;
cout << Y::i << endl;
cout << j << endl;
return 0;
}

// ispisuje 0
// ispisuje 2
// ispisuje 9

Zadatak. Klase koje definiraju grafike objekte upakirajte u jedan imenik


GrafickaBiblioteka. Napiite program koji crta grafike objekte na ekranu tako da
poziva navedeni imenik.

446

14. Rukovanje iznimkama


Ja nikada ne zaboravljam lica, ali u Vaem
sluaju rado u uiniti iznimku.
Groucho Marx (1895 - 1977)
(Leo Rosten: People I have Loved,
Known or Admired, 1970)

Iznimke (engl. exceptions) su situacije u kojima program ne moe nastaviti svojim


normalnim tokom, ve je potrebno prekinuti nit izvoenja te izvoenje prenijeti na neku
posebnu rutinu koja e obraditi novonastalu situaciju. Upravo to omoguava posebno
svojstvo C++ jezika koje se naziva rukovanje iznimkama.
Iznimka moe biti uzrokovana raznim dogaajima: to moe biti posljedica pogreke
u radu raunala (kao na primjer nedostatak memorije ili nepostojanje neke datoteke na
disku) ili jednostavno situacija koja je u programu uzrokovana samim izvoenjem
programa. Rutina na koju se izvoenje prenosi moe dobiti podatke o iznimci te na taj
nain poduzeti odgovarajue akcije, na primjer osloboditi memoriju ili ispisati poruku o
pogreki.

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]) {}
int JeLiPogreska() { return pogreska; }
int& operator[] (int indeks);
};
inline int& Niz::operator[] (int indeks) {
pogreska = 0;
if (0 <= indeks && indeks <= duljina)
return pokNiz[indeks];
// vraanje reference
else {
// Kako vratiti neku suvislu vrijednost?
pogreska = 1;
return pokNiz[0];
// Nije dobro rjeenje!
}
}

U gornjem primjeru koristi se preoptereeni operator [] za pristup lanovima niza. On


provjerava je li indeks unutar dozvoljenih granica. Ako jest, onda vraa referencu na
eljeni lan niza. Zbog toga to se vraa referenca, mogue je smjestiti operator [] na
lijevu stranu operatora pridruivanja te na taj nain simulirati uobiajenu sintaksu polja.
Razmotrimo to se deava u sluaju ako je indeks izvan opsega dozvoljenih
vrijednosti. Tada bi bilo potrebno prekinuti normalan tok izvoenja programa i
obavijestiti korisnika da je dolo do problema s pristupom lanovima polja. Potrebno je
pronai mehanizam kojim e se zavriti funkcija i signalizirati pogreka. Jedan od
moguih naina je naveden u primjeru. Svaki niz ima svoj podatkovni lan pogreska,
koji, ako je postavljen na vrijednost 1, signalizira pogreku prilikom pristupa, a ako je
postavljen na 0 ispravan pristup. No potrebno je vratiti i nekakvu vrijednost: funkcija
mora zavriti return naredbom. Zbog toga se vraa referenca na poetni objekt.
Ovakvo rjeenje nije dobro zbog nekoliko razloga. Promotrimo sljedei primjer koji
e nam prikazati velike nedostatke:
Niz a(10);
a[0] = 80;
a[130] = 0;

448

if (a.JeLiPogreska())
cout << "Pogreka u pristupu nizu a." << endl;

Veliki nedostatak je potreba eksplicitne provjere ispravnosti nakon svakog pristupa


objektu. To moe biti zaista zamorno u sluaju da se pie program koji esto pristupa
poljima. Takoer, iako je objekt a detektirao pogreku prilikom pristupa, vratio je
referencu na poetni lan (zato jer operator [] mora neto vratiti) pa je pridruivanje
ipak prepisalo poetni lan niza. Sada nije mogue jednostavno izvesti oporavak od
pogreke, odnosno nastaviti izvoenje programa imajui na umu da je dolo do
neispravnog pristupa. Poetni lan je nepovratno izgubljen u bespuima binarne
zbiljnosti!
Rjeenje za ovakve i sline probleme nudi nam podsistem C++ jezika koji se naziva
mehanizmom za rukovanje iznimkama (engl. exception handling). Osnovni pojam tog
sustava je iznimka (engl. exception) dogaaj u raunalu koji onemoguava normalan
nastavak izvoenja programa te zahtjeva posebnu obradu. Kada se detektira takva
situacija, program podie iznimku (engl. to raise an exception). Njegovo izvoenje se
prekida, te se iznimka prosljeuje rutini za oporavak od iznimke (engl. exception
recovery routine).

14.2. Blokovi pokuaja i hvatanja iznimaka


Cijeli C++ program se razbija u niz blokova koji sadravaju neke operacije koje bi
mogle biti problematine prilikom izvoenja programa. Ti blokovi se nazivaju
blokovima pokuaja (engl. try block). Oni se specificiraju tako da se navede kljuna
rije try te se iza nje u vitiastim zagradama navedu problematine instrukcije. Svaki
blok pokuaja mora biti popraen barem jednim blokom hvatanja, koji se navodi
pomou kljune rijei catch. Prema tome, opa sintaksa try-catch blokova ima
oblik:
try {
// ovo je blok pokuaja
}
catch (parametar) {
// ovo je blok hvatanja
}

Evo kako bismo problematinu operaciju pridruivanja smjestili u blok pokuaja:


Niz a(10);
try {
// ovdje dolazi problematian dio programa,
// na primjer, na pristup polju:
a[130] = 0;
}
// nije jo gotovo...

449

Za podizanje iznimke (ponekad se koristi i izraz bacanje iznimke engl. throwing an


exception) unutar bloka pokuaja koristi se kljuna rije throw. Iza nje potrebno je
navesti tono jedan parametar koji e opisivati tip iznimke koji je nastao. Naime, sustav
rukovanja pogrekama omoguava razlikovanje iznimaka koje se javljaju prilikom
izvoenja. Tako je mogue definirati razliite rutine za obradu pojedinih iznimaka. Kao
parametar kljunoj rijei throw moe se navesti objekt, pokaziva ili referenca na bilo
koji ugraeni ili korisniki definiran tip podataka koji opisuje iznimku. Na primjer:
char *pokZn = new char[10000];
if (!pokZn) throw 10000;

Nailaskom na kljunu rije throw prilikom izvoenja prekida se normalan tijek


izvoenja programa te se skae na rutinu za obradu pogreke. Parametar naveden
kljunoj rijei throw bit e proslijeen toj rutini kako bi se identificirao razlog zbog
kojeg je do iznimke dolo. U gornjem primjeru doi e do podizanja iznimke u sluaju
da dodjela memorije nije uspjeno obavljena. Kao parametar koji opisuje iznimku
navodi se duljina niza koja je izazvala problem. Ova iznimka je cjelobrojnog tipa, zato
jer je kao parametar prilikom podizanja iznimke naveden cjelobrojni tip. Mogue je,
primjerice, uiniti i sljedee:
if (!pokZn) throw "Ne mogu alocirati memoriju...";

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; }
char *Pogreska() { return pogreska; }
};

Definirali smo klasu NizoveIznimke za opis pogreke koje e bacati funkcijski


lanovi klase Niz u sluaju da doe do nepravilnog izvoenja bilo koje operacije. Ona
sadrava tekstualan opis pogreke, a zbog svoje strukture potreban je i konstruktor
kopije i destruktor (mehanizam za upravljanje pogrekama esto kopira i unitava
privremene objekte koji rukuju pogrekama). Sada je mogue operator pristupa napisati
na sljedei nain:

450

inline int& Niz::operator[] (int indeks) {


if (0 <= indeks && indeks <= duljina)
return pokNiz[indeks];
else
throw NizoveIznimke("Pogrean pristup nizu.");
}

Vie nije potrebno postavljanje podatkovnog lana pogreska umjesto toga, u


problematinim situacijama jednostavno se podigne izuzetak.
Paljiv itatelj moe se pitati kakva je razlika izmeu ovakvog bacanja iznimke i
jednostavnog bacanja iznimke:
// ...
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
}

14.3. Tijek obrade iznimaka


Razmotrimo malo detaljnije to se zapravo dogaa kada se prilikom izvoenja programa
naie na kljunu rije throw. Na primjer, promotrimo to e se dogoditi u sljedeem
sluaju (pri tome vrijede deklaracija i definicija klase Niz iz prethodnog odsjeka):
void Obracunaj(Niz &n) {
int u;
u = n[10];
// mogua pogreka ako niz n ima
// manje od deset lanova
u += n[3];
// i ovo je diskutabilno mjesto
}
void Poziv() {
Niz b(6);
try {
Niz a(11);
cout << "Obraunavam se
Obracunaj(a);
cout << "Obraunavam se
Obracunaj(b);
}
catch (NizoveIznimke& pogr)
cerr << pogr.Pogreska()
}
cout << "Kraj programa." <<
}

s poljem \'a\'" << endl;


s poljem \'b\'" << endl;

{
<< endl;
endl;

U gornjem primjeru funkcija Poziv() definira lokalna polja a i b te ih prosljeuje kao


parametre funkciji Obracunaj() koja koristi operator indeksiranja za pristup
proslijeenom nizu. U funkciji moe doi do iznimke prilikom pristupanja desetom
lanu polja ako polje ima manje od deset lanova. Tada e se podii iznimka unutar
operatora indeksiranja. Izvoenje operatora se prekida te se stvara privremeni objekt
klase NizoveIznimke koji sadrava podatke o iznimci. Iznimka se zatim prosljeuje u
funkciju Obracunaj() zato jer je ta funkcija neposredno ispred operatora indeksiranja
u lancu poziva. Operacija pridruivanja se takoer prekida. Unitavaju se svi lokalni i
privremeni objekti koji su definirani u bloku u kojem je dolo do iznimke. To zapravo

452

znai da se unitava lokalna varijabla u te funkcija zavrava. Iznimka se prosljeuje u


blok iz kojeg je problematina funkcija pozvana, a to je pokuajni blok u funkciji
Poziv(). Izvravanje pokuajnog bloka se prekida te se takoer unitavaju svi lokalni
objekti poziva se destruktor za objekt a te se oslobaa memorija koju je on zauzeo.
Kontrola se prenosi na prvi blok hvatanja iznimke iza bloka pokuaja koji prihvaa
baenu iznimku.
Nakon to zavri blok hvatanja iznimke, unitava se privremeni objekt koji je uvao
podatke o iznimci i kontrola se prenosi na prvu naredbu iza zadnjeg bloka hvatanja. U
naem primjeru to je poruka o zavretku programa. Zatim zavrava sama funkcija
Poziv() te se unitava njena lokalna varijabla b. Na slici 14.1 prikazan je tijek poziva
pojedinih funkcija i obrade iznimaka.
U sluaju da unutar pokuajnog bloka ne doe do ikakve iznimke, preskau se svi
blokovi za hvatanje iznimke te se kontrola prenosi na prvu naredbu nakon posljednjeg
catch bloka.
Ako pak unutar pokuajnog bloka doe do iznimke koja po tipu ne odgovara niti
jednom bloku hvatanja koji neposredno slijedi pokuajni blok, tada se izvoenje te
funkcije prekida, a iznimka se prenosi na prvi nadreeni pokuajni blok.
Na primjer, ako bismo promijenili funkciju Obracunaj() tako da ona u svom radu
koristi i klasu Skup koja baca iznimke tipa SkupoveIznimke, te ako u toku izvoenja
stvarno doe do takve iznimke, blok za hvatanje iznimaka NizoveIznimke nee
uhvatiti SkupoveIznimke. Izvoenje funkcije Poziv() e se prekinuti, unitit e se
lokalni objekt b, te e se iznimka proslijediti u funkciju koja je pozvala funkciju
Poziv().

void Poziv() {
Niz b(6);
try {
Niz a(11);
cout << "..." << endl;
Obracunaj(a);
cout << "..." << endl;
Obracunaj(b);
}
catch (NizovePogreske &pogr) {
cerr << pogr.Pogreska() << endl;
}
cout << "Kraj programa." << endl;
}

OK!

void Obracunaj(Niz &n) {


int u;
u = n[10];
u += n[3];
}

void Obracunaj(Niz &n) {


int u;
Pogreka! u = n[10];
u += n[3];
}

Slika 14.1. Tijek obrade iznimke

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

Ako se iznimka ne obradi sve do zavretka funkcije main(), poziva se funkcija


terminate() iz standardne biblioteke koja prekida program. To zapravo znai da se
cijeli C++ program moe zamisliti kao da je napisan na sljedei nain:
try {
main();
}
catch (...) {
terminate();
}

Pri tome oznaka ... u parametrima kljune rijei catch oznaava hvatanje svih
iznimaka (bit e kasnije detaljnije objanjeno).
Nakon bacanja iznimke provodi se postupak odmatanja stoga koji e unititi
sve lokalne objekte stvorene unutar pokuajnog bloka. Time sustav obrade
iznimaka osigurava integritet memorije.

14.4. Detaljnije o kljunoj rijei catch


Deklaracija parametara bloku hvatanja ima svojstva slina deklaraciji preoptereenih
funkcija. Odgovarajui blok hvatanja se pronalazi tako da se usporeuju tipovi objekta
koji je baen prilikom podizanja iznimke i parametra navedenog u bloku hvatanja. Ako
iza bloka pokuaja postoji nekoliko blokova hvatanja, traenje odgovarajueg bloka se
obavlja u redoslijedu njihovog navoenja. Ako niti jedan blok hvatanja ne odgovara
svojim parametrima, pretrauje se nadreeni blok. Takoer, slino funkcijama, ako se
ne namjerava pristupati vrijednosti parametra unutar bloka, dovoljno je samo navesti tip
parametra, a naziv se moe izostaviti.
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.
Odreivanje bloka hvatanja se provodi tako da se blokovi pretrauju u
redoslijedu navoenja nakon bloka pokuaja.

454

Potrebno je obratiti panju na redoslijed navoenja blokova hvatanja. Na primjer,


sljedei redoslijed nije ispravan, jer bez obzira koji se pokaziva baci, pomou
standardne konverzije pokazivaa mogue ga je svesti na void * pa se drugi blok nee
nikada niti razmatrati:
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.
Dinamika memorija se ne oslobaa automatski nakon bacanja iznimke. To
znai da e memorija zauzeta u bloku pokuaja ostati u memoriji ako ju ne
obriemo u bloku hvatanja.
Drugim rijeima, ovakva funkcija nee ispravno raditi:
void Func1() {
char *mem = new char[100];
try {
Problematicna();
}

456

catch (...) {
throw;
}
delete [] mem;

// prosljeivanje iznimke

Prilikom podizanja iznimke u funkciji Problematicna() izvoenje programa e se


prekinuti, a kontrola toka e se prenijeti u blok hvatanja. On e ponovo baciti iznimku i
unititi objekt mem, no nee osloboditi memoriju. Program koji esto poziva tu funkciju
e se nakon nekog vremena vrlo vjerojatno zaglaviti jer e ponestati slobodne
memorije.
Takoer, potrebno je biti oprezan i s alokacijama memorije pokazivaima koji su
deklarirani unutar bloka pokuaja. Ta situacija je jo loija, jer blok hvatanja uope
nema prilike osloboditi zauzetu memoriju:
try {
char *mem = new char[100];
Problematicna();
}
catch (...) {
throw;
// prosljeivanje iznimke
}

Kada funkcija Problematicna() podigne iznimku, automatski e se unititi lokalni


pokaziva mem, a memorija nee biti osloboena, niti e se moi osloboditi u bloku
hvatanja, jer nemamo vie pokaziva.
Ako se promotri funkcija Func1(), moe se initi suvinim ponavljati naredbu za
brisanje podataka i u bloku hvatanja i na kraju funkcije. No to je neophodno: operator
delete u bloku hvatanja pozivat e se samo u sluaju iznimke. Ako iznimka izostane,
funkcija regularno zavrava te je takoer potrebno osloboditi zauzetu memoriju.
Osnovni razlog gornjeg ponaanja jest u tome to se pokaziva alocira na stogu,
dok se memorija na koju on pokazuje alocira dinamiki. Bilo bi potrebno uvesti vezu
izmeu tih dvaju objekata. To se moe uiniti tako da se uvede nova klasa koja simulira
pokaziva na objekt, s time da ta klasa u svom destruktoru oslobodi zauzeti objekt:
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; }
};

Sada bi se funkcija Func1() mogla napisati ovako:

457

void Func1() {
PokZnak mem = new char[100];
try {
Problematicna();
}
catch (...) {
throw;
// prosljeivanje iznimke
}
}

Pokaziva PokZnak se u ovom sluaju stvara na stogu; njegov destruktor e automatski


osloboditi memoriju te e se memorija oistiti automatski nakon prosljeivanja
iznimke nadreenom bloku. Vie nije potrebno istiti memoriju ak niti nakon zavretka
funkcije Func1().
Na kraju, potrebno je obratiti panju na razliku izmeu objekta kao parametra i
reference na objekt kao parametra. Na primjer, promotrimo razliku izmeu prvog i
drugog bloka za hvatanje:
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.
Kada je kao parametar bloku hvatanja navedena referenca, blok hvatanja
moe promijeniti proslijeeni mu objekt. Ako on dalje proslijedi objekt,
promjena e biti proslijeena narednim blokovima hvatanja.
Iz gore navedenog je vidljivo da deklaracija objekta kao parametra moe rezultirati
sporijom obradom iznimke (zbog toga to je potrebno kopiranje i unitavanje objekta).
Zbog toga se esto kao parametar navodi referenca na objekt, iako se ne namjerava
mijenjati sama vrijednost objekta.

14.5. Navoenje liste moguih iznimaka


U sloenim programima esto nije mogue pratiti koja se iznimka baca u pojedinoj
funkciji. Zbog toga je potrebno uvesti nekakav mehanizam kojim e svaka funkcija
obavijestiti ostatak programa o iznimkama koje ona moe baciti. Jezik C++ posjeduje tu
mogunost u obliku liste moguih iznimaka.
Lista moguih iznimaka navodi se iza liste parametara funkcije tako da se navede
kljuna rije throw iza koje se u zagradama popiu svi tipovi iznimaka koje funkcija
moe baciti:
void Func1() throw(int, const char*, NizoveIznimke) {
// definicija funkcije
}
void Func2() throw() {

459

// funkcija koja ne baca nikakvu iznimku


}

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:
void Func() throw(int) {
try {
// ...
throw "Iznimka";
// ...
}
catch (char *) {
throw 0;
}
}

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
void Fn() throw();
void Fn() throw(int);
void Fn();

sve predstavljaju istu funkciju, a ne preoptereene varijante iste funkcije. Pokuaj


definiranja gornjih funkcija rezultirat e pogrekom prilikom prevoenja.
Zadatak. Poznato je da nije dozvoljeno vaditi logaritam iz negativnog broja.
Realizirajte funkciju sigurni_log() koja e u sluaju negativnog argumenta ili nule
baciti izuzetak definiran klasom
class LogPogreska {
public:
double argument;
};

460

Podatkovni lan argument e sadravati neispravni argument. Takoer, u deklaraciju


funkcije ubacite naznaku koja e odrediti mogue iznimke koje funkcija moe baciti.
Zadatak. Modificirajte klasu Lista iz poglavlja 10 o predlocima tako da zatitite listu
od moguih pogreaka. Tako u sluaju da alokacija objekta ElementListe ne uspije,
baca se objekt klase
class NemaMemorije {};

U sluaju da se nekom funkcijskom lanu proslijedi neispravan parametar, baca se


objekt klase
class LosParametar {};

14.6. Obrada pogreaka konstruktora


Prije uvoenja iznimaka u jezik C++, bilo je vrlo teko obraditi eventualne pogreke
koje bi se mogle dogoditi prilikom izvoenja konstruktora. Jasno je i zato: konstruktor
se ne poziva eksplicitno i nema povratnu vrijednost koju bismo mogli iskoristiti za
signalizaciju pogreke.
Na primjer, u dosadanjim primjerima koristili smo klasu ZnakovniNiz koja je
koristila dinamiku alokaciju memorije. U poseban komad memorije smjeten je niz
koji je proslijeen konstruktoru kao parametar. No to ako u sistemu nema dovoljno
memorije? Na konstruktor nije provodio nikakvu provjeru jednostavno je nakon
alokacije memorije pomou standardne funkcije strcpy() prepisao parametar u
stvoreno memorijsko podruje. Ako operator new nije uspio alocirati memoriju, on je
vratio nul-pokaziva, pa e zbog toga operacija prepisivanja sigurno zavriti sistemskom
pogrekom (general protection fault, segmentation fault, core dump i sl.). Bilo bi dobro
zatititi klasu od takvih neurotinih ispada i uvjeriti ju da nije lijepo sruiti program
samo zato jer nije bilo dovoljno memorije.
No postavlja se pitanje kako signalizirati pozivajuem programu pogreku? Jedna
od mogunosti je postavljanje podatkovnog lana objekta u odreeno stanje. Nakon
kreiranja objekta, bilo deklaracijom unutar bloka, bilo dinamikom alokacijom,
potrebno je ispisati vrijednost tog lana kako bi se ustanovilo je li objekt ispravno
stvoren.
No takvo rjeenje nije elegantno: time se korisnik prisiljava stalno provjeravati jesu
li novi objekti ispravno stvoreni. Ako programer ne provjeri ispravnost, postoji
mogunost da doe do pogreke prilikom koritenja objekta, a takve pogreke su
izuzetno sloene za pronalaenje. Osim toga, ako se i detektira pogreka prilikom
stvaranja objekta, potrebno je zapisati dodatne informacije koje e pomoi prilikom
unitavanja objekta. Naime, memorija za takav objekt s pogrekom e biti zauzeta, pa e
ju biti potrebno osloboditi destruktor mora imati informacije o stanju objekta kako bi
ga mogao ispravno unititi.

461

Dodatne probleme e prouzroiti eventualni privremeni objekti: ako primjerice


vraamo ZnakovniNiz iz neke funkcije i prilikom izvoenja konstruktora kopije doe
do pogreke jer nema dovoljno memorije, nemamo mogunosti ispisati ispravnost
objekta. U takvim sluajevima se jedino moemo nadati da do pogreke nee doi, a
kako praksa pokazuje, takva nada je obino uzaludna. Zlatno pravilo programiranja kae
da e memorije nestati upravo u trenutku kada svoj program pokazujete efu odjela. Bit
e vrlo teko tada objasniti, znojei se i mucajui, da program u biti radi, no ba sada
nema memorije, i ako doe za dva sata, sve ete srediti.
Iznimke pruaju pravi odgovor: u sluaju da prilikom izvoenja konstruktora doe
do pogreke, konstruktor e baciti iznimku koja e opisati pogreku. Objekt nee biti
stvoren, a izvoenje e se prenijeti na rutinu za hvatanje pogreaka koja e eventualno
omoguiti neki zaobilazni nain rjeavanja problema ili e jednostavno ispisati poruku o
pogreki i zavriti program. Dodajmo kd za provjeru ispravnosti klasi ZnakovniNiz:
class ZnakovniNiz {
friend ZnakovniNiz operator +(ZnakovniNiz &a, ZnakovniNiz &b);
private:
char *pokNiz;
public:
ZnakovniNiz(char *niz = "");
ZnakovniNiz(const ZnakovniNiz &ref);
~ZnakovniNiz();
operator const char *() { return pokNiz; }
ZnakovniNiz &operator =(const ZnakovniNiz &ref);
};
ZnakovniNiz::ZnakovniNiz(char *niz) :
pokNiz(new char[strlen(niz) + 1]) {
if (pokNiz == NULL) throw "Nema dovoljno memorije.";
strcpy(pokNiz,niz);
}
ZnakovniNiz::ZnakovniNiz(const ZnakovniNiz &ref) :
pokNiz(new char[strlen(ref.pokNiz) + 1]) {
if (pokNiz == NULL) throw "Nema dovoljno memorije.";
strcpy(pokNiz, ref.pokNiz);
}
ZnakovniNiz::~ZnakovniNiz() {
delete [] pokNiz;
}
ZnakovniNiz &ZnakovniNiz::operator =(const ZnakovniNiz &ref) {
if (this != &ref) {
delete [] pokNiz;
pokNiz = new char[strlen(ref.pokNiz) + 1];
strcpy(pokNiz, ref.pokNiz);
}

462

return *this;
}
ZnakovniNiz operator +(ZnakovniNiz &a, ZnakovniNiz &b) {
char *pok = new char[strlen(a.pokNiz) +
strlen(b.pokNiz) +1];
strcpy(pok, a.pokNiz);
strcat(pok, b.pokNiz);
ZnakovniNiz rez(pok);
delete [] pok;
return rez;
}

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

15. Identifikacija tipa tijekom izvoenja


Vi vidite stvari i pitate se: Zato?
No ja sanjam stvari koje nikad
nisu bile i pitam se: Zato ne?
George Bernard Shaw (1856-1950)

Kako bi se koncept objektnog programiranja dosljedno proveo, u C++ jezik je ubaeno


svojstvo koje je svojevrsna nadgradnja polimorfizma. Pomou polimorfizma je mogue
definirati operacije koje su ovisne o tipu i na taj nain ue povezati tip i njegovo
ponaanje, no samo pomou njega nije mogue doznati o kojem se tipu stvarno radi.
Identifikacija tipa u toku izvoenja omoguava upravo to: bez obzira na pokaziva ili
referencu kojom rukujemo s nekim objektom, u bilo kojem trenutku je mogue doznati
koje klase jest objekt na koji oni pokazuju. Osim prepoznavanja, uvedena je i
mogunost usporedbe tipova.
Takoer, kako bi se poveala sigurnost dobivenih programa, uveden je niz novih
operatora za dodjelu tipa koji eksplicitno izraavaju smisao dodjele. Najvaniji od njih
je operator dinamike dodjele, koji prije dodjele provjerava ima li dodjela uope smisla.

15.1. Statiki i dinamiki tipovi


Jedno od najznaajnijih svojstava koje C++ jezik ini monim i prikladnim za razna
podruja primjene je polimorfizam. Prisjetimo se ukratko o emu se radi. Osnovna ideja
je da tip objekta ne specificiramo strogo prilikom prevoenja, ve da prilikom
izvoenja objekt sam obavi svoje operacije na sebi svojstven nain. To je postignuto
nasljeivanjem i koritenjem virtualnih funkcija. Na primjer:
class Linija {
// ...
public:
virtual void Crtaj();
// ...
};
class LinijaSaStrelicama : public Linija {
// ...
public:
virtual void Crtaj();

464

// ...
};

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:
Linija *ln = new Linija;
ln->Crtaj();
// pozvat e Linija::Crtaj i iscrtat e
// obinu liniju na ekranu
delete ln;
ln = new LinijaSaStrelicama;
ln->Crtaj();
// pozvat e LinijaSaStrelicama::Crtaj
// i iscrtat e liniju sa strelicama
delete ln;

U drugom sluaju stvara se objekt LinijaSaStrelicama, ali se njegova adresa


pridodijeljuje pokazivau na tip Linija. Ako bi se poziv
ln->Crtaj();

obavio po statikom tipu pokazivaa ln, odnosno ako bi se ilo po logici ln je


pokaziva na objekt Linija pa e se zato pozvati lan Crtaj() iz klase Linija,
rezultat ne bi bio korektan, jer ln zapravo pokazuje na tip LinijaSaStralicama. U
tome i jest bit polimorfizma: pomou pokazivaa na osnovnu klasu moe se korektno
baratati i objektima izvedenih klasa.
No ponekad nam moe biti vrlo interesantno ustvrditi na koji objekt svarno
pokaziva pokazuje. Na primjer, zamislimo da imamo niz pokazivaa koji pokazuju na
objekte klase Linija (ili objekte izvedene iz te klase). Neka je sada potrebno napisati
program koji e nacrtati samo linije, dok e se linije sa strelicama namjerno preskoiti
(primjerice radi lakeg pregleda crtea na ekranu). Problem je u tome to mi ne moemo
sa sigurnou odrediti toan tip na koji pokazuje pojedini pokaziva niza. Jedno od
moguih rjeenja je uvoenje virtualne funkcije Tip koja e vraati cijeli broj koji e na
jedinstven nain identificirati klasu. Na primjer:
#define CLS_LINIJA
#define CLS_LINIJASASTRELICAMA

1
2

class Linija {
// ...
public:
virtual int Tip() { return CLS_LINIJA; }
// ...
};

465

class LinijaSaStrelicama : public Linija {


// ...
public:
virtual int Tip() { return CLS_LINIJASASTRELICAMA; }
// ...
};

Sada se pomou mehanizma virtualnih funkcija pojedini tipovi mogu razlikovati.


Gornji kd se moe pozivati ovako:
int main() {
Linija *niz[100];
int duljNiza = 0;
// inicijalizacija elemenata niza
niz[duljNiza++] = new Linija;
niz[duljNiza++] = new LinijaSaStrelicama;
// itd...
// crtanje samo linija
for (int i = 0; i < duljNiza; i++)
if (niz[i]->Tip() == CLS_LINIJA)
niz[i]->Crtaj();
return 0;
}

Izneseno rjeenje e raditi sasvim korektno. No problem je to svaki programer moe


osigurati tipiziranje objekata na svoj vlastiti nain. Na primjer, jedan e funkciju
Tip(),
drugi
DajTip()
a
trei
nazvati
ReciMiReciOgledajceMoje_TipMojKojiJe(). Takoer, ne postoji nikakav nain
da se osigura jednoznanost svih tipova. To znai da ako koristimo biblioteku koju je
razvio netko drugi, moe nam se dogoditi da identifikatori tipa iz te biblioteke budu
jednaki onima koje smo koristili u drugim dijelovima programa. Zbog takvih i slinih
problema, u C++ jezik je uveden mehanizam za identifikaciju tipova tijekom izvoenja
(engl. run-time type identification).

15.2. Operator typeid


Kako bi se omoguio jednoznaan nain dobavljanja informacije o stvarnom tipu nekog
objekta, uveden je operator typeid. On se primjenjuje tako da se u zagradama navede
izraz koji nakon izraunavanja ima vrijednost pokazivaa, objekta ili reference na objekt
neke klase. Kao rezultat operator e dati informaciju o stvarnom tipu objekta tako to
e vratiti referencu na konstantan objekt klase type_info. Ta klasa je definirana
standardom u datoteci zaglavlja typeinfo.h, a objekti te klase nose sve relevantne
podatke o tipu objekta. Objekte klase type_info mogue je usporeivati operatorima
== i != kako bi se ispitala jednakost dvaju tipova. Evo kako je ona deklarirana:

466

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:
// poetak programa je identian
for (int i = 0; i < duljNiza; i++)
if (typeid(niz[i]) == typeid(Linija))
niz[i]->Crtaj();

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;
typeid(ln1) == typeid(ln2);
typeid(ln1) == typeid(const Linija);

// uvijek je istina
// takoer je
// uvijek istina

Ako se kao argument typeid operatoru proslijedi nul-pokaziva, operator e podignuti


iznimku bad_typeid. To je radi toga to nul-pokaziva nema tipa, pa operator typeid
ne moe vratiti nikakvu suvislu vrijednost. Jedina ispravna akcija je podii iznimku.
Napomenimo da intenzivno koritenje operatora typeid vrlo esto signalizira da
neto sa strukturom programa nije u redu. Naime, taj operator izriito rui tipizacijski

467

sustav jezika C++. U mnogim sluajevima je pametnije umjesto typeid operatora


dodati odreenu virtualnu funkciju u osnovnu klasu koja e obavljati operaciju ovisnu o
tipu. To ne znai da typeid treba protjerati zajedno s naredbom goto na planetu Salsua
Secundus. Postoje situacije u kojima je typeid koristan: na primjer, u odreenim
uvjetima htjeli bismo, radi provjere ispravnosti programa, ispisati naziv klase nekog
objekta. To moemo uiniti ovako:
void MaKojeSiMiTiKlase(GrafObjekt *go) {
cout << typeid(*go).name() << endl;
}

Primjetimo da nas esto ne interesira koje je tono klase odreeni objekt, ve je li


objekt moda izveden iz neke klase kako bismo sigurno mogli pristupiti nekom
njegovom svojstvu. U tom sluaju definitivno nije preporuljivo pisati u programu
velike switch naredbe u kojima se testira tip objekta: ako se pojavi nova klasa u
hijerarhiji potrebno je sve takve blokove proiriti. Pametnije je korisiti sigurnu
pretvorbu na nie.

15.3. Sigurna pretvorba


U poglavlju o nasljeivanju opisane su standardne konverzije pokazivaa. Tamo je
navedeno da se svaki pokaziva na neki objekt moe implicitno pretvoriti u pokaziva
na objekt ija klasa je javna osnovna klasa objekta na koji pokaziva pokazuje. To znai
da je mogue sljedee:
Linija *pokLinija = new LinijaSaStrelicama;

Gornje pridruivanje je mogue zato jer je LinijaSaStrelicama izvedena iz klase


Linija javnim nasljeivanjem. Svaka linija sa strelicom e sadravati sve podatkovne
i funkcijske lanove linije. Takva pretvorba se zove pretvorba navie (engl. upcast).
No ponekad je potrebno i suprotno. Na primjer, u gornjem sluaju pokaziva
pokLinija u biti pokazuje na objekt klase LinijaSaStrelicama. Zbog toga ima
smisla pretvoriti ga u pokaziva na LinijaSaStrelicama te pristupati eventualnim
dodatnim podatkovnim i funkcijskim lanovima. No prevoditelj ne moe znati u toku
prevoenja na to e pokaziva pokazivati to se zna tek prilikom izvoenja. Zbog toga
pretvorba iz osnovne u izvedenu klasu nije automatski mogua, nego ju je potrebno
eksplicitno zatraiti:
Linija *pokLinija = new LinijaSaStrelicama;
LinijaSaStrelicama *lss = (LinijaSaStrelicama*)pokLinija;

Ovakva pretvorba se zove pretvorba nanie (engl. downcast). Ona se ne provodi


automatski, nego iskljuivo na eksplicitan zahtjev programera. Ako bi u gornjem sluaju

Carski Zatvorski Planet za okorjele kriminalce iz romana Dune Franka Herberta.

468

pokLinija ipak pokazivao na objekt klase Linija, nakon pretvorbe u


LinijaSaStrelicama i pokuaja pristupa nekom podatkovnom lanu moe doi do

velikih problema ako taj lan postoji samo u izvedenoj klasi. Na primjer, ako klasa
LinijaSaStrelicama sadri cjelobrojni lan tipStrelice koji oznaava tip
strelice, tada e sljedei programski kd prouzroiti velike probleme:
Linija *pokLinija = new Linija;
LinijaSaStrelicama *lss = (LinijaSaStrelicama *)pokLinija;
pokLinija->tipStrelice = 10;
// opasno (po ivot)!

Objekt na koji pokLinija pokazuje ne sadri lan tipStrelice, jer je pomou


operatora new alociran objekt klase Linija. No mi smo pomou eksplicitne pretvorbe
prisilili prevoditelj da provede pretvorbu tipa. Time smo omoguili pristup
nepostojeem lanu. Pokuaj dodjele e prepisati neki sluajan komad memorije koji se
nalazi neposredno iza objekta, a to sigurno nije ono to smo eljeli (osim ako piemo
novu verziju virusa). Problem je u tome to je cijeli program ispravno preveden: sva
odgovornost je na programeru.
Pretvorba nanie je potencijalno opasna ako se ne koristi paljivo. Ako ju
morate koristiti, posluite se radije sigurnom pretvorbom nanie opisanom u
tekstu koji slijedi.
Kada je u C++ jezik uveden podsustav za odreivanje tipova prilikom izvoenja,
otvorila se mogunost da se uvede sigurna pretvorba nanie. Naime, mogue je prvo
provjeriti koji je to tip na koji pokaziva pokLinija pokazuje, a zatim obaviti
pretvorbu ako je ona dozvoljena. U suprotnom, pretvorba se nee obaviti. Tu operaciju
uvodi novi operator dynamic_cast. Njegova sintaksa je
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

Ponaanje operatora dinamike konverzije se razlikuje za pokazivae i reference u


sluaju da gornji uvjet nije ispunjen. Ako se radi o pretvorbi pokazivaa, rezultat
konverzije e biti nul-pokaziva. Ako se pak radi o konverziji referenci, bit e podignuta
iznimka tipa bad_cast. Razmotrimo zbog ega dolazi do takvog ponaanja.
U sluaju pokazivaa, neispravna konverzija se jednostavno moe dojaviti ostatku
programa tako da se vrati nul-pokaziva. No nul-reference ne postoje pa operator
konverzije ne moe vratiti nikakvu suvislu vrijednost koja bi ukazivala na lou
konverziju. Zbog toga je jedino ispravno rjeenje prekinuti program i podii iznimku.
Razmotrimo to na primjeru. Sljedea konverzija je ispravna jer pokaziva uistinu
pokazuje na objekt klase LinijaSaStrelicama:
Linija *pokLinija = new LinijaSaStrelicama;
LinijaSaStrelicama *lss =
dynamic_cast<LinijaSaStrelicama *>(pokLinija);
if (lss) {
// ...
}

Kako se radi o pretvorbi pokazivaa, nakon provedene pretvorbe ispravnost konverzije


se moe provjeriti tako da se testira je li pokaziva jednak nul-pokazivau. U sljedeem
sluaju konverzija referenci e biti neispravna te e doi do podizanja iznimke
bad_cast. Zbog toga je potrebno cijeli postupak staviti u blok pokuaja:
Linija &ln = *new Linija;
try {
dynamic_cast<LinijaSaStrelicama &>(ln).tipStrelice = 10;
}
catch (bad_cast) {
cerr << "Loa pretvorba." << endl;
}

U gornjem primjeru se referenca ln na objekt klase Linija pokuava pretvoriti u


referencu na objekt klase LinijaSaStrelicama pomou operatora dymanic_cast.
Ako pretvorba uspije, rezultat je referenca pa se odmah moe pristupiti lanu
tipStrelice pomou operatora . za pristup lanovima klase. Ako pretvorba ne uspije
(to e biti sluaj u naem primjeru, jer je referenca inicijalizirana objektom klase
Linija), postupak je potrebno prekinuti i sprijeiti neispravno pridruivanje. Zbog toga
e operator dynamic_cast podignuti iznimku bad_cast koja e se uhvatiti u bloku
hvatanja.
Neuspjela dinamika dodjela tipa e u sluaju pokazivaa vratiti nulpokaziva, dok e u sluaju referenci podii iznimku bad_cast.
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

// pretpostavimo da smo sve grafike objekte izveli iz klase


// Atom kako bismo omoguili njihovo pohranjivanje u listu
// ...
GrafObjekt *pgo = lst.AmoGlavu();
if (Bezier *pbez = dynamic_cast<Bezier *>(pgo)) {
// slobodno pristupite Bezierovoj krivulju preko pbez
// ...
}

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.

15.4. Ostali operatori konverzije


Osim operatora dinamike dodjele tipa C++ jezik raspolae s jo tri razliita operatora.
Oni su uvedeni da bi se olakale i uinile jasnijima neke dodjele potrebne u objektno
orijentiranom programiranju. Oni u principu obavljaju sve to moe obaviti i obina
dodjela tipa.
15.4.1. Promjena konstantnosti objekta

Jezik C++ raspolae kljunom rijei const kojom se prevoditelju moe dati na znanje
da mora osigurati nepromjenjivost nekog objekta. Ipak, ponekad se moe ukazati
potreba da se privremeno konstantnost objekta ukloni, te da mu se promijeni neki
podatkovni lan. Tada je mogue iskoristiti operator const_cast. On je opeg oblika
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:
const Vektor v(0, 50);
const_cast<Vektor&>(v).PostaviXY(10, 20);

Kako je objekt v definiran konstantnim, poziv funkcijskog lana PostaviXY() na


njemu ne bi bio dozvoljen. No pomou operatora const_cast objekt se moe uiniti
nekonstantnim te mu se ipak moe promijeniti vrijednost.
15.4.2. Statike dodjele

Postoje dva operatora statike dodjele: static_cast i reinterpret_cast. Njihov


opi oblik je

472

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;
};
class Izvedena : public Osnovna1, public Osnovna2 {
public:
int izv;
};
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;
}

Vano je sjetiti se da se objekt klase Izvedena sastoji od dva podobjekta svaki za


jednu osnovnu klasu. U memoriji taj objekt je sloen prema slici 15.1.

473

Pokaziva &i pokazuje na prvu memorijsku lokaciju zauzetu objektom. Prilikom


pretvorbe u pokaziva na klasu Osnovna2 vrijednost pokazivaa se mora podesiti tako
da on pokazuje na poetak podobjekta Osnovna2. To je mogue zato to operator
dodjele tipa tretira klase kao kompletne tipove pa pomou deklaracije klase moe
podesiti vrijednost pokazivaa. Zbog toga e se gornji kd izvesti na oekivani nain:
dodjela preko pokazivaa pok e stvarno pristupiti lanu osn2 te e se on promijeniti i
u objektu i.
Operator reinterpret_cast radi na drukiji nain. On sve klase tretira kao
nekompletne te ne uvaava njihove deklaracije. Promijenimo funkciju main() na
sljedei nain:
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;
}

Prilikom pretvorbe operator nee provesti nikakvu promjenu vrijednosti pokazivaa. On


e jednostavno interpretirati njegovu vrijednost na novi nain kao pokaziva na objekt
Osnovna2. Pristup lanu osn2 preko pokazivaa pok rezultirat e promjenom
vrijednosti lana osn1, to se moe i vidjeti nakon prevoenja i izvoenja programa.
Ovakva pretvorba moe biti vrlo korisna prilikom pisanja sistemskih programa, kada je
potrebno vrlo precizno kontrolirati sve resurse raunala.

osn1
osn2
izv

dio klase Osnovna1


dio klase Osnovna2
dio klase Izvedena

&i
pok

Slika 15.1. Princip smjetaja objekta u memoriju

474

16. Pretprocesorske naredbe


Je li to napredak kada ljudoder
koristi no i vilicu?
Stanislaw Lec (19091966),
Nepoeljana razmiljanja

U ovom poglavlju upoznat emo se s pretprocesorskim naredbama i njihovom


primjenom pri pisanju programa u jeziku C++. Pretprocesorske naredbe te makro
definicije i makro funkcije koje se pomou pretprocesorskih naredbi ostvaruju esto se
koriste pri pisanju programa u programskom jeziku C. Meutim, zahvaljujui dodatnim
ugraenim svojstvima jezika C++, njihova primjena u jeziku C++ je znaajno smanjena.

16.1. U poetku bijae pretprocesor


Prilikom pisanja programa, veinu kda sainjavaju C++ naredbe koje se prevoenjem
i povezivanjem pretvaraju u izvedbeni kd. Meutim, osim naredbi jezika C++, u
nerijetko se koriste i takozvane pretprocesorske naredbe kojima se automatiziraju neke
operacije. Pretprocesorske naredbe prepoznaju se po znaku # (jugonostalgiari bi rekli
taraba) kojim svaka pretprocesorska naredba (ili direktiva) zapoinje.
Analiza pretprocesorskih naredbi se provodi neposredno prije prevoenja kda.
Pretprocesor e modificirati izvorni kd, generirajui pritom novu, privremenu datoteku.
Ta datoteka, u kojoj vie nema pretprocesorskih naredbi, proslijedit e se prevoditelju.
Korisnik tu datoteku nee moi vidjeti, osim ako posebno podesi parametre svog
prevoditelja. Prevoditelj dakle ne obrauje originalnu datoteku izvornog kda, ve
datoteku koju mu je pretprocesor pripremio. Tijek (pret)procesiranja izvornog kda
moe se vidjeti na slici 16.1 na sljedeoj stranici. (Molimo da nam pravi ljubitelji
tatarskog bifteka ne zamjere na koritenju flaj-maine.)

Tablica 16.1. Pretprocesorske naredbe

#if
#ifdef
#ifndef
#elif
#else
#endif

#include
#define
#undef
#line
#error
#pragma

Znak # koji oznaava poetak


pretprocesorske naredbe mora biti prvi
znak u retku iza eventualnih praznina.
Naredba se protee do kraja retka (tj. do
znaka za novi redak), ali se moe nastaviti
i u sljedeem retku, ako se redak zakljui
znakom \. U tablici 16.1 su navedene sve
pretprocesorske naredbe, grupirane u dva
stupca: naredbe za uvjetno ukljuivanje
kda i kontrolne linije.

475

Slika 16.1. Pretprocesiranje, kompajliranje i linkanje tatarskog bifteka

Pretprocesorske naredbe se u C++ programima uglavnom koriste za ukljuivanje


datoteka s deklaracijama funkcija i klasa te za uvjetno prevoenje dijelova programa.
One takoer omoguavaju definiranje konstanti te makro funkcija, to se obilato
koristilo u C programima. Meutim, predloci funkcija, umetnute funkcije i
preoptereenje funkcija ine makro funkcije u C++ programima potpuno izlinima.
Budui da pretprocesorske naredbe uzrokuju promjenu izvornog kda neposredno
prije prevoenja, one znaju oteati pronalaenje pogreaka u kdu. Iz istog razloga one
e bitno ograniiti usluge koje vam pruaju pomoni alati poput programa za simboliko
otkrivanje pogreaka ili programa za praenje efikasnosti kda (engl. profiler). To emo
ilustrirati u odjeljku 16.3, na primjeru definicije konstante.

16.2. Naredba #include


Naredbu #include ve smo uestalo koristili za ukljuivanje deklaracije funkcija iz
standardnih biblioteka. Njome se u sutini provodi tekstovno povezivanje razliitih
datoteku u jednu cjelinu. Tako e naredbu
#include "toupet"

pretprocesor nadomjestiti sadrajem datoteke toupet. Ta datoteka mora biti izvorni


kd pisan u C++ jeziku, budui da e ona ui u privremenu datoteku koja se
prosljeuje prevoditelju. Ime datoteke koju treba ukljuiti moe biti unutar dvostrukih
navodnika " ili unutar znakova < i > (manje od, odnosno vee od):
#include "mojeKlase.h"
#include <iostream.h>

// iz tekueg imenika
// iz standardnog imenika

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

datoteke zaglavlja, kako je definirano razliitim opcijama prevoditelja. Ako je


definirano vie imenika, tada se traenje provodi redoslijedom kako su ti imenici
navedeni. Kada je ime omeeno navodnicima, tada se datoteka prvo trai u tekuem
(radnom) imeniku, a ako se ne pronae tamo, datoteka se trai u standardnim imenicima
za datoteke zaglavlja.
Prilikom navoenja imena datoteke valja paziti da se ne umeu praznine izmeu
imena i navodnika, odnosno znakova < >:
#include < string.h >

// nije isto kao <string.h>

Naredba #include je neizbjena pri ukljuivanju biblioteka standardnih funkcija, te


kod veih programa u kojima se programski kd rasprostire kroz nekoliko datoteka, o
emu e biti detaljno govora u zasebnom poglavlju.

16.3. Naredba #define


Pretprocesorskom naredbom #define definira se vrijednost simbola, tzv. makro imena
(engl. macro name). Tako e naredba
#define NEKAJ

NETO DRUGO UMJESTO NEKAJ

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.
Iako nije nuno, uobiajena je programerska praksa da se, zbog lake
uoljivosti u izvornom kdu, makro imena piu velikim slovima.
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;
char *b = "NEKAJ";
c = PONEKAJ;

// nadomjestit e
// nee nadomjestiti
// nee nadomjestiti

Pretprocesor e nadomjestiti samo prvu pojavu niza, na to e prevoditelj prijaviti


pogreku, jer e naredba koju e on zaprimiti imati oblik:
a = NETO DRUGO UMJESTO NEKAJ;

477

U drugoj naredbi e sadraj znakovnog niza ostati nepromijenjen, jer pretprocesor ne


pretrauje sadraje znakovnih nizova. U treoj naredbi zamjena nee biti obavljena, jer
niz nije u potpunosti jednak traenom.
Naredba #define se esto rabi u C programima za definiranje konstanti, ali u
jeziku C++ valja izbjegavati takvu njenu namjenu. Obrazloimo i zato. elimo li
definirati nepromjenjivu veliinu, u jeziku C++ emo to napraviti pomou kljune rijei
const:
const double pi = 3.14159;

Pomou pretprocesorske naredbe to bismo napravili na sljedei nain:


#define PI 3.14159

Ona e prouzroiti da sve pojave znakovnog niza PI u izvornom kdu budu


zamijenjene brojem 3.14159. Iako u konanom ishodu (sasvim vjerojatno) nee biti
nikakve razlike, postoji sutinska razlika izmeu ova dva pristupa. U prvom sluaju e
u programu postojati (istina nepromjenjiva) varijabla pi, koja e zauzimati odreeni
memorijski prostor, pa e biti dostupna programu za otkrivanje pogreaka. Naprotiv, u
sluaju pretprocesorske definicije e sve pojave znakovnog niza PI biti zamijenjene
brojem 3.14159 jo prije prevoenja. Stoga nee postojati zasebna varijabla s tom
vrijednou i program ju nee moi identificirati. Pa to onda? PI je uvijek PI, tj.
3.14159, rei e poneki itatelj. To je tono. Meutim, poelite li u programu za
simboliko otkrivanje pogreki izraunati vrijednost koju daje izraz
float PovrsinaKruga = r * r * PI;

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

Ovakva pretprocesorska naredba natjerat e pretprocesor da ukloni sve pojave niza


SOLO iz izvornog kda. Taj oblik naredbe poprima puni smisao u paru s naredbama za
uvjetno prevoenje #ifdef i #ifndef, koje emo upoznati kasnije u ovom poglavlju.
16.3.1. Trajanje definicije

Vrijednost makro imena moe se promijeniti novom naredbom #define:

478

#define POZDRAV "Dobar dan!"


cout << POZDRAV << endl;

// Dobar dan!

#define POZDRAV "Hvala, tebi takoer."


cout << POZDRAV << endl;

// Hvala, tebi...

Neki prevoditelji e odaslati upozorenje o promjeni definicije makro imena.


Jednom definirano makro ime ostaje definirano do kraja tekue datoteke ili do
naredbe #undef kojom se on ponitava. Zbog toga e, nadoveemo li na gornji kd
sljedee naredbe:
#undef POZDRAV
cout << POZDRAV << endl;

// pogreka: nema vie POZDRAV-a

Prevoditelj sada javlja pogreku, jer je naredba ekvivalentna


cout <<

<< endl;

// pogreka: dva operatora << uzastopce

Valja napomenuti da pod datotekom ovdje podrazumijevamo privremenu datoteku koju


generira pretprocesor, zajedno s kdom datoteka ukljuenih naredbom #include.
16.3.2. Rezervirana makro imena

Postoje odreena makro imena koja su rezervirana za specifine namjene, te se ne smiju


predefinirati ili unititi naredbom #undef. Navedimo prvo imena koja su odreena
standardom, s njihovim kratkim opisom. Njihovu primjenu emo ilustrirati kasnije u
ovom poglavlju.
_ _LINE_ _
_ _FILE_ _
_ _DATE_ _

_ _TIME_ _
_ _STDC_ _

_ _cplusplus

Broj tekueg retka u datoteci izvornog kda (decimalna konstanta).


Ime tekue datoteke izvornog kda (znakovni niz).
Datum prevoenja datoteke izvornog koda. Datum je znakovni niz
oblika Mmm dd gggg, gdje je ime mjeseca troslovna kratica
engleskog imena mjeseca.
Vrijeme prevoenja datoteke izvornog kda. Vrijeme je znakovni niz
oblika hh:mm:ss.
Ovo makro ime ne mora biti definirano. Ako jest, vrijednost ovisi o
implementaciji, tj. moe se razlikovati za pojedine prevoditelje. Ako
je ime definirano, ono oznaava da se prevoenje provodi u skladu s
ANSI standardom.
Ovo makro ime je definirano prilikom prevoenja datoteke C++
prevoditeljem.

Meutim, veina prevoditelja definira jo i neka dodatna imena, iji se opis moe nai
u pripadajuim uputama.

479

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:
cout <<
<<
<<
<<

"Redak br. " << _ _LINE_ _ - 1 << endl


"Datoteka: " << _ _FILE_ _ << endl
"Datum prevoenja: " << _ _DATE_ _ << endl
"Vrijeme prevoenja: " << _ _TIME_ _ << endl;

16.3.3. Makro funkcije

Pomou naredbe #define mogu se definirati i makro funkcije. Makro funkcija je


simbol stvoren pomou #define naredbe, koji moe prihvaati argumente poput
funkcija pisanih u jeziku C++. Openito se makro funkcija definira kao:
#define imeMakroFun( lista_argumenata ) ( tijelo_funkcije )

Pretprocesor e simboliki poziv makro funkcije u izvornom kdu nadomjestiti tijelom


makro funkcije s umetnutim stvarnim argumentima. Na primjer, pretpostavimo da je
definirana makro funkcija
#define KVADRAT(x) ((x) * (x))

Napiemo li sada negdje u programu:


c = KVADRAT(10);

// izvorni kd

nju e pretprocesor nadomjestiti kdom:


c = ((10) * (10));

// nakon pretprocesora

Meutim, makro funkcije znaju vrlo malo o sintaksi C++ jezika, ve vrte svoj film.
Stoga e sljedei poziv makro funkcije:
d = KVADRAT(++i);

pretprocesor razviti u kd

// neto tu zaudara...

480

d = ((++i) * (++i));

// nakon pretprocesora

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

poziv makro funkcije


d = KVADRAT_UPITNI(2 + 3);

pretprocesor bi razvio u naredbu:


d = 2 + 3 * 2 + 3;

// nije ni kvadrat vie ono


// to je neko bio

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.
U C++ programima valja izbjegavati koritenje makro funkcija. Primjena
makro funkcija odraava nedosljednost programskog jezika, programa ili
programera [Stroustrup91].

16.3.4. Operatori za rukovanje nizovima

Pri pisanju makro funkcija ili makro definicija, korisniku stoje na raspolaganju dva
operatora koji omoguavaju rukovanje simbolikim imenima (engl. token) iz programa:
operator # za pretvorbu simbolikog imena u znakovni niz, te operator ## za stapanje
simbolikih imena.
Operatorom # je mogue simboliko ime pretvoriti u znakovni niz. Ilustrirajmo to
sljedeim primjerom: prilikom razvoja programa elimo provjeriti vrijednosti pojedinih

481

varijabli, tako da ih ispisujemo iz programa. Da bismo znali koja ispisana vrijednost


odgovara kojoj varijabli, uz vrijednost moramo ispisati i ime varijable. Zdravo-seljaki
pristup bi bio umetanje naredbe za ispis oblika:
cout << "varijabla = " << varijabla << endl;

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

Operator # ispred simbolikog imena pretvorit e ga u znakovni niz, onakav kakvim ga


podrazumijeva jezik C++. Za ispis e sada trebati samo umetnuti naredbe oblika:
PROVJERA(a);

Gornja naredba se prilikom obrade pretprocesorom pretvara u


cout << "a" " = " << a << endl;

Operator ## omoguava stapanje simbolikih imena. To se ponekad koristi za


generiranje novih simbolikih imena. Na primjer, definiramo li makro funkciju:
#define NESTO_SASVIM_NOVO(i, j) (i ## j)

poziv u naredbi
int NESTO_SASVIM_NOVO(x, 6);

e generirati novo simboliko ime, jer e pretprocesor ovu naredbu razviti u


int x6;

16.4. Uvjetno prevoenje


Makro naredbe za uvjetno ukljuivanje omoguavaju da se u kd koji se prevodi
ukljuuju razliiti programski odsjeci, shodno rezultatu nekog logikog izraza. To nam
omoguava da samo promjenom definicije makro imena koje se koristi u tom logikom
izrazu, generiramo razliite inaice istog kda.
Postoje tri naredbe za ispitivanje uvjeta uvjetnog prevoenja: #if, #ifdef i
#ifndef. Blok naredbi koji zapoinje s bilo kojom od te tri naredbe mora biti zakljuen

482

#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:
#define JEZIK HRVATSKI
//...
#if
JEZIK == ENGLISH
char *pozdrav = "Do you speak English?";
#elif JEZIK == DEUTSCH
char *pozdrav = "Sprechen Sie Deutsch?";
#elif JEZIK == HRVATSKI
char *pozdrav = "Zborite li rvatski?";
#else
// umjesto esperanta:
char *pozdrav = " j ?";
#endif
//...
cout << pozdrav << endl;

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

"dozvoljava ispis na vaem pisau" << endl;


#else
IspisiNaPisacu(rezultat);
#endif

Uz ovakav #define generirat e se demo verzija programa; izbacivanjem naredbe


#define, makro ime DEMO e biti nedefinirano, te e se generirati radna verzija.
Komplementarna pretprocesorskoj naredbi #ifdef jest naredba #ifndef koja
ispituje je li makro ime koje slijedi nedefinirano: ako je nedefinirano, prevode se
naredbe koje slijede; u protivnom se preskau sve naredbe do pripadajue #endif
direktive.
Sklopovi naredbi #if, #ifdef i #ifndef redovito se koriste u programima u
kojima se izvorni kd rasprostire kroz vie datoteka, pa emo se s njihovom praktinom
primjenom jo podrobnije upoznati u poglavlju 15.
16.4.1. Primjena uvjetnog prevoenja za pronalaenje pogreaka

Uvjetno prevoenje u kombinaciji s makro funkcijama se esto koriste za olakavanje


pronalaenja pogreaka prilikom razvoja programa. Na primjer, mogue je definirati
makro funkciju KONTROLA() koja e ispisivati vrijednost parametra na standardni izlaz
za pogreke koji kasnije moemo analizirati i zakljuiti zato neki program ne radi. Pri
tome emo definirati funkciju tako da se jednim potezom svi ispisi mogu iskljuiti, i
ponovnim prevoenjem izbaciti iz kda. Na taj nain dobili smo kd koji e s jedne
strane omoguavati kontrolu programa prilikom razvoja, dok s druge strane
komercijalna verzija nee biti optereena nepotrebnim kdom (za koji se esto pokae
da i nije ba tako nepotreban).
Prisutnost ili odsutnost kontrolirajuih ispisa emo nadzirati makro imenom
NDEBUG. To ime se koristi u datoteci zaglavlja assert.h. Ta datoteka sadrava makro
funkciju assert() koja, ako izraz koji je naveden kao parametar funkciji nakon
izraunavanja daje vrijednost false, ispisuje na ekran poruku Assertion failed:. Ako,
pak, definiramo simbol NDEBUG, sve assert() makro funkcije e se prilikom
prevoenja zamijeniti praznim naredbama ime e se provjere izbaciti iz programa. Evo
mogueg kda makro funkcije KONTROLA():
#ifdef NDEBUG
#
define KONTROLA(izraz) ((void)0)
#else
#
define KONTROLA(izraz) cerr << izraz << endl
#endif

Sada ako na odreenom mjestu u programu elimo provjeriti vrijednost varijable a, to


moemo uiniti ovako:
KONTROLA(a);

484

Ako je na odreenom mjestu u programu smislena vrijednost varijable a razliita od


nule, umjesto ispisa koji bi mogao samo dodatno opteretiti ekran pogreaka, moemo
koristiti makro funkciju assert():
assert(a != 0);

16.5. Ostale pretprocesorske naredbe


Pretprocesorska naredba #error ima openiti oblik:
#error poruka

gdje je poruka tekst koji elimo ispisati. Ona e prilikom prevoenja prouzroiti ispis
poruke oblika:
Error: ImeDatoteke BrLinije : Error directive: poruka

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

Zaboravimo li definirati JEZIK, pretprocesor e onemoguiti prevoenje, uz ispis


imena datoteke i broja linije u kojoj se nalazi navedena #error naredba te poruke
navedene u naredbi.
Naredbom #pragma se podeavaju parametri pretprocesora ili prevoditelja.
Openiti oblik naredbe je:
#pragma parametar
parametar je niz koji podeava odreeno svojstvo prevoditelja. Ti nizovi su odreeni

implementacijom, pa zainteresiranog itatelja upuujemo na prirunik s uputama za


prevoditelj koji koristi.

485

Pretprocesorskom naredbom #line moe se promijeniti vrijednost makro imena


_ _LINE_ _ i _ _FILE_ _ , to se ponekad moe iskoristiti za referenciranja pri traenju
pogreaka. Naredba ima openiti oblik:
#line broj_retka "ime_datoteke"

Ime datoteke se moe izostaviti u tom sluaju _ _FILE_ _ ostaje nepromijenjen.


Pretpostavimo da smo pri pisanju nekog programa dio kda neke datoteke
jednostavno preslikali u na novi kd. Da bismo pri provjeri rada programa znali da se
radi o kdu umetnutom iz druge datoteke, ispred umetnutih naredbi ubacit emo
naredbu kojom emo redefinirati vrijednosti imena _ _LINE_ _ i _ _FILE_ _ . Naravno
da iza umetnutog odsjeka treba opet vratiti makro imena na stare vrijednosti:
// ...
#line 1 "umetnuto.cpp"
// slijedi umetnuti kd
#line 134 "novikod.cpp"
// slijedi novi kd

16.6. Ma a e meni pretprocesor?


Kao to smo vidjeli, upotreba pretprocesorskih naredbi je kod pisanja programa u jeziku
C++ esto problematina. Sam jezik C++ sadri mehanizme koji su u C-u bili ostvarivi
iskljuivo preko makro funkcija. U sutini, primjena pretprocesorskih naredbi trebala bi
se ograniiti na sljedea tri sluaja:

Ukljuivanje datoteka #include naredbom.


Uvjetno ukljuivanje dijelova kodova za kontrolne ispise prilikom razvoja programa.
Uvjetno ukljuivanje pri prevoenju razliitih izvedbi programa (na primjer s
porukama na razliitim jezicima, na razliitim operacijskim sustavima).

486

17. Organizacija kda u sloenim programima


Tiranija je bolje organizirana nego sloboda.
Charles Pguy 18731914, Rat i mir

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.

17.1. Zato u vie datoteka?


Primjeri koji su koriteni u ovoj knjizi su bili najjednostavniji mogui (iako toga moda
u danom trenutku niste bili svjesni) i za njih nije bilo potrebe kd pohranjivati u
odvojene datoteke. U veini kraih programa od nekoliko stotina redaka kda, cijeli
program se sasvim lijepo dade smjestiti u jednu datoteku i kao takav obraivati.
Meutim, kod kompleksnijih problema se redovito ukazuje potreba za razbijanjem kda
u nekoliko odvojenih datoteka. Navedimo glavne razloge za razbijanje izvornog kda u
vie datoteka.
Zamislimo da smo napisali program od desetak tisua redaka izvornog kda.
Promijenimo li samo jednu naredbu (na primjer ime neke varijable ili vrijednost neke
konstante), trebat e prevesti cjelokupni izvorni kd i povezati dobiveni objektni kd.
Proces prevoenja cjelokupnog kda u ekstremnim sluajevima moe trajati ak i vie
od sat vremena. Naprotiv, ako se izvorni kd rasprostire kroz nekoliko datoteka, tada
treba prevesti samo datoteku u kojoj je ispravka napravljena, te pripadajui objektni kd
povezati s ve gotovim objektnim kdovima ostalih datoteka. tovie, dananji
prevoditelji imaju ugraene mehanizme kojima samostalno provjeravaju koje su
datoteke izvornog kda mijenjane od zadnjeg prevoenja/povezivanja, tako da
programer ne treba eksplicitno navoditi koju datoteku treba prevoditi.
Druga stvar koja vam se moe dogoditi jest da prevoditelj tijekom prevoenja
odbije poslunost i javi da nema dovoljno memorije za uspjeni zavretak operacije.
Naime, prevoditelj prilikom prevoenja generira simboliku tablicu koja uspostavlja
vezu izmeu simbolikih naziva objekata i funkcija u izvornom kdu i njihove adrese u

487

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
void cijaFunk(int dajStoDas);
// ...
cijaFunk(0);

// deklaracija
// poziv

Tek se prilikom povezivanja trai odgovarajua definicija funkcije i ako se ona ne


pronae, poveziva e prijaviti pogreku da definicija funkcije te i te nije pronaena.
Na primjer, ako je funkcija cijaFunk() definirana u modulu definic.cpp kao
definic.cpp
int cijaFunk(char *tkoTeSljivi) {
//...
}

// definicija

poveziva e prijaviti pogreku. Oevidno to znai da prevoditelja moemo (ak i


nehotice) prevariti tako da navedemo u nekom modulu deklaraciju nedefinirane
funkcije (ili deklaraciju koja e se razlikovati od definicije), meutim na kraju e
poveziva ipak prozreti na podmukli pokuaj. (You may fool all the people some of the
time; you can even fool some of the people all the time; but you cant fool all of the
people all the time).
Prevoditelj vidi samo modul koji prevodi pa ne moe usporediti deklaraciju
objekta ili funkcije sa stvarnom definicijom ako su one u odvojenim
datotekama.
No valja napomenuti da prevoditelj nee uvijek reagirati ovako promptno. U to se
moemo uvjeriti ako u definiciji gornje funkcije uskladimo tip argumenta s tipom
argumenta u deklaraciji, tj. ako u datoteci definic.cpp funkciju definiramo kao:
int cijaFunk(int dajStoDas) {
//...
}

// promijenjena definicija

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.
Ako deklaracija funkcije u jednoj datoteci i definicija istoimene funkcije u
drugoj datoteci imaju jednake potpise, poveziva e shvatiti da se radi o
istoj funkciji. Ako su se one razlikuju po tipu povratnih vrijednosti, nastupit
e pogreka pri izvoenju.
Iz dosadanjih razmatranja moemo naslutiti da je za pravilnu organizaciju kda
neophodno razluiti doseg imena pojedinih identifikatora. Ve smo nekoliko puta
naglaavali da su objekti deklarirani unutar blokova vidljivi samo unutar tog bloka. To
se odnosi kako na objekte deklarirane unutar funkcija, tako i na ugnijeene klase.
Identifikatori koji su deklarirani izvan funkcija i klasa uglavnom su (uz asne izuzetke
koje emo navesti) vidljivi u cijelom kdu, bez obzira to se taj kd moe rasprostirati
kroz vie datoteka. Za identifikatore (tj. objekte i funkcije) koji su prilikom povezivanja
vidljivi i u drugim modulima kae se da imaju vanjsko povezivanje (engl. external
linkage). Zbog toga u cijelom programu smije postojati samo jedan objekt, tip ili
funkcija s tim imenom.
S druge strane, postoje objekti i funkcije koji su vidljivi samo unutar datoteke. Za
njih se kae da imaju unutarnje povezivanje (engl. internal linkage). Globalne umetnute
(inline) funkcije i simbolike konstante imaju unutarnje povezivanje. Zato e
umetnuta funkcija svojaUsvojoj() i simbolika konstanta lokalnaKonstanta u
kdu
prima.cpp
inline void svojaUsvojoj() {
// tijelo funkcije
}
const int lokalnaKonstanta = 23;

biti nevidljivi izvan datoteke u kojoj su definirani. To znai da u drugim modulima


moemo definirati istoimene umetnute funkcije, odnosno simbolike konstante, a da se
njihova imena ne sukobljavaju s navedenim identifikatorima. Na primjer, u nekoj drugoj
datoteci moemo napisati
seconda.cpp
inline int svojaUsvojoj(int n) {
// ...
}

// potpuno nova funkcija

const float lokalnaKonstanta = 12.; // ... i konstanta

490

Prilikom povezivanja obiju datoteka poveziva nee prijaviti pogreku da se funkcija


svojaUsvojoj() i objekt lokalnaKonstanta pojavljuju dva puta, jer je svaki od
njih lokalan za datoteku u kojoj su navedeni.
Zanimljivo je spomenuti da se ime s vanjskim povezivanjem definirano u nekoj
datoteci nee sukobiti s imenom objekta ili funkcije s unutarnjim povezivanjem u nekoj
drugoj datoteci. To znai da e povezivanjem bilo koje od gornjih datoteka s datotekom
tertima.cpp u kojoj su definirani funkcija i varijabla s vanjskim povezivanjem,
poveziva stvoriti dvije funkcije svojaUsvojoj() i dvije varijable
lokalnaKonstanta:
tertima.cpp
void svojaUsvojoj() {

// funkcija s vanjskim
// povezivanjem

// ...
}
double lokalnaKonstanta = 210.; // varijabla s vanjskim pov.

Svi pozivi funkcije svojaUsvojoj() i sva dohvaanja varijable lokalnaKonstanta


iz programa poveziva e usmjeriti funkciji, odnosno varijabli iz datoteke
tertima.cpp, osim ako se to ne ini iz modula u kojima su definirani istoimeni
funkcija i objekt s unutarnjim povezivanjem. Naravno da ovakvo svojstvo predstavlja
potencijalnu opasnost, koju valja izbjegavati pravilnom organizacijom kda.
Korisniki tipovi definirani pomou kljune rijei typedef su takoer vidljivi
samo unutar datoteke u kojoj su definirani. Zbog toga se isti sinonim moe koristiti u
razliitim datotekama za razliite tipove objekata, odnosno funkcija:
prva.cpp
typedef int Tip;
// ...

druga.cpp
// donja deklaracija moe miroljubivo i aktivno
// koegzistirati s onom u datoteci "prva.cpp":
typedef double Tip;
// ...

Za imena deklarirana unutar blokova se kae da nemaju povezivanje. Takva se imena


(konkretno imena lokalnih klasa i pobrojenja) ne mogu koristiti za deklaraciju objekata
izvan bloka u kojemu su definirani:

491

void funkcija() {
class LokalnaKlasa {
//...
};
}
LokalnaKlasa classaLocale;

// klasa bez povezivanja

// pogreka

Ako za neki identifikator elimo povezivanje koje nije podrazumijevano, tip


povezivanja moemo promijeniti kljunim rijeima static i extern, o emu e biti
rijei u sljedeem odjeljku.
17.2.1. Specifikatori static i extern

Dodavanjem kljune rijei static ispred deklaracije objekta, funkcije ili anonimne
unije, eksplicitno se pridjeljuje interno povezivanje. To omoguava da se u pojedinim
modulima definiraju razliiti globalni objekti ili funkcije s jednakim imenima, a da zbog
toga ne doe do sukoba imena prilikom povezivanja.
U sljedeem primjeru e navedene objektne datoteke nakon povezivanja imati
svaka svoje inaice funkcije f(), varijable broj i anonimne unije:
suveren1.cpp
static int f(int argument) {
//...
}
static float broj;
static union {
int br;
char ch;
};

suveren2.cpp
static void f() {
//...
}
static char broj;
static union {
int br;
char ch;
};

Paljiviji itatelj se zasigurno sjea da smo kljunu rije static ve upoznali i to u


dva navrata: kod funkcija i kod klasa. U oba sluaja ona je imala razliito znaenje. Kod
funkcija kljuna rije static, primijenjena na lokalne objekte unutar funkcije, ini te

492

objekte trajnima pri izlasku iz funkcije se statiki objekt ne unitava, ve ostaje


nedodirljiv (i nepromijenjen) do ponovnog poziva funkcije. U klasama se kljunom
rijei static pojedini podatkovni ili funkcijski lan proglaava jedinstvenim za sve
objekte te klase. Sada smo upoznali i tree znaenje kljune rijei static, a to je da
globalne objekte i funkcije ini privatnima za pripadnu datoteku.
Preporuljivo je sve globalne objekte i funkcije deklarirati kao statike, osim
ako postoji oita potreba da se oni dohvaaju iz drugih modula.
Time se smanjuje vjerojatnost sukoba globalnih imena iz razliitih modula, do kojeg
moe lako doi ako ih piu razliiti programeri. Osim toga, deklaracija static
primijenjena na funkcije moe doprinijeti boljoj optimizaciji kda prilikom prevoenja.
Kljunom rijei extern se podrazumijevano unutarnje povezivanje nekog
identifikatora pretvara u vanjsko. Ako elimo, primjerice, simbolike konstante uiniti
dostupnima i iz druge datoteke, to moemo uiniti na sljedei nain:
svecnst1.cpp
extern const float pi = 3.1415926;

// definicija

svecnst2.cpp
// samo deklaracija definicija je negdje drugdje:
extern const float pi;
cout << pi << endl;
// ispisuje 3.14159...

Umetanjem kljune rijei extern ispred deklaracije prevoditelju se daje na znanje da je


taj objekt (misli se bilo na objekt neke klase, varijablu ili funkciju) definiran u drugom
modulu. Ilustrirajmo to primjerom programa razdvojenim u dvije datoteke:
johann.cpp
int bwv = 565;
int toccata() {
//...
}

// definicija funkcije

bastian.cpp
extern int bwv;
extern int toccata();

// samo deklaracija
// takoer

float fuga() {

// deklaracija & definicija

493

bwv = toccata();
}

U drugom modulu koriste se varijabla bwv i funkcija toccata() definirane u prvom


modulu. Da bi prevoditelj mogao potpuno obraditi drugi modul, moraju mu biti
dostupne deklaracije te varijable, odnosno funkcije kako bi znao njihove tipove. Stoga
su deklaracije navedene na poetku datoteke bastian.cpp. Kljunom rijei extern
ispred deklaracija varijable bwv i funkcije toccata() eksplicitno se naglaava da je ta
varijabla definirana u nekom drugom modulu i da je navedena naredba iskljuivo
deklaracija. Izostavi li se ta kljuna rije, naredba
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:
extern int bwv = 538;

naredba e postati definicijom. Prevoditelj e zbog operatora pridruivanja jednostavno


zanemariti kljunu rije extern.
U deklaraciji funkcije toccata() kljuna rije extern nije nuna, jer prevoditelj
sam moe prepoznati da se radi o deklaraciji, budui da iza zagrada () slijedi znak
toka-zarez. Stoga se u deklaracijama vanjskih funkcija kljuna rije extern redovito
izostavlja, no ako se stavi, nije pogrena.
Zadatak. Razmislite zato e poveziva prijaviti pogreke tijekom povezivanja sljedea
dva modula (pretpostavite da se samo ta dva modula ine cijeli program):
modul_1.cpp
int crniVrag = 1;
int zeleniVrag = 5;
extern float zutiVrag;

494

modul_2.cpp
int crniVrag;
extern double zeleniVrag;
extern float zutiVrag;

// pogreka kod povezivanja


// pogreka kod izvoenja
// pogreka kod povezivanja

U gornjem zadatku, cjelobrojna varijabla crniVrag je inicijalizirana dva puta: u


modul_1.cpp na vrijednost 1, a u modul_2.cpp na vrijednost 0. Naime, kao to je
ve reeno, globalne varijable bez prethodee kljune rijei extern se, ak i ako nije
eksplicitno specificirana neka vrijednost, inicijaliziraju podrazumijevanom vrijednou
nula. Varijabla zeleniVrag je u modulima deklarirana razliitog tipa, jednom kao int,
a drugi puta kao double. Veina povezivaa nee uoiti razliku u tipovima, te e
povezati kd, to e onda prouzroiti pogreku kod izvoenja. Varijabla zutiVrag je u
oba modula deklarirana kao vanjska nedostaje njena definicija.
Kljuna rije extern je neophodna i prilikom povezivanja kda pisanog u jeziku
C++ s izvornim kdom pisanim u nekom drugom programskom jeziku, o emu e biti
vie govora u odjeljku 17.6.

17.3. Datoteke zaglavlja


U poglavljima o funkcijama i klasama naglasili smo razliku izmeu deklaracije i
definicije funkcije. Deklaracijom funkcije, odnosno klase, stvara se samo prototip, koji
se konkretizira tek navoenjem definicije. Iako je svaka definicija ujedno i deklaracija,
pod deklaracijom u uem smislu podrazumijevamo samo navoenje prototipa.
Deklaracije se smiju ponavljati, ali se one moraju podudarati po tipu (izuzetak su
preoptereene funkcije i operatori). Naprotiv, definicija smije biti samo jedna u
protivnom e prevoditelj prijaviti pogreku. Pritom je neophodno da funkcija, odnosno
klasa, mora biti deklarirana prije prvog poziva funkcije, odnosno instantacije objekta
klase. Ovo je neophodno zato da bi prevoditelj ve prilikom prvog nailaska na poziv
funkcije ili instantaciju klase mogao provjeriti ispravnost poziva te generirati ispravan
objektni kd, provodei eventualno potrebne konverzije tipova.
U sloenijim programima se esto neka funkcija poziva iz razliitih datoteka, ili se
na osnovu neke klase stvaraju objekti u razliitim datotekama. Vie nego oito jest da
deklaracije funkcije/klase moraju biti izdvojene od definicija i dostupne cjelokupnoj
datotenoj klijenteli. Jedna mogunost jest da se neophodne deklaracije prepiu na
poetke odgovarajuih datoteka, ali je odmah uoljiv nedostatak ovakvog rjeenja:
prilikom promjene u deklaraciji neke funkcije ili klase treba pretraiti sve datoteke u
kojima se ona ponavlja te provesti runu izmjenu. Osim to je ovakvo pretraivanje
mukotrpno, ono je podlono i moguim pogrekama.
Daleko je efikasnije deklaracije izdvojiti u zasebne datoteke zaglavlja, koje e se
pretprocesorskom naredbom #include ukljuivati na poetku svake datoteke gdje je to
neophodno. U ovom sluaju, izmjena u deklaraciji se obavlja samo na jednom mjestu,
tako da ne postoji mogunost pogreke prilikom viekratnog ispravljanja. Radi
preglednosti se datotekama zaglavlja daje isto ime kao i datotekama koje sadre

495

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):
/**********************************************************
Program:
Datoteka:
Autori:
Izmjene:

Moj prvi sloeni program


Slozeni.h
Boris (bm)
Julijan (j)
31.11.96.
(j) dodana funkcija xyz()
29.02.97.
(bm) izbaen SudnjiDan

**********************************************************/

Pretprocesorske naredbe #include za ukljuivanje drugih datoteka zaglavlja,


najee standardnih biblioteka:
#include <iostream.h>
#include "MyString.h"

Najee se ipak te naredbe navode u samoj datoteci izvornog kda.

Definicije tipova (deklaracije klasa i struktura):


class KompleksniBroj {
private:
double Re;
double Im;
public:
//...
};

496

Definicije pojedinih funkcijskih i podatkovnih lanova klasa navode se u samoj


datoteci izvornog kda. Izuzetak su, naravno, umetnuti (inline) funkcijski lanovi
koji se moraju navesti unutar deklaracije klase. Razlog tome je to prevoditelj
prilikom prevoenja mora imati dostupan kd koji e umetnuti na mjesto poziva.

Deklaracija i definicije predloaka (u nekim sluajevima):


template <class Tip>
class Tablica {
private:
Tip *pok;
public:
Tablica(int n);
// ...
};

// deklaracija

template <class Tip>


Tablica<Tip>::Tablica(int n) {
// ...
}

Naime, slino umetnutim funkcijama, izvorni kd predloka mora biti dostupan


prevoditelju prilikom prevoenja kako bi mogao instancirati predloak potreban broj
puta. Baratanje s predlocima u vie datoteka izvornog koda dosta je sloeniji
problem, ije rjeenje ovisi o razvojnoj okolini koja se koristi. Posljednja verzija C++
standarda nudi mogunost eksplicitne instantacije predloaka, ime se moe izbjei
stavljanje definicije predloaka u datotekama zaglavlja. Ovo emo potanko objasniti
u zasebnom odjeljku.

Deklaracije funkcija:
extern void ReciMiReci(const char *poruka);

Definicije umetnutih (inline) funkcija:


inline double kvadrat(double x) { return x*x };

Deklaracije globalnih podataka koji moraju biti dohvatljivi iz razliitih modula:


extern char *verzijaPrograma;

Definicije konstanti koje moraju biti dohvatljive iz razliitih modula:


const float e = 2.718282

Pobrojenja:
enum neprijatelj { unutarnji, vanjski, VanOvan, Feral };

497

Makro definicije i makro funkcije:


#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:
void IspisStraga(const char *tekst) {
int i = strlen(tekst);
while (i-- > 0) cout << *(tekst + i);
cout << endl;
}

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:


const char abeceda = {'a', 'b', 'c', 'd'};

Argument protiv navoenja konstantnih argumenata u datotekama zaglavlja je


pragmatian: veina prevoditelja ne provjerava jesu li generirane redundantne kopije
agregata [Stroustrup91].

17.4. Organizacija predloaka


Organizaciju kda jezik C++ nasljeuje primarno od jezika C. Predloci su koncept koji
se ne uklapa glatko u opisani model datoteka zaglavlja i datoteka izvornog kda. Zbog
toga je rukovanje predlocima u veim programima dosta sloeno.

498

U starijoj varijanti jezika C++ u kojoj nije postojao operator za eksplicitnu


instantaciju predloka imali smo nekoliko mogunosti, od kojih niti jedna nije bila
previe sretna. Mogunost koju su koristili mnogi programeri, a koje su zahtijevale i
neke implementacije, jest bila smjestiti definiciju predloka u datoteku zaglavlja. Tako
bi u svakoj datoteci koja ukljuuje to zaglavlje, predloci bili automatski dostupni, te bi
se prilikom prevoenja eljeni predloci instancirali. Ako bi vie datoteka izvornog kda
koristilo isti predloak iz neke datoteke zaglavlja, na kraju bismo dobili objektni kd
koji bi sadravao nekoliko instanci iste funkcije. Poveziva bi zatim morao izbaciti sve
definicije osim jedne. Takvo rjeenje definitivno nije sretno: osim to ovisimo o dobroj
volji povezivaa da nam skrati izvedbeni kd, mogue je zamisliti predloak koji e u
razliitim datotekama izvornog kda biti instanciran u razliiti izvedbeni kd. Time se
kri osnovno C++ pravilo da svaki predloak mora imati tono jednu definiciju, a
dobiveni izvedbeni kd zasigurno ne bi bio ispravan.
Druga mogunost, koja dosta ovisi o programskoj okolini u kojoj radimo, jest samo
deklarirati predloak u datoteci zaglavlja, a posebnu datoteku u kojoj su definicije staviti
prevoditelju na raspolaganje. Prilikom povezivanja, ako bi se ustanovilo da instanca
nekog predloka ne postoji za eljeni tip, poveziva bi ponovo pozvao prevoditelj koji bi
generirao traeni predloak, a postupak povezivanja bi se zatim ponovio. Niti ovakvo
rjeenje nije ba ugodno: postupak prevoenja sada je bio znatno produljen, jer bi se
mnogo vremena gubilo na prebacivanje izmeu prevoenja i povezivanja. Takoer,
mnoge jednostavnije razvojne okoline nisu omoguavale da poveziva poziva
prevoditelj.
Programeri su se tada dovijali na razne naine. Jedno od moda najee koritenih
rjeenja sastojalo se u tome da se u datoteku zaglavlja smjesti samo deklaracija
predloka, a u zasebnu datoteku definicija predloka, te da se eksplicitno prisili
prevoditelj da instancira predloak za sve koritene varijante parametara. To se moe
uiniti tako da se stvori neki globalni objekt ili da se pozove eljena funkcija.
Promotrimo kako to izgleda na jednostavnom primjeru. Zamislimo da emo raditi s
predlokom klase Kompleksni parametrizirane tipom koji odreuje preciznost brojeva.
Takoer, imat emo preoptereen operator << za ispis na izlazni tok. Deklaraciju klase i
operatora emo smjestiti u zaglavlje kompl.h, a definiciju predloka u datoteku
kompl.cpp. U toj datoteci emo takoer prisiliti prevoditelj da instancira predloke
Kompleksni<int> i Kompleksni<double> i pripadajue operatore umetanja.
kompl.h
#include <iostream.h>
template <class Prec>
class Kompleksni {
private:
Prec re, im;
public:
Kompleksni(Prec i = 0, Prec b = 0) : re(i), im(b) {}
Prec DajRe();
// ovi lanovi namjerno nisu umetnuti

499

Prec DajIm();
// kako bi se pokazala organizacija
void PostaviReIm(Prec, Prec);
};
template <class Prec>
ostream &operator <<(ostream &, Kompleksni<Prec> &);

kompl.cpp
#include <iostream.h>
#include "kompl.h"
template <class Prec>
Prec Kompleksni<Prec>::DajRe() {
return re;
}
template <class Prec>
Prec Kompleksni<Prec>::DajIm() {
return im;
}
template <class Prec>
void Kompleksni<Prec>::PostaviReIm(Prec r, Prec i) {
re = r; im = i;
}
template <class Prec>
ostream &operator <<(ostream &os, Kompleksni<Prec> &c) {
os << c.DajRe();
if (c.DajIm() >= 0) os << "+";
os << c.DajIm() << "i";
return os;
}
// sada idu forsirane instance klase i operatora:
static NekoristenaFunkcija() {
Kompleksni<double> c1;
Kompleksni<int> c2;
cout << c1 << c2;
}

U gornjem primjeru imamo funkciju NekoristenaFunkcija() koja je uinjena


statikom kako ne bi bila vidljiva izvan datoteke kompl.cpp. U njoj su deklarirana dva
objekta klase Kompleksni i pozvani su operatori za ispis kako bi se prisilio prevoditelj
da instancira predloak. Evo kako bi se navedeni predloci koristili iz glavnog
programa, navedenog u datoteci pozovi.cpp:

500

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;
}

Ovakvo rjeenje radi dobro, ako se ne bunimo na to to smo morali deklarirati


nepotrebnu funkciju NekoristenaFunkcija(), samo zato da bismo instancirali
predloak. Posljednja verzija standarda jezika podrava eksplicitnu instantaciju
predloaka, pa programer moe sam odrediti koje sve predloke treba generirati. Svaki
put kada se naie na potrebu za time da se instancira predloak za odreeni tip, u
datoteku gdje je predloak definiran ubaci se dodatna naredba za instantaciju. Tako bi u
naem sluaju datoteka kompl.cpp izgledala ovako:
kompl.cpp
// definicija predloka je ista, samo emo navesti naredbe
// za eksplicitnu instantaciju predloka
template
template
template
template

class Kompleksni<double>;
class Kompleksni<int>;
ostream &operator <<(ostream &,Kompleksni<double>);
ostream &operator <<(ostream &,Kompleksni<int>);

Pri tome valja primijetiti da ako je predloak parametriziran korisnikim tipom, na


mjesto gdje se instancira predloak potrebno je ukljuiti deklaraciju tipa. Kako bi takav
pristup mogao dovesti do toga da se u jednu datoteku ukljuuju mnoga zaglavlja,
pogodnije je instancu predloka za svaki poseban tip smjestiti u zasebnu datoteku.
Definicija predloka se tada stavlja u posebnu datoteku koja se ukljuuje u svaku .cpp
datoteku gdje se obavlja instantacija. U naem sluaju to bi znailo da se datoteka
kompl.h ne mijenja (jer je to datoteka koja se ukljuuje u druge datoteke iz kojih se
predloak poziva). Iz datoteke kompl.cpp bismo tada izbacili naredbe za eksplicitnu
instantaciju predloaka i naredbe za ukljuivanje drugih zaglavlja, a samo datoteku
bismo preimenovali u datoteku kompldef.h. Instantacije Kompleksni<double> i
Kompleksni<int> bismo smjestili u zasebne datoteke, na primjer kompldbl.cpp i
komplint.cpp:

501

kompldbl.cpp
#include <iostream.h>
#include "kompl.h"
#include "kompldef.h"

// za instantaciju operatora <<


// deklaracija predloka klase
// definicija predloka

template class Kompleksni<double>;


template ostream &operator <<(ostream &,Kompleksni<double>);

komplint.cpp
#include <iostream.h>
#include "kompl.h"
#include "kompldef.h"

// za instantaciju operatora <<


// deklaracija predloka klase
// definicija predloka

template class Kompleksni<int>;


template ostream &operator <<(ostream &,Kompleksni<int>);

17.5. Primjer raspodjele deklaracija i definicija u vie


datoteka
Ilustrirajmo opisani pristup preko datoteka zaglavlja programom u kojem traimo
presjek krunice i pravca. U programu emo deklarirati i definirati tri klase: Tocka,
Pravac i Kruznica. Klasa Tocka posluit e nam kao osnovna klasa za izvedenu
klasu Kruznica, a takoer emo ju iskoristiti u konstruktoru klase Pravac. Glavnu
funkciju emo pohraniti u datoteku poglavar.cpp, dok emo klase i njihove
funkcijske lanove definirati u zasebnim datotekama zaglavlja (.h), odnosno izvornog
kda (.cpp). Funkciju za ispis presjecita smjestiti emo u datoteke presjek.h,
odnosno presjek.cpp. Na slici 17.1 shematski je prikazana struktura programa s
oznaenim ukljuivanjima.
Klasu Tocka deklarirat emo u datoteci tocka.h, a njene lanove emo definirati
u tocka.cpp:
tocka.h
#ifndef TOCKA_H
#define TOCKA_H

// smisao ovoga e biti objanjen


// iza cjelokupnog kda

class Tocka {
public:
Tocka(double x = 0, double y = 0);
double x() const { return x_koord; }
double y() const { return y_koord; }

502

void pomakni(double x, double y);


protected:
double x_koord;
double y_koord;
};
#endif

tocka.cpp
#include "tocka.h"
Tocka::Tocka(double x, double y) {
x_koord = x;
y_koord = y;
}

void Tocka::pomakni(double x, double y) {


x_koord = x;
y_koord = y;
}

503

tocka.h

pravac.h

kruznica.h

#include "tocka.h"
// deklaracija
// klase Tocka

// deklaracija
// klase Pravac

tocka.cpp

#include "tocka.h"
#include "pravac.h"
// deklaracija i
// definicija
// klase Kruznica

pravac.cpp

#include "tocka.h"

#include "pravac.h"

// definicije
// lanova klase
// Tocka

// definicije
// lanova klase
// Pravac

presjek.h
poglavar.cpp
#include
#include
#include
#include

"tocka.h"
"pravac.h"
"kruznica.h"
"presjek.h"

int main() {
//...
}

#include "pravac.h"
#include "kruznica.h"
// deklaracije
// funkcija iz
// presjek.cpp

presjek.cpp
#include "presjek.h"
// definicije
// funkcija

Slika 17.1. Struktura ukljuivanja u primjeru sloenog programa

Uoimo kako su podrazumijevani argumenti konstruktora navedeni samo u deklaraciji


klase, ali ne i u definiciji konstruktora.
Klasu Pravac emo deklarirati (zajedno s definicijama umetnutih funkcijskih
lanova) u datoteci pravac.h, a njene lanove emo definirati u datoteci pravac.cpp.
Konstruktor klase Pravac kao argumente prihvaa toku i prirast u smjeru osi x,
odnosno osi y. Stoga je u deklaraciju klase neophodno ukljuiti i deklaraciju klase
Tocka iz datoteke pravac.h.
pravac.h
#ifndef PRAVAC_H
#define PRAVAC_H

504

#include "tocka.h"
class Pravac {
public:
Pravac(Tocka t, double dx,
double ka() const { return
double kb() const { return
double kc() const { return
private:
double a;
double b;
double c;
};

double dy);
a; }
b; }
c; }

#endif

pravac.cpp
#include "pravac.h"
Pravac::Pravac(Tocka t, double dx, double dy) {
a = dy;
b = -dx;
c = dx * t.y() - dy * t.x();
}

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

Funkciju PresjecistaKruzniceIPravca za raunanje presjecita krunice i pravca


definirat emo u datoteci presjek.cpp, a njenu emo deklaraciju pohraniti u datoteku
presjek.h:
presjek.h
#ifndef PRESJEK_H
#define PRESJEK_H
#include "pravac.h"
#include "kruznica.h"
enum VrstaPresjeka {NijednoPresjeciste,
PravacDiraKruznicu,
DvaPresjecista};
VrstaPresjeka PresjecistaKruzniceIPravca(const Kruznica &k,
const Pravac &p, Tocka &t1, Tocka &t2);
#endif

Pobrojenje VrstaPresjeka se koristi za definiciju povratnog tipa funkcije za


raunanje presjeka. Ta vrijednost oznaava sijee li uope pravac krunicu, da li ju
samo (njeno) dira ili ju sijee u dvije toke.
presjek.cpp
#include "presjek.h"
#include <math.h>
VrstaPresjeka PresjecistaKruzniceIPravca(const Kruznica &k,
const Pravac &p, Tocka &t1, Tocka &t2) {
if (p.kb()) {
// ako pravac nije okomit
double a_b = p.ka() / p.kb();
double c_b = p.kc() / p.kb();
double A = 1. + a_b * a_b;
double B = 2. * (a_b * c_b + a_b * k.y0() - k.x0());
double C = k.x0() *- k.x0() + k.y0() * k.y0() k.r() * k.r() + c_b * (c_b + 2 * k.y0());
double diskr = B * B - 4 * A * C;
if (diskr < 0)
// nema presjecista
return NijednoPresjeciste;
else if (diskr == 0) {
// pravac dira kruznicu

506

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!
}
}

Konano evo i datoteke poglavar.cpp. Funkcija main() e stvoriti krunicu zadanu


koordinatom sredita i polumjerom i pravac zadan tokom i prirastima u x i y smjeru.
Nakon toga e se pozvati funkciju PozoviPaIspisi() za izraunavanje i ispis
presjecita pravca i krunice:
poglavar.cpp
#include
#include
#include
#include
#include

<iostream.h>
"tocka.h"
"pravac.h"
"kruznica.h"
"presjek.h"

static void PozoviPaIspisi(const Kruznica &k,


const Pravac &p) {
Tocka t_pr1;
// toke za pohranjivanje
Tocka t_pr2;
// koordinata presjecita
switch (PresjecistaKruzniceIPravca(k, p, t_pr1, t_pr2)) {
case 0:
cout << "Nema presjecista!" << endl;
break;
case 1:
cout << "Pravac dira kruznicu u tocki T("
<< t_pr1.x() << "," << t_pr1.y() << ")";
break;
case 2:
cout << "Pravac sijece kruznicu u tockama T1("
<< t_pr1.x() << "," << t_pr1.y() << ") i T2("
<< t_pr2.x() << "," << t_pr2.y() << ")" << endl;
break;

507

}
}
int main() {
Tocka t0(1., 1.);
double r = 5.;
Kruznica k(t0, r);
Pravac p(t0, 1., 1.);

//
//
//
//
//

postavljamo toku
polumjer krunice
definiramo krunicu
pravac kroz sredite krunice
pod 45 stupnjeva

PozoviPaIspisi(k, p);
return 0;
}

Funkcija PozoviPaIspisi() je statika, to znai da ona nije vidljiva izvan datoteke


poglavar.cpp. To i ima smisla, jer je ta funkcija uvedena samo zato da se dio kda
koji poziva funkciju za raunanje presjeka i ispisuje rezultate, izdvoji zasebno i na taj
nain istakne. Poeljno je da ime takve funkcije ne zagauje globalno podruje imena.
U svim datotekama zaglavlja paljivi itatelj je sigurno primijetio pretprocesorske
naredbe #ifndef-#define-#endif. Na primjer, u datoteci tocka.h su sve
deklaracije i definicije smjetene unutar naredbi
#ifndef TOCKA_H
#define TOCKA_H
// deklaracija i definicije
#endif

Pomou ovakvog ispitivanja izbjegava se mogua viekratna deklaracija klase. Na


primjer, u gornjem programu deklaracija klase Tocka ukljuena je modul
poglavar.cpp, jer se u glavnoj funkciji generira objekt te klase. Istovremeno, klasa
Tocka mora biti poznata deklaraciji klase Pravac, jer se objekt klase Tocka prenosi
kao argument njegovom konstruktoru. Kako je deklaracija klase Pravac takoer
ukljuena u glavni modul, a naredba #include obavlja jednostavno umetanje teksta, u
glavnom modulu e se deklaracija klase Tocka pojaviti dva puta: jednom izravno,
ukljuivanjem datoteke tocka.h, a drugi puta posredno preko ukljuivanja datoteke
pravac.h (slika 17.2). Na ovo e prevoditelj prijaviti kao pogreku, bez obzira to su
one potpuno identine). Slina situacija je i kod klase Kruznica. Paljivim odabirom
rasporeda ukljuivanja bi se ova pogreka mogla ukloniti, meutim to iziskuje preveliki
intelektualni napor i mo kombiniranja daleko elegantnije rjeenje jest uvjetno
prevoenje pomou gornjeg sklopa naredbi.
Kako funkcionira navedeni sklop? Pri prvom nailasku na pretprocesorsku naredbu
za ukljuivanju datoteke tocka.h, makro ime TOCKA_H nije definiran, pa se prevodi
kd izmeu naredbi #ifndef i #endif. Unutar bloka #ifndef-#endif nalazi se
naredba
#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"
//...

Slika 17.2. Dvokratna deklaracija klase Tocka u


glavnom modulu (izdvojeni detalj sa slike 17.1)

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.
Makro ime moe biti proizvoljno, ali je uobiajeno da se uskladi s imenom
datoteke, osim to se toka ispred nastavka .h ili .hpp zamijeni znakom
podcrtavanja.
Time se znaajno smanjuje mogunost da se neko ime upotrebi u razliitim datotekama
zaglavlja.
Postupak izluivanja datoteka zaglavlja esto zna za poetnika biti mukotrpan
posao. Standard jezika ne specificira niti jedno pravilo glede organizacije kda koje bi
prisiljavalo programera da ga se dri. Stoga taj postupak iziskuje dosta discipline i sva
odgovornost lei na programeru; za one manje marljive, u [Horstmann96] dan je kd
programa za automatsko generiranje datoteka zaglavlja, pa ko voli, nek izvoli.

17.6. Povezivanje s kdom drugih programskih jezika


Jezik C++ jest dobar, ali ne toliko dobar da bi ga trebalo koristiti pod svaku cijenu za
rjeavanje svih problema. Na primjer, on ne podrava vektor-operacije na strojevima
koji imaju za to posebno optimizirano sklopovlje, no FORTRAN to ima. Zbog toga je
prirodno rjeenje napisati dio programa koji koristi takve operacije u FORTRAN-u, a
zatim to povezati s C++ kosturom koji primjerice, ostvaruje prikladno Windows suelje.
Takoer, mnogi programeri posjeduju ve znatne megabajte izvornog C kda.
Naposljetku taj je jezik jo uvijek (ali ne zadugo, hehehe...) najrasprostranjeniji jezik

509

(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.
17.6.1. Poziv C funkcija iz C++ kda

Standardom je definiran nain povezivanja C++ i C kda. Jednostavna deklaracija Cfunkcije specifikatorom extern nee puno pomoi, jer se njome samo naznauje da je
funkcija definirana izvan datoteke izvornog kda, ali se podrazumijeva da ta funkcija
odgovara specifikacijama jezika C++. Funkcije u jeziku C++ razlikuju se od C-funkcija
po mnogim svojstvima, a jedno izmeu njih je preoptereenje imena funkcija. Prilikom
prevoenja kda, C++ prevoditelj e pohraniti imena funkcija u objektni kd prema
posebnim pravilima, da bi mogao pratiti njihovo preoptereenje. Taj postupak se naziva
kvarenje imena (engl. name mangling). Da bi poveziva mogao razlikovati razliite
verzije funkcija za razliite parametre, prevoditelj e u objektni kd na naziv funkcije
nalijepiti jo dodatne znakove kojima e oznaiti parametre koje ta funkcije uzima.
Nain na koji to pojedini prevoditelj radi se razlikuje ovisno o proizvoau i nije
definiran standardom.
eli li se u C++ kd ukljuiti C-funkciju, iza kljune rijei extern treba navesti
"C" tip povezivanja, ime se prevoditelju daje na znanje da za tu funkciju treba
obustaviti kvarenje imena:

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" void NekaCFunkcija(char *poslanica);

Mogue je ukljuiti i nekoliko funkcija istovremeno, tako da se funkcije iza


extern "C" navedu u vitiastim zagradama:
extern "C" {
float funkcijaCSimplex(int);
int rand();
void srand(unsigned int seed);

// standardne C-funkcije
// za generiranje
// sluajnih brojeva

tovie, sljedeim ukljuivanjem mogu se C datoteke zaglavlja pretvoriti u C++


datoteke zaglavlja:
extern "C" {
#include "mojeCfun.h"
}

Mnogi C++ prevoditelji se isporuuju sa standardnim bibliotekama koje u datotekama


zaglavlja C biblioteka imaju ukljuenu ovakvu naredbu koja omoguava vrlo
jednostavan poziv tih funkcija u C++ programima. Pri tome se koristi mogunost
uvjetnog prevoenja pretprocesorski simbol _ _cplusplus je definiran ako se
provodi prevoenje u C++ modu, u kojem se sluaju tada prevodi i extern "C"
naredba koja deklarira sve funkcije iz biblioteke sa C povezivanjem.
#ifdef _ _cplusplus
extern "C" {
#endif
// slijede deklaracije...
#ifdef _ _cplusplus
}
#endif

Valja primijetiti da direktiva extern "C" specificira samo konvenciju za povezivanje,


ali ne utjee na semantiku poziva funkcije. To znai da se za funkciju deklariranu kao
extern "C" i dalje primjenjuju pravila provjere i konverzije tipa kao da se radi o C++
funkciji. Ta su pravila stroa nego pravila za jezik C.
Budui da su tipovi podataka ugraeni u jezik C++ potpuno jednaki tipovima u
jeziku C, argumenti C funkcijama se mogu prenositi izravno, bez pretvaranja zapisa
podataka u drugaiji format. S drugim jezicima to nije sluaj: na primjer, u Pascalu se
znakovni nizovi pamte tako da se u prvi bajt niza upie njegova duljina iza koje slijede
sami znakovi. Ako se znakovni niz alje iz C++ programa u Pascal program, potrebno je
pretvoriti niz u Pascal format.

511

Napomenimo jo da je mogue eksplicitno deklarirati C++ povezivanje pomou


rijei extern "C++", no kako je to podrazumijevano povezivanje, takva deklaracija se
gotovo nikada ne koristi.
17.6.2. Ukljuivanje asemblerskog kda

Asemblerski jezik je najnii programski jezik koji omoguava izravno rukovanje


memorijskim lokacijama. On je praktiki jednak strojnim instrukcijama, osim to svaka
strojna instrukcija ima svoj mnemonik u obliku kratice na engleskom jeziku, na primjer:
mov move, asr arithmetic shift right, psh push. Kako smo vidjeli, jezik C++ prua
neke operacije koje su vrlo bliske ili identine asemblerskim naredbama, kao to su
bitovni operatori |, &, <<. Pa ipak, ponekad niti to nije dovoljno postoje aplikacije u
kojima je vrijeme izvoenja dijela kda od presudnog znaaja. Tipian primjer za to bi
mogao biti program koji prima podatke s nekog vanjskog ureaja, te ih odmah mora
obraditi. Program sam po sebi ne mora biti brz, ali kada podatak naie, obradu tog
podatka treba provesti u to kraem moguem roku kako bi sistem bio spreman
prihvatiti sljedei podatak koji e doi s vanjskog ureaja. elimo li dobiti najveu
moguu brzinu obrade, morat emo taj odsjeak napisati u asembleru.
ANSI/ISO standard omoguava jednostavno izravno ukljuivanje asemblerskog
kda u izvorni kd pisan u jeziku C++ pomou kljune rijei asm. Sintaksa je oblika
asm ( znakovni_niz );

pri emu je znakovni_niz asemblerska naredba omeena navodnicima. Sintaksa asm


naredbe za pojedine prevoditelje esto odstupa od gornjeg oblika, tako da je neophodno
konzultirati pripadajui prirunik.
Valja naglasiti da se asemblerske naredbe razlikuju za razliite familije procesora:
asembler za Intelov Pentium procesor e sadravati drugaije naredbe nego asembler za
Motoroline procesore. tovie, ak i procesori iste familije (na primjer Intel 8086,
80286 ili Pentium) ne moraju imati iste asemblerske naredbe, jer se razvojem procesora
poveava skup podranih instrukcija. Ipak, obino postoji kompatibilnost unatrag, to
znai da e asemblerski kd pisan za i8086 vrtiti na svim novijim Intelovim
procesorima.
Koritenje asemblerskog jezika iziskuje dosta iskustva i opreza, budui da on
omoguava izravno dohvaanje memorijskih lokacija to moe dovesti do prekida rada
raunala. Stoga poetniku ne preporuujemo riskantna zaletavanja u to podruje.
Zbog navedenih razloga ovdje nee biti dan niti jedan konkretan primjer asemblerskog
kda. elite li se ipak upustiti u povezivanje C++ i asemblerskog kda, najzgodnije je
potraiti gotove primjere u specijaliziranim knjigama ili asopisima. Takoer, kao izvor
asemblerskog kda na kojemu se moete uiti mogu posluiti programi za simboliko
pronalaenje pogreaka koji omoguavaju prikaz izvedbenog kda u obliku
asemblerskih mnemonika. Pregledom takvog kda, iskusni programer e uoiti
redundantne operacije umetnute tijekom prevoenja/povezivanja te e ih znati skresati
u vremenski kritinim dijelovima kda.

512

513

18. Ulazni i izlazni tokovi


Tee i tee, tee jedan slap;
to u njem znai moja mala kap?
Dobria Cesari: Slap

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

Slika 18.1. Enkapsulacija ulazno-izlaznog toka

Uz pojam ulazno-izlaznih tokova usko je povezano i meupohranjivanje (engl.


buffering). Naime, na neke vanjske jedinice podaci se ne zapisuju pojedinano, bajt po
bajt, ve u blokovima. Tako se podaci koji se alju na disk prvo pohranjuju u
meuspremnik (engl. buffer). Kada se meuspremnik napuni, cijeli se sadraj
meuspremnika poalje disku koji ga snimi u odgovarajui sektor na disku.
Meuspremnik se pri tome isprazni i spreman je za prihvat novog niza podataka. Kod
uitavanja podataka s diska, prvo se cijeli sektor s podacima uita u meuspremnik, a
potom se pojedini podaci itaju iz meuspremnika.
Postupak pranjenja meuspremnika je automatiziran, tako da o njemu programer
ne mora voditi rauna. Meutim, prilikom ispisa s meupohranjivanjem redovito
posljednji podaci koji se ispisuju ne popune meuspremnik bez naknadne intervencije
programa oni bi ostali u meuspremniku, ekajui priliku da se meuspremnik
eventualno popuni te da i oni budu prebaeni na vanjsku jedinicu. Da bi se osigurao
zapis i tih podataka, neophodno je isprazniti (engl. flush) meuspremnik. Iako se
meuspremnik prazni automatski prilikom zatvaranja toka, zbog sigurnosti je ponekad
poeljno obaviti ga runo.

515

18.2. Biblioteka iostream


iostream biblioteka sadri dvije usporedne obitelji klasa (slika 18.2):

1. Osnovnu klasu streambuf i iz nje izvedene klase, kojima je zadatak osigurati


meupohranu podataka pri uitavanju i ispisu podataka.
2. Osnovnu klasu ios i iz nje izvedene klase, koje se koriste za formatirano uitavanje i
ispis podataka. Klasa ios sadri i pokaziva na streambuf klasu, to omoguava
meupohranu podataka za objekte klase ios, odnosno bilo koje iz nje izvedene klase.
streambuf

ios

istream

ostream

ifstream

ofstream

Slika 18.2. Pojednostavljeni prikaz stabla


nasljeivanja klasa u iostream biblioteci

Iz klase ios (poetna slova od Input-Output Stream) izravno su izvedene klase


istream i ostream. Klasa istream se koristi kao osnovna klasa za ulazne tokove,
dok se klasa ostream koristi kao osnovna klasa za izlazne tokove. One se mogu
proiriti operacijama za ispis i uitavanje korisniki definiranih tipova. Biblioteka
iostream takoer podrava operacije za ispis u datoteku i itanje iz datoteke. Klasa
ofstream, izvedena iz klase ostream, podrava ispis ugraenih tipova podataka.
Klasa ifstream, izvedena iz klase istream, podrava uitavanje ugraenih tipova
podataka iz datoteke. Klasa fstream podrava i itanje i pisanje istodobno. Kako se
sve klase za ulazne i izlazne tokove se izvode iz klasa u iostream biblioteci, sve one
imaju barem jednu od klasa ios ili streambuf kao osnovnu. Sve ove klase obrauju
podatke tipa char, ali imaju svoje pandane za znakove tipa wchar_t u klasama wios,
wistream, wostream, wifstream, wofstream i wstreambuf.
Valja napomenuti da je stablo nasljeivanja u iostream biblioteci na slici Error!
Reference source not found. prikazano bitno pojednostavnjeno tamo su definirane i
druge klase, a veza meu njima je bitno sloenija jer neke od tih klasa viestruko
nasljeuju razne osnovne klase.
Pokretanjem svakog C++ programa u koji je ukljuena iostream.h datoteka
zaglavlja, automatski se stvaraju i inicijaliziraju etiri objekta:

cin koji obrauje unos sa standardne ulazne jedinice tipkovnice,

516

cout koji obrauje ispis na standardnu izlaznu jedinicu zaslon,


cerr koji obrauje ispis na standardnu jedinicu za ispis pogreaka zaslon,
clog koji osigurava ispis pogreaka s meupohranjivanjem. Radi se o istim
porukama kao i u cerr toku, ali se ovaj ispis obino preusmjerava u datoteku.
Za podatke tipa wchar_t na raspolaganju su odgovarajui objekti win, wout, werr i
wlog za njih vrijede sva razmatranja koja emo navesti za objekte cin, cout, cerr,
odnosno clog.

18.3. Stanje toka


Svaki tok, bilo da se radi o izlaznom ili ulaznom toku, ima pripadajue stanje (engl.
state). Pogreke nastale tijekom izluivanja sa toka ili umetanja podataka na tok mogu
se detektirati i prepoznati ispitujui stanje toka. Sva stanja su obuhvaena pobrojenjem
definiranim u klasi ios, a imaju nazive goodbit, badbit, failbit i eofbit. Stanje
toka je pohranjeno u bitovnu masku iostate u klasi ios kao kombinacija gornjih
vrijednosti. iostate se moe oitati pomou funkcijskog lana rdstate(). Meutim,
radi lake detekcije stanja definirana su etiri funkcijska lana (tipa bool) za itanje
stanja pojedinih bitova:
1. eof() vraa true, ako je s toka izluen znak za kraj datoteke.
2. bad() vraa true, ako operacija nije uspjela zbog neke nepopravljive pogreke
(npr. oteene datoteke).
3. fail() vraa true, ako operacija nije bila uspjeno obavljena zbog bilo kojeg
razloga.
4. good() vraa true ako niti jedan od gornjih uvjeta nije true, tj. ako je operacija
potpuno uspjeno izvedena.
Razlika izmeu bad() i fail() (odnosno badbit i failbit) je suptilna i za veinu
primjena nije neophodno razlikovati ta dva lana.
Takoer je definiran i funkcijski lan clear() koji postavlja stanje toka.
Podrazumijevana vrijednost argumenta tog funkcijskog lana je stanje ios::goodbit,
pa ako se ne navede neki drugi argument, clear() e izbrisati stanje pogreke u toku.
Klasa ios posjeduje operator konverzije koji konvertira objekt klase ios u tip
void *. Taj operator vraa nul-pokaziva ako je u stanju toka postavljen failbit,
badbit ili eofbit. Inae, vraa se neki pokaziva koji nije nul-pokaziva (vrijednost
tog pokazivaa ne moe koristiti ni za to drugo osim za testiranje). To omoguava
testiranje toka: ako se tok nae u uvjetu if ili while naredbe, pozvat e se taj operator
konverzije, te e uvjet biti ispunjen ako je tok u stanju goodbit. Na primjer:
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.

18.4. Ispis pomou cout


cout je globalni objekt klase ostream koji se automatski stvara izvoenjem programa
u iji je izvorni kd ukljuena datoteka iostream.h. On usmjerava ispis podataka na
zaslon raunala pomou operatora <<. Taj operator je preoptereen za sve ugraene

tipove podataka, a korisnik ga moe po potrebi preopteretiti za korisniki definirane


tipove. Osim toga, mogue je formatirati ispis podataka, poravnati stupce u ispisu,
ispisati podatke u dekadskom ili heksadekadskom obliku, skuhati kavu...
18.4.1. Operator umetanja <<

Ispis podataka ostvaruje se pomou izlaznog operatora <<. On se obino zove


operatorom umetanja (engl. insertion operator), jer se njime podatak umee na izlazni
tok. U sutini se radi o operatoru za bitovni pomak ulijevo koji je preoptereen tako da
s lijeve strane kao operand prihvaa referencu na objekt tipa ostream. S desne strane
se moe nai bilo koji tip podatka: char, short, int, long int, char *, float,
double, long double, void *. Tako su sljedee operacije standardno podrane:
cout
cout
cout
cout

<<
<<
<<
<<

9.3 <<
'C' <<
666 <<
"Hvala

endl;
endl;
endl;
bogu, jo uvijek sam ateist";

//
//
//
//

float
char
int
char *

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

Reenica koju je izrekao Luis Buuel (19001983), Le Monde, 1959. godine.

518

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 << 1001 << " no";

Ova naredba e biti interpretirana kao:


(cout.operator<<(1001)).operator<<(" no");

Pritom e podaci biti ispisani redoslijedom kako su navedeni, s lijeva na desno.


Operator << kotira dovoljno nisko u hijerarhiji operatora, tako da aritmetike izraze
ije rezultate elimo ispisati najee nije neophodno pisati u zagradama:
cout << 5 + 4 << endl;
cout << ++i << endl;

// ispisat e 9
// ispisat e i uvean za 1

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;

ispisat e sljedei, na prvi pogled neoekivani rezultat:


Vei od brojeva 5 i 7 je 0

Razlog ovakvom rezultatu shvatit emo im bacimo pogled na tablicu operatora u


poglavlju 2 i uoimo da uvjetni operator ima nii prioritet od operatora umetanja. To
znai da e naredbu prevoditelj interpretirati jednako kao da je pisalo:
(cout << (prvi > drugi)) ? prvi : drugi;

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

Ako se u naredbama za ispis navode izrazi, dobra je navika omeiti ih


zagradama.
Primijenjeno na gornji primjer to znai da smo naredbu za ispis trebali napisati kao:
cout << ((prvi > drugi) ? prvi : drugi);

Neki prevoditelji e prilikom prevoenja dati upozorenje izostave li se zagrade.


Zadatak. Pokuajte predvidjeti to e se ispisati na zaslonu izvoenjem sljedeih
naredbi:
int i =
cout <<
cout <<
cout <<

7, j = 0;
i * j;
(i && j);
i && j;

18.4.2. Ispis korisniki definiranih tipova

Operator umetanja se moe preoptereivati za korisniki definirane tipove podataka.


Pritom treba kao prvi argument operatorske funkcije navesti referencu na objekt tipa
ostream, a kao drugi argument navodi se eljeni tip. Povratni tip mora biti referenca na
objekt tipa ostream kako bi se omoguilo ulanavanje operacija ispisivanja.
Preoptereenje operatora << ilustrirat emo primjerom ispisa kompleksnih brojeva.
Pretpostavimo da smo za kompleksne brojeve deklarirali klasu Kompleksni:
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;}
//...
};

Operatorsku funkciju za ispis kompleksnog broja moemo definirati na sljedei nain:


ostream& operator<<(ostream &os, Kompleksni &z) {
os << z.Re();
if (z.Im() >= 0) os << "+";

520

return os << z.Im() << "i";


}

U ispravnost funkcije uvjerit emo se nakon izvoenja sljedeih naredbi:


Kompleksni z(2., -8.);
cout << z << endl;

Na zaslonu e se ispisati na kompleksni broj:


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.
18.4.3. Ostali lanovi klase ostream

Ispis pojedinanih znakova mogu je i pomou funkcijskog lana put(). On kao


argument prihvaa znak tipa char, a vraa referencu na objekt tipa ostream.
Meutim, kako operator << sasvim zadovoljava potrebe ispisa znakova, put() se
rijetko koristi.
Slina je situacija sa funkcijskim lanom write() namijenjenog za ispis
znakovnih nizova. On kao prvi argument prihvaa znakovni niz char *, a drugi
argument je broj znakova koje treba ispisati. Kao rezultat, i ovaj lan vraa referencu na
pripadajui objekt tipa ostream.
Funkcijski lan flush() predvien je za pranjenje meuspremnika. Njegovim
pozivom se svi podaci koji se trenutno nalaze u meuspremniku alju na vanjsku
jedinicu. Moe se pozvati standardno, kao svaki funkcijski lan:
cout.flush();

ili preko manipulatora, koji se pomou operatora umetanja ubacuje u objekt cout:
cout << flush;

Mehanizam manipulatora je detaljno objanjen u odsjeku 18.6.6.

521

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.

18.5. Uitavanje pomou cin


Uitavanje podataka je slino ispisu podataka. Pokretanjem programa u koji je
ukljuena datoteka zaglavlja iostream.h automatski se stvara globalni objekt cin
klase istream. Upis se obavlja pomou operatora >>. U klasi istream definirana je
preoptereena inaica operatora za sve ugraene tipove podataka, a dodatnim
preoptereenjem moe se ostvariti uitavanje korisniki definiranih tipova.
18.5.1. Uitavanje pomou operatora >>

Uitavanje podataka ostvaruje se pomou ulaznog operatora >>. On se obino zove


operatorom izluivanja (engl. extraction operator), jer se njime podatak izluuje s
izlaznog toka. U sutini se radi o operatoru za bitovni pomak udesno koji je
preoptereen tako da s lijeve strane kao operand prihvaa referencu na objekt tipa
istream. S desne strane se moe nai bilo koji tip podatka za koji je ulazni tok
definiran: char, short, int, long int, char *, float, double, long double. Tako
su uitavanja sljedeim naredbama standardno podrana:
int i;
long l;
float f;
double d;
cin
cin
cin
cin

>>
>>
>>
>>

i;
l;
f;
d;

//
//
//
//

int
long int
float
double

Uspjenim uitavanjem podatka, operator izluivanja vraa kao rezultat referencu na


objekt tipa istream. Zbog toga se naredbe za uitavanje mogu ulanavati, pa smo
gornja uitavanja mogli napisati i u jednoj naredbi:
cin >> i >> l >> f >> d;

522

Uitavanje se nastavlja od mjesta gdje je prethodno uitavanje zavrilo. Pritom se


praznine (znakovi za razmak, tabulator, novi redak, pomak papira) standardno ignoriraju
i one slue za odvajanje pojedinih podataka. To znai da smo sve ulazne podatke za
gornji primjer mogli napisati u jednom retku, razmaknute prazninama:
24

102345

34.56

3.4567e95

a tek potom pritisnuti tipku za unos (Enter).


Ako uitavanje ne uspije, operator e tok postaviti u stanje failbit. Takva
situacija e nastupiti ako se umjesto oekivanog tipa u ulaznom toku nae podatak
nekog drugog tipa. To se iskoritava kod opetovanog uitavanje podataka u petlji kada
broj podataka koje treba uitati nije unaprijed poznat. Donji primjer e uitavati cijele
brojeve dok se ne unese znak, kada se tok postavlja u stanje failbit. Petlja tada
zavrava:
int i;
while ((cin >> i) != 0)
cout << "Upisan je broj " << i << endl;
cout << "Fajrunat!" << endl;

Valja primijetiti da e izvoenje petlje prekinuti i decimalna toka u eventualno


upisanom realnom broju. Na primjer, upiemo li sljedei niz brojeva:
12 27 34.2 5

izvoenjem gornje petlje ispisat e se:


Upisan je broj 12
Upisan je broj 27
Upisan je broj 34
Fajrunat!

Kao to vidimo, izvoenje petlje je prekinuto na decimalnoj toki. Budui da naredba


za uitavanje oekuje samo cijele brojeve, svaki znak razliit od neke decimalne
znamenke program e shvatiti kao graninik. Stoga e uitati samo dio treeg broja
lijevo od decimalne toke. U sljedeem prolazu petlje pokuat e uitati decimalnu
toku, ali e pokuaj njenog uitavanja u cjelobrojnu varijablu zavriti neuspjehom te
e se izvoenje petlje prekinuti.
Zadatak. Isprobajte to e se dogoditi ako u gornjem primjeru broj unesete u
heksadekadskom formatu (npr. 0xff).

Naravno, da smo u gornjoj petlji uitavali realne brojeve (float ili double), problema
s decimalnom tokom ne bi bilo.

523

18.5.2. Uitavanje korisniki definiranih tipova

Poput operatora umetanja, i operator izluivanja >> moe se preopteretiti za korisniki


definirane tipove podataka. Kao prvi argument operatorske funkcije se navodi referenca
na objekt tipa istream, a kao drugi referenca na objekt koji se uitava referenca je
nuna kako bi operator izluivanja mogao promijeniti vrijednost objekta koji se uitava.
Povratni tip mora biti referenca na objekt tipa istream kako bi se omoguilo
ulanavanje operacija uitavanja.
Preopteretimo operator izluivanja tako da se njime mogu uitavati kompleksni
brojevi (klasu smo definirali u primjeru preoptereenja operatora umetanja, na str. 519):
istream& operator>>(istream &is, Kompleksni &z) {
if (!is) return is;
// ako je tok ve u stanju
// pogreke, operacija se prekida
double r, i;
// realni i imaginarni dio
char zn;
// za uitavanje predznaka
int imPredznak = 1;
// predznak imaginarnog dijela
is >> r >> zn;
switch (zn) {
case '-':
imPredznak = -1;
case '+':
cin >> i >> zn;
if (zn != 'i') {
// ako je prilikom unosa
// detektirana pogreka
// tok se postavlja u
// stanje pogreke:
is.clear(ios::failbit)
return is;
// i operacija se prekida
}
break;
}
z = Kompleksni(r, i * imPredznak);
return is;
}

Pretpostavlja da se kompleksni broj upisuje tono u zadanom obliku:


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.
18.5.3. Uitavanje znakovnih nizova

Budui da praznine predstavljaju graninike koji prekidaju uitavanje podatka,


uitavanje znakovnih nizova nije trivijalno. Sljedea petlja nee tekst uitati kao jedan
znakovni niz, ve e svaka rije biti niz za sebe:
char text[80];
while (cin >> text) cout << text << endl;

Za upisani tekst
Svakim danom u svakom pogledu napredujemo.

gornja petlja e ispisati niz rijei:


Svakim
danom
u
svakom
pogledu
napredujemo.

elimo li uitati znakovni niz zajedno sa prazninama, u klasi istream na raspolaganju


su funkcijski lanovi get(), getline()i read(). Funkcijski lan get() definiran je
u nekoliko preoptereenih varijanti:
1. get(char &znak) izluuje jedan znak s ulaznog toka te ga pohranjuje u varijablu
znak. Funkcijski lan kao rezultat vraa referencu na objekt klase istream. Uita li
znak za kraj datoteke, tok e se postaviti u stanje eofbit, koje moemo testirati
operatorom konverzije. elimo li uitati sve znakove utipkanog niza koristei ovaj
funkcijski lan, moemo napisati sljedei kd:
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 nulznak. 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:
while ((niz[i] = cin.get()) != -1)
if (niz[i++] == '\n')
break;
niz[i - 1] = '\0';

3. get(char *znNiz, int duljina, char granicnik = '\n') izluuje niz


znakova do najvie duljina - 1 znakova. Rezultat se smjeta na lokaciju koju
pokazuje znNiz. Izluivanje se prekida ako naleti na znak jednak argumentu
granicnik ili ako uita znak EOF. Na kraj uitanog niza uvijek e dodati zakljuni
nul-znak, a povratna vrijednost jednaka je pripadajuem objektu istream, osim ako
nije uitan niti jedan znak. itatelj sigurno nasluuje da je ovaj oblik funkcijskog
lana najpogodniji za uitavanje utipkanog teksta. Rijeimo zadatak uitavanja
utipkanog niza pomou ovog funkcijskog lana:
char niz[80];
cin.get(niz, sizeof(niz));

Budui da je u deklaraciji funkcijskog lana znak za novi redak podrazumijevani


graninik, ne trebamo ga navoditi eksplicitno kao trei argument funkciji. Uz to, sam
funkcijski lan dodaje zakljuni nul-znak na kraj uitanog niza, tako da ga ne
trebamo naknadno dodavati. Naravno, i dalje valja paziti na duljinu niza u koji se
izluuju znakovi duljina niza se moe utvrditi pomou operatora sizeof.

Funkcijski lan get(char *, int, char) ostavlja znak '\n' na toku.

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:
while (cin.get(char niz, sizeof(niz)))
//...

526

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
istream &getline(char *znNiz, int duljina,
char granicnik = '\n);

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().
lan getline(), za razliku o get(), uklanja znak '\n' s ulaznog toka.
Stoga se operacije uitavanja pomou getline() mogu ulanavati, ali
pomou get() ne.
Funkcijski lan read() takoer uitava niz znakova zadane duljine, ali se njegovo
izvoenje prekida samo u sluaju nailaska na znak EOF. Deklariran je kao:
istream &read(char *znNiz, int duljina);

Njegova primjena uglavnom je ograniena na uitavanje podataka iz datoteka.


Funkcijski lan gcount() vraa broj neformatiranih znakova uitanih funkcijskim
lanovima get(), getline() ili read(). Koristi za odreivanje duljine uitanog niza
ili za provjeru ispravnosti uitavanja.
18.5.4. Ostali lanovi klase istream

Funkcijskim lanom ignore() preskau se znakovi na ulaznom toku. On prihvaa dva


argumenta: prvi argument je broj znakova koliko ih treba preskoiti, a drugi argument je
znak kojim se preskakanje znakova moe prekinuti. Podrazumijevana vrijednost tog
znaka jest EOF. Pretpostavimo da prilikom unosa podataka elimo pohraniti samo prvih
deset utipkanih znakova u svakom retku. U tom sluaju emo uporabiti funkcijski lan
ignore():
char podaci[n][11];
for (int i = 0; i < n; i++) {
cin.get(podaci[i], 11);
cin.ignore(99, '\n');
}

// odbaci preostale znakove

527

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;
while ((znak = cin.peek()) != '\n') {
// preskae praznine:
if (isspace(znak)) cin.get(znak);
// je li prvi znak niza znamenka, predznak ili dec. toka?
else if (isdigit(znak) || znak == '+' || znak == '-' ||
znak == '.') {
int predznak = (znak == '-') ? -1 : +1;
// izluuje predznak s toka
if (znak == '-' || znak == '+') cin >> znak;
if (!(isdigit(znak = cin.peek()) || znak == '.')) {
cout << "Pogreka: iza predznaka mora biti "
"broj!" << endl;
break;
}
if (cin.peek() == '.') {
// izluuje decimalnu toku da bi provjerio
// slijedi li iza nje znamenka
cin >> znak;
if (!isdigit(cin.peek())) {
cout << "Pogreka: iza toke mora biti "
"broj!" << endl;
break;
}
// ako je OK, vraa decimalnu toku na tok
cin.putback(znak);
}
cin >> broj;
cout << (broj * predznak) << endl;
if (!isspace(cin.peek())) {

528

cout << "Pogreka: iza broja mora biti razmak!"


<< endl;
break;
}
}
// ako je prvi znak niza slovo ili podcrtavanje:
else if (isalpha(znak) || znak == '_') {
int i = 0;
// izluuje pojedinano znakove sve dok je slovo,
// broj ili podcrtavanje
do {
rijec[i++] = cin.get();
} while(isalnum(znak = cin.peek()) || znak == '_');
rijec[i] = '\0';
cout << rijec << endl;
if (!isspace(cin.peek())) {
cout << "Pogreka: iza rijei mora biti razmak!"
<< endl;
break;
}
}
// sve ostalo "guta":
else
cin >> znak;
}

Funkcijski lan peek() poziva se u gornjem primjeru na nekoliko mjesta, prilikom


ispitivanja sljedeeg znaka na toku. On nema argumenata, a kao rezultat vraa kd
(tipa int) sljedeeg znaka na toku.
Funkcijski lan putback() se poziva samo na jednom mjestu. Naime, nakon to se
uita predznak, treba provjeriti slijedi li decimalna toka, a iza nje znamenka. Budui da
se funkcijom peek() moe vidjeti samo jedan znak, da bismo provjerili znamenku iza
decimalne toke valja prvo izluiti decimalnu toku s toka tek tada moemo baciti
pogled na sljedei znak. Nakon to se uvjerimo da je sljedei znak znamenka, funkcijom
putback() vraamo decimalnu toku na tok, da bismo broj uitali operatorom >>.
Funkcijski lan putback() kao argument prihvaa znak (tipa char) koji treba vratiti
na tok, a vraa referencu na pripadajui objekt tipa istream.
Obratimo jo panju na to kako se provelo razlikovanje pojedinih vrsta znakova. To
je provedeno pomou funkcija deklariranih u zaglavlju ctype.h:

isspace() vraa true ako je znak naveden kao argument funkciji praznina (' '),
tabulator ('\t'), znak za povrat (engl. return, '\r'), za novi redak ('\n') ili pomak
papira ('\f').
isdigit() vraa true ako je znak naveden kao argument funkciji decimalna
znamenka ('0', '1'... '9').

529

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')

Naime, znakovi ne moraju biti na svakom raunalu sloeni po (najeem) ASCII


slijedu, za koji bi gornji uvjet odgovarao pozivu funkcije isalpha(). Dodatni
argument za koritenje gornjih funkcija su nacionalno-specifina slova ('', '',
''...), koja se ne uklapaju niti u jedan meunarodni slijed znakova.
Navedena tri funkcijska lana klase istream svoju prvenstvenu primjenu nai e
za pisanje jezinih procesora programa koji uitavaju tekst (znak po znak) napisan u
nekom simbolikom jeziku, provjeravaju sintaksu teksta te interpretiraju tekst
pretvarajui ga u interne naredbe.
Funkcijski lanovi seekg() i tellg() omoguavaju kontrolirano pomicanje du
toka. lan tellg() vraa trenutnu poziciju u toku, a seekg() se postavlja na zadanu
poziciju. Njihova primjena bit e prikazana u odsjecima koji slijede, jer se koriste
primarno za kontrolu pristupa datotekama.

18.6. Kontrola uitavanja i ispisa


Ispisi u svim dosadanjim primjerima su bili neformatirani podaci su bili pretvoreni u
niz znakova prema podrazumijevanim pravilima za pripadajui tip podataka te kao
takvi ispisani. Meutim, esto elimo kontrolirati izgled ispisa. Tipian primjer je
tablini ispis rezultata, gdje elimo poravnati podatke u pojedinim recima i posloiti ih
jedan ispod drugoga.
Veina kontrola za unos i ispis podataka deklarirana je u klasi ios. Ve smo na
poetku poglavlja spomenuli da je ta klasa osnovna za klase istream i ostream, tako
da su sve kontrole dohvatljive i izvedenih klasa. Osim toga, klasa ios sadri i
pokaziva na klasu streambuf koja je omoguava meupohranjivanje pri izluivanju
podataka s toka ili umetanju na tok. Pokaziva je mogue dohvatiti te pomou njega
podeavati svojstva meupohranjivanja.
18.6.1. Vezivanje tokova

Vezivanje tokova osigurava da se izlazni tok isprazni prije nego to pone izluivanje s
ulaznog toka (i obrnuto). Ilustrirajmo to sljedeim primjerom [Stroustup91]:

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;

Izlazni tok cout koristi meuspremnik, tako da openito ne postoji garancija da e se


poruka "Zaporka: " pojaviti na zaslonu prije nego to se eksplicitno (npr. pomou
endl) ili implicitno (zavretkom programa) isprazno izlazni tok. Sreom, izmeu
ulaznog toka cin i izlaznog toka cout uspostavljena je veza koja osigurava da se prije
svakog unosa izlazni tok isprazni. Ova veza se automatski uspostavlja prilikom
stvaranja globalnih objekata cin i cout. Takva veza postoji i izmeu izlaznih tokova
cerr, odnosno clog i ulaznog toka cin.
Pozivom funkcijskog lana tie() moe se ulazni tok vezati na bilo koji izlazni tok.
Argument u pozivu funkcije mora biti izlazni tok na koji se dotini ulazni tok vezuje, a
funkcija vraa pokaziva na izlazni tok na kojeg je ulazni tok bio prethodno vezan ili
nul-pokaziva ako ulazni tok prethodno nije bio vezan. Ako se kao argument navede 0,
prekida se veza izmeu ulaznog i izlaznog toka, a ako se tie() pozove bez
argumenata, vraa se trenutana veza. Tako su tokovi cin i cout vezani naredbom
cin.tie(&cout) koja se izvodi prije ulaska u funkciju main().
Kada je neki ostream vezan na istream, ostream se isprazni im se pokua
neto uitati s istream toka. Stoga se gornje naredbe za ispis i uitavanje interpretiraju
kao:
cout << "Zaporka: ";
cout.flush();
cin >> zaporka;

18.6.2. irina ispisa

esto elimo da podaci koji se ispisuju zauzimaju tono odreenu irinu na zaslonu,
neovisno o broju znamenaka u broju ili znakova u nizu, tako da budu poravnati po
stupcima. irinu polja za ispis ili uitavanje brojeva ili znakovnih nizova moemo
podesiti funkcijskim lanom width(). Cjelobrojni argument odreuje broj znakova
koji e ispis ili upis zauzimati. Na primjer:
cout << '>';
cout.width(6);
cout << 1234 << '<' << endl;

// postavlja irinu ispisa

Izvoenjem ovih naredbi, na zaslonu e se ispisati:


>

1234<

Podeena irina traje samo jedan ispis nakon toga ona se postavlja na
podrazumijevanu irinu. Zbog toga e izvoenje kda

531

cout << '>';


cout.width(6);
cout << 1234 << '<' << endl;
cout << '>' << 1234 << '<' << endl;

dati sljedei ispis:


> 1234<
>1234<

Ako se navede irina manja od neophodno potrebne za ispis, naredba za irinu e se


ignorirati. Zbog toga e izvoenjem naredbi
cout << '>';
cout.width(2);
cout << 1234 << '<' << endl;

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.
18.6.3. Popunjavanje praznina

Ako je irina polja za ispis zadana width() funkcijom, ira od podatka, nedostajui
znakovi bit e popunjeni bjelinama. elimo li umjesto bjelina ispisati neki drugi znak,
najjednostavnije emo to uraditi funkcijskim lanom fill(). On kao argument
prihvaa znak kojim elimo popuniti praznine, a vraa kd znaka koji je prethodno bio
koriten za popunjavanje.
U sljedeem primjeru funkcijskim lanom fill() se praznine popunjavaju
zvjezdicama:
cout << '>';
cout.width(12);
cout.fill('*');
cout << 1234 << '<' << endl;

532

Izvoenjem gornjih naredbi ispisat e se:


>********1234<

Znak za popunjavanje praznina se moe podesiti i manipulatorom setfill() koji e


biti opisan u odjeljku posveenom manipulatorima. Postavljeni znak ostaje zapamen
dok ga eventualno ponovo ne promijenimo.
18.6.4. Zastavice za formatiranje

U ios klasi definirane su i zastavice (engl. flags) za formatirani ispis i uitavanje


podataka (tablica 18.1). One omoguavaju preciznu kontrolu ispisa podataka, na
primjer poravnavanje ulijevo, udesno, ispis u heksadekadskom, oktalnom ili dekadskom
formatu i sl. Budui da je nain na koji su zastavice pohranjene ovisan o
implementaciji, za njihovo dohvaanje i mijenjanje definirani su funkcijski lanovi
setf(), unsetf() i flags().
Funkcijski lan flags() slui za dohvaanje i promjenu kompletnog seta
zastavica. U primjeni su nam zanimljiviji funkcijski lanovi setf() i unsetf() koji
slue za postavljanje, odnosno brisanje stanja pojedine zastavice. Ilustrirajmo njihovu
primjenu sljedeim primjerom:

533

Tablica 18.1. Zastavice u ios klasi

zastavica

grupa

znaenje

left

adjustfield

poravnanje ulijevo

right

adjustfield

poravnanje udesno

internal

adjustfield

predznak lijevo, ostatak desno

dec

basefield

hex

basefield

oct

basefield

fixed
scientific

floatfield

pretvorba cjelobrojnih ulaznih podataka ili ispis u


dekadskoj bazi
pretvorba cjelobrojnih ulaznih podataka ili ispis u
heksadekadskoj bazi
pretvorba cjelobrojnih ulaznih podataka ili ispis u
oktalnoj bazi
ispis brojeva s fiksnom decimalnom tokom

floatfield

ispis brojeva u znanstvenoj notaciji

showpos

ispis predznaka + pozitivnim brojevima

showpoint

ispis decimalne toke za sve realne brojeve

showbase

ispis prefiksa koji ukazuje na bazu cijelih brojeva

skipws

zamjena nekih malih slova (npr. 'e', 'x') velikim


slovima (npr. 'E', 'X')
preskakanje praznina pri uitavanju

boolalpha

umetanje i izluivanje bool tipa u slovanom obliku

unitbuf

pranjenje izlaznog toka nakon svake izlazne operacije

uppercase

// pohranjuje zateeno stanje


long zastavice = cout.flags();
cout.setf(ios::showpos);
cout.width(8);
cout << 11 << endl;

// '+' ispred pozitivnih brojeva

// poravnaj desno, predznak lijevo


cout.setf(ios::internal, ios::adjustfield);
cout.width(8);
cout << 12 << endl;
// ispis u heksadekadskom formatu
cout.setf(ios::hex, ios::basefield);
cout << 13 << endl;
// ispii bazu ispred broja
cout.setf(ios::showbase);
cout << 14 << endl;
// ispis irine 6 ...

534

cout.width(6);
// ... ispunjeno nulama ...
cout.fill('0');
// ... izmeu 0x i broja
cout.setf(ios::internal, ios::adjustfield);
cout << 15 << endl;
// oktalni ispis
cout.setf(ios::oct);
cout << 16 << endl;
// natrag na dekadski ispis
cout.setf(ios::dec, ios::basefield);
cout << 17. << endl;
// vie ne ispisivati '+'
cout.unsetf(ios::showpos);
cout << 18. << endl;
// natrag na poetno stanje
cout.flags(zastavice);

Izvoenjem gornjih naredbi dobit e se sljedei ispis:


+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

Neke zastavice su povezane u srodne grupe. Prilikom postavljanja tih zastavica


lanom setf potrebno je navesti grupu kojoj zastavica pripada. Tako je za postavljanje
zastavica iz grupa adjustfield (left, right, internal), basefield (dec, hex,
oct) i floatfield (fixed, scientific) potrebno pozvati preoptereenu inaicu
funkcije setf() koja prihvaa dva argumenta: prvi argument je zastavica, a drugi je
grupa. Brisanje tih zastavica, tj. postavljanje na podrazumijevano stanje postie se tako
da se kao prvi argument navede 0:
setf(0, ios::floatfield);

Sve zastavice ostaju aktivne sve do ponovne eksplicitne promjene. Naprotiv,


funkcijski lan width() postavlja irinu samo sljedee naredbe za ispis,
odnosno uitavanje.
Napomenimo da zastavica skipws utjee samo na ulazni tok. Podrazumijevano je ta
zastavica postavljena pa ju nije potrebno eksplicitno postavljati. Zastavica se moe
ugasiti ako je to potrebno.
18.6.5. Formatirani prikaz realnih brojeva

U prethodnom odjeljku upoznali smo kako je mogue pomou pojedinih zastavica


kontrolirati ispis i upis podataka. Formatiranje ispisa i upisa posebno je vano kod
obrade brojanih podataka. Za formatiranje realnih brojeva, naroito su korisne
zastavice scientific, fixed i showpoint te funkcijski lan precision(). Njihovu
primjenu ilustrirat emo sljedeim primjerom:
cout << 10. << endl;
cout.setf(ios::showpoint);
cout << 11. << endl;
cout << 12 << endl;
cout.setf(ios::fixed);
cout << 12e3 << endl;
cout << 123456e7 << endl;

// uvijek ispisuje decimalnu toku


// za realne brojeve
// za cijele brojeve nema efekta
// u fiksnom formatu

cout.setf(ios::scientific, ios::floatfield);
// u znanstvenom zapisu
cout << 13e4 << endl;
cout << 134 << endl;
cout.setf(ios::uppercase);
cout << 14. << endl;

// ... s velikim slovom 'E'

Izvoenje gornjih naredbi rezultirat e sljedeim ispisom:

536

10
11.0000
12
12000.000000
1234560000000.000000
1.300000e+05
134
1.400000E+01

Postavljanjem zastavice showpoint ispisuje se decimalna toka i pratee nule za sve


realne brojeve (podrazumijevano se ispisuje ukupno est znamenki); prije postavljanja
zastavice, decimalna toka se ispisuje samo ako broj ima decimalne znamenke. Kao to
vidimo iz tree naredbe za ispis broja 12, zastavica showpoint nema nikakvog utjecaja
na ispis cijelih brojeva.
Zastavica fixed forsira ispis broja u standardnom formatu s decimalnom tokom i
est znamenki desno od decimalne toke, dok zastavica scientific forsira ispis u
znanstvenom zapisu. Za cijele brojeve niti ove dvije zastavice nemaju utjecaja.
Zastavica uppercase prouzroit e ispis velikog slova 'E' ispred eksponenta u
znanstvenom formatu te velikog slova 'X' ispred heksadekadskog broja.
Standardno se realni brojevi ispisuju sa do 6 znamenki. Broj koji se ne moe
prikazati u toliko znamenki bit e ispisan u znanstvenom zapisu. Tako e naredbe
cout << 12.34 << endl;
cout << 1234567890123456. << endl;
cout << 0.00000012345 << endl;

ispisati
12.34
1.23457e+15
1.2345e-07

Funkcijski lan precision() fiksira ukupan broj znamenki koji e se prikazati.


Stavimo li ispred prethodne tri naredbe za ispis naredbu
cout.precision(2);

umjesto prethodnog ispisa, na zaslonu emo dobiti


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;

ispisati brojeve u sljedeem formatu:


12.34
1234567890123456.00
0.00
1.23e+01
1.23e+15
1.23e-07

Treba naglasiti da se prilikom odbacivanja znamenki, zadnja znamenka zaokruuje


prema uobiajenim aritmetikim pravilima.
Zadatak. Napiite funkciju koja e ispisivati realne brojeve jedan ispod drugoga s
poravnatim decimalnim tokama, kao na primjer:
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:
cout << setw(12) << setiosflag(ios::showpos) << 1.;

Standardni manipulatori navedeni su u tablici 18.2. Kao to se iz tablice vidi, postoje


manipulatori koji uzimaju argumente i oni bez argumenata. Svi manipulatori koji
prihvaaju argumente definirani su u zaglavlju iomanip.h te ako ih elimo koristiti, ne
Tablica 18.2. Standardni ulazno-izlazni manipulatori

manipulator

zaglavlje

znaenje

setw(int)
setfill(int)
dec
hex
oct
setbase(int)
setprecision(int)
setiosflags(long)
resetiosflag(long)

iomanip.h
iomanip.h
iostream.h
iostream.h
iostream.h
iomanip.h
iomanip.h
iomanip.h
iomanip.h

irina polja za ispis/uitavanje


znak za popunjavanje praznina
dekadski prikaz
heksadekadski prikaz
oktalni prikaz
postavi bazu
broj znamenki
postavlja zastavicu
brie zastavicu

flush
endl

iostream.h
iostream.h

ends
ws

iostream.h
iostream.h

prazni izlazni tok


prazni izlazni tok i umee znak za novi
redak ('\n')
umee zakljuni nul-znak na izlazni tok
ignorira praznine na ulaznom toku

smijemo zaboraviti ukljuiti to zaglavlje. Vjerujemo da e paljiviji itatelj prepoznati


znaenje manipulatora ve na temelju slinosti imena sa zastavicama i funkcijskim
lanovima klase ios koje smo opisali u prethodnim odjeljcima. Sve to smo tamo rekli
vrijedi i za ove manipulatore. Tako setw() manipulator utjee samo na sljedeu ulaznu
ili izlaznu operaciju, pa ga po potrebi treba ponavljati. Manipulatori setiosflag() i
resetiosflag() slue za postavljanje i brisanje pojedinih zastavica, slino kao
funkcijski lanovi setf() i unsetf() prihvaaju jednake argumente (interno
definiranog tipa fmtflag), ali se mogu umetati u ulazni, odnosno izlazni tok:
cout << setiosflag(ios::showpos) << 1.23 << endl;

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:
typedef ostream &(*Omanip)(ostream &);
ostream &operator<<(ostream &os, Omanip f) {
return f(os);
}

Tip Omanip je pokaziva na funkciju koja kao parametar uzima referencu na ostream i
vraa referencu na ostream. Izraz
cout << endl;

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>
ostream &cajti(ostream &os) {
time_t vrij;
time(&vrij);
tm *v = localtime(&vrij);
os << v->tm_hour << ":" << v->tm_min << ":" << v->tm_sec;
return os;
}

Manipulator koristi standardno zaglavlje time.h za itanje sistemskog sata. Varijabla


vrij je tipa time_t: u nju se smjeta broj sekundi proteklih od 01.01.1970. godine. To
se vrijeme pomou funkcije localtime pretvara u strukturu tipa tm koja sadrava sat,
minutu, sekundu, datum i jo neke podatke. Ta struktura je statika i definirana je unutar
sistemske biblioteke, a funkcija localtime vraa pokaziva na nju. Evo kako se na
manipulator moe pozvati:
cout << "Sada je tono " << cajti << endl;

540

Manipulatori takoer mogu uzimati parametre. U tom sluaju se mehanizam


manipulatora poneto komplicira. Na primjer, manipulator width() uzima cjelobrojni
parametar. width() je funkcija koja vraa objekt koji se zatim ispisuje na tok. Kako je
dosta nezgodno za svaki manipulator dodavati poseban objekt koji e obraivati
odreeni manipulator, u datoteci iomanip.h je definirana klasa omanip koja u sebi
uva adresu manipulatorske funkcije i sam parametar. Klasa je definirana predlokom,
tako da se za svaki tip predloka klasa moe instancirati bez potrebe za ponovnim
pisanjem kda. Takoer, operator << je definiran predlokom tako da prihvaa bilo
koju varijantu objekta omanip. Evo kako je sve to realizirano u standardnoj biblioteci:
template <class T>
class omanip {
public:
ostream &(*fn)(ostream &, T);
T arg;
omanip(ostream &(*f)(ostream &, T), T a) :
fn(f), arg(a) {}
};
template <class T>
ostream &operator<<(ostream &os, omanip<T> & obj) {
return obj.fn(os, obj.arg);
}
ostream &postavi_sirinu(ostream &os, int sirina);
omanip<int> width(int sirina) {
return omanip<int>(postavi_sirinu, sirina);
}

Nazivi pojedinih lanova su izmijenjeni u odnosu na standardnu biblioteku, te su


deklaracije uinjene razumljivijima, no sutina je ostala sauvana. Klasa omanip je
parametrizirana tipom koji odreuje argument manipulatora. Funkcija width()
prilikom poziva stvara privremeni objekt te klase u koji pakira adresu funkcije
postavi_sirinu i samu irinu. Taj objekt preuzima operator << koji zauzvrat poziva
funkciju iz objekta i prosljeuje joj zadani parametar. Iskoristimo to kako bismo dodali
manipulator razmaka koji na tok ispisuje onoliko razmaka koliko mu je zadano
parametrom. Evo potrebnih dodataka sustavu tokova:
#include <iostream.h>
#include <iomanip.h>
ostream &pisi_razmake(ostream &os, int koliko) {
while (koliko--) os << ' ';
return os;
}
omanip<int> razmaka(int koliko) {

541

return omanip<int>(pisi_razmake, koliko);


}

Evo kako se taj manipulator moe pozvati:


int main() {
cout << "Amo" << razmaka(5) << "!" << endl;
return 0;
}

18.7. Datoteni ispis i uitavanje


Ispis rezultata i poruka na zaslonu te unos podataka pomou tipkovnice dostatni su
samo za najjednostavnije aplikacije; u ozbiljnijim programima neophodna je mogunost
pohranjivanja podataka u datoteke na disku i mogunost uitavanja podataka iz
datoteka. Iako je princip ulaznih i izlaznih tokova u potpunosti primjenjiv i na ispis
podataka u datoteke, odnosno uitavanje iz datoteka, postoje specifinosti pri radu s
njima. Za ispis na zaslon ili upis preko tipkovnice nismo trebali stvarati nikakve objekte
neophodni globalni objekti cin, cout, cerr i clog se stvaraju automatski na poetku
svakog programa u koji je ukljuena datoteka zaglavlja iostream.h. Naprotiv, za rad
sa datotekama programer mora sam stvoriti objekt (ulazni ili izlazni tok), vezati ga na
neku datoteku, specificirati format zapisa podataka. Tim i slinim specifinostima
komunikacije s datoteke posvetiti emo se do kraja ovog poglavlja.
18.7.1. Klase ifstream i ofstream

Za rad s datotekama standardom su definirane tri klase: ifstream, ofstream i


fstream. Klasa ifstream je naslijeena od klase istream i namijenjena je
prvenstveno za uitavanje podataka iz datoteke. Klasa ofstream je naslijeena od klase
ostream i namijenjena u prvom redu za upis podataka u datoteku. Klasa fstream
omoguava i upis i uitavanje podataka u, odnosno iz datoteka. Sve su tri klase
deklarirane u datoteci zaglavlja fstream.h.
Da bismo mogli koristiti izlazne i ulazne tokove za upis u datoteke, odnosno
itanje iz datoteka, u program treba ukljuiti datoteku zaglavlja fstream.h.
Budui da su klase navedene u toj datoteci izvedene iz klasa deklariranih u
datoteci iostream.h, potonju u tom sluaju ne treba dodatno ukljuivati.
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

int main(int argc, char* argv[]) {


// ako je u pozivu programa naveden samo jedan parametar:
if (argc == 2) {
// stvara se ulazni tok datnica klase ifstream i
// pridruuje datoteci navedenoj kao parametar
ifstream datnica(argv[1]);
// ako otvaranje datoteke nije uspjelo:
if (!datnica) {
cerr << "Nemrem otpreti potraivanu datoteku "
<< argv[1] << endl;
exit(1);
}
char zn;
// znak po znak, sadraj datoteke:
while (datnica.get(zn)) cout << zn;
}
else if (argc < 2) {
// ako se program pokrene bez parametra (imena
// datoteke koju treba ispisati), ispisuje uputu:
cout << "Program za ispis sadraja datoteke" << endl;
cout << "Koritenje: ISPIS <ime_datoteke>" << endl;
}
else {
cerr << "Preve parametara u pozivu programa" << endl;
exit(1);
}
return 0;
}

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.
Prilikom inicijalizacije toka valja provjeriti je li inicijalizacija bila uspjeno
provedena, tj. je li datoteka uspjeno otvorena za uitavanje ili upisivanje.
U protivnom, daljnje izvoenje programa nee imati nikakvog efekta.
Ako je inicijalizacija bila uspjena i datoteka otvorena za itanje, u while-petlji se
iitava sadraj datoteke. Za itanje se koristi funkcijski lan get(char &), opisan u
odjeljku 18.5.3. Budui da je klasa ifstream naslijeena iz istream, za uitavanje iz

543

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.
18.7.2. Otvaranje i zatvaranje datoteke

Da bi se podaci mogli pohraniti u ili uitati iz neke datoteke, prethodno je potrebno


datoteku otvoriti. Ako datoteka u koju se podaci upisuju ne postoji, treba ju stvoriti, a
ako datoteka koju elimo itati nije dostupna, valja prijaviti pogreku. Nakon
obavljenog prijenosa podataka, datoteka se mora zatvoriti. Ovo je posebno vano kod
upisa podataka u datoteku, jer se tek prilikom zatvaranja na disku auriraju podaci o
datoteci (duljina, vrijeme pristupa i slino). Prema tome, moemo razluiti tri faze
prijenosa podataka:
1. otvaranje datoteke,
2. ispis ili uitavanje podataka i
3. zatvaranje datoteke.
U dosadanjim primjerima smo datoteku otvarali prilikom inicijalizacije toka, navodei
ime datoteke u konstruktoru toka. Datoteka se zatvarala prilikom unitavanja toka, to se
automatski dogaa prilikom izlaska iz bloka u kojem je tok definiran. U prethodnim
primjerima to se deavalo pri izlasku iz glavne funkcije naredbom return ili funkcijom
exit().
Datoteka se moe otvoriti i naknadno, pomou funkcijskog lana open(). Naime,
konstruktori klasa ifstream i ofstream definirani su u po dvije preoptereene verzije:
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

Tablica 18.3. Modovi otvaranja datoteka definirani u klasi ios

mod

znaenje

app
ate
binary
in
nocreate
noreplace
out
trunc

upis se obavlja na kraju datoteke (append)


datoteka se otvara i skae se na njen kraj (at-end)
upis i uitavanje se provode u binarnom modu
otvaranje za itanje
vraa se pogreka ako datoteka ne postoji
vraa se pogreka ako datoteka postoji
datoteka se otvara za pisanje
sadraj postojee datoteke se brie prilikom
otvaranja (truncate)

nadovee na ve postojei, u naredbi za otvaranje toka emo kao drugi parametar


navesti, uz podrazumijevani mod out, i mod app. U primjeru na 543. stranici to bi
znailo da izlazni tok trebamo inicijalizirati naredbom
ofstream ponornik(argv[2], ios::out | ios::app);

Ako prevedemo ovakav program i vie puta ga uzastopce pozovemo s istim


parametrima u komandnoj liniji (npr. ulazna.dat izlazna.dat), sadraj ulazne
datoteke e se prilikom svakog poziva dodati izlaznoj.

546

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:
ifstream izlazniUlaz("kamo.dat", ios::out);
ofstream ulazniIzlaz("oklen.dat", ios::in);

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.
18.7.3. Klasa fstream

Ponekad trebamo iz iste datoteke podatke itati i u nju pohranjivati podatke. Na primjer,
program za evidenciju stanja na tekuem raunu uitavat e iz datoteke zadnje stanje
na raunu, dodati ili oduzeti nove uplate, odnosno izdatke te potom pohraniti novi saldo
u datoteku. Kako klase ifstream i ofstream podravaju komunikaciju samo u
jednom smjeru, njihovo koritenje bi iziskivalo da prvo otvorimo datoteku za
uitavanje, uitamo podatke te zatvorimo datoteku, a tek potom otvorimo istu datoteku
za upis, upiemo novi podatak i na kraju zatvorimo izlazni tok. Takvo programiranje bi
bilo dosta zamorno, a rezultiralo bi i vrlo sporim programom. Postupak pojednostavljuje
klasa fstream izvedena iz klase iostream, koja je pak izvedena iz klasa istream i
ostream ona u sebi ima ukljuene sve operacije za pisanje i itanje jedne te iste
datoteke. Ilustrirajmo primjenu klase fstream primitivnim programom za voenje
evidencije stanja na raunu:
//...
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.;

Klasa fstream nije navedena u standardu, unato injenici da se spominje u veini


referentnih knjiga. Stoga ne moemo sa sigurnou rei hoe li ona biti dostupna uz sve
biblioteke tokova (no sve su prilike da hoe).

547

}
else {
do {
strujiStruja >> staroStanje;
} while(strujiStruja);
}
cout << setiosflags(ios::fixed) << setprecision(2)
<< setw(15) << "staro stanje:"
<< setw(8) << staroStanje << endl;
cout << setw(15) << "uplate:";
float uplate;
cin >> uplate;
cout << setw(15) << "rashodi:";
double rashodi;
// za rashode uvijek koristite double!
cin >> rashodi;
// ispisuje 25 znakova '-' za podcrtavanje
cout << setw(25) << setfill('-') << " " << endl
<< setfill(' ');
float novoStanje = staroStanje + uplate - rashodi;
strujiStruja.clear();
strujiStruja << novoStanje << endl;
if (strujiStruja.fail())
cerr << "Nekaj ne valja s upisom" << endl;
else {
cout << setw(15) << "novo stanje:"
<< setw(8) << novoStanje << endl << endl
<< "Novo stanje je s uspjehom pohranjeno" << endl;
}
//...

Prilikom inicijalizacije objekta strujiStruja klase fstream kao prvi argument


navodimo ime datoteke. Drugi argument konstruktoru jest mod: definiramo in i out
(uitavanje i upisivanje), uz eksplicitnu zabranu automatskog generiranja nove datoteke
ako datoteka s navedenim imenom ne postoji (mod noreplace). Ovo nam treba zato da
bi korisnika upozorili da datoteka ne postoji (moda postoji, ali u drugom imeniku), te
da bismo inicijalizirali vrijednost varijable staroStanje, koju inae uitavamo iz
datoteke, na nulu. Ako datoteka ne postoji, nakon ispisa poruke se otvara nova datoteka
naredbom
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());

Po zavrenom unosu podataka i raunu, vrijednost varijable novoStanje se pohranjuje


na kraj datoteke. Uoimo naredbu
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.
18.7.4. Odreivanje i postavljanje poloaja unutar datoteke

Prilikom pisanja i itanja datoteke, poseban pokaziva datoteke (engl. file pointer)
pokazuje na poziciju u datoteci na kojoj se operacija obavlja. U dosadanjim primjerima
podaci su se uzastopno upisivali u datoteke ili iz njih uzastopno uitavali. Pritom se
pokaziva datoteke automatski uveava za broj upisanih/uitanih bajtova. Eksplicitnim
usmjeravanjem tog pokazivaa na eljeno mjesto mogue je itati podatke ili pisati ih
na proizvoljnom mjestu unutar datoteke.
Pokaziva datoteke je potrebno razlikovati od obinih C++ pokazivaa on je
jednostavno cijeli broj koji pokazuje na koliko se bajtova od poetka datoteke upis
obavlja. Tip pokazivaa je definiran tipom streampos. Taj tip je definiran pomou
kljune typedef u datoteci zaglavlja fstream.h te je njegova definicija
implementacijski zavisna. U veini implementacija taj tip je zapravo long int.
U klasi ostream definirana su dva funkcijska lana kojima se dohvaa pokaziva
datoteke, te time kontrolira poloaja unutar izlaznog toka:
1. tellp() koji kao rezultat vraa trenutnu vrijednost pokazivaa datoteke,
2. seekp() kojim se pomie na zadani poloaj unutar datoteke.
Slino su u klasi istream definirana dva funkcijska lana koji omoguavaju kontrolu
poloaja u ulaznom toku:
1. tellg() koji kao rezultat vraa trenutnu vrijednost pokazivaa datoteke,
2. seekg() kojim se pomie na zadani poloaj unutar datoteke.
Zbog slinosti imena, u poetku e korisnik teko razlikovati lanove klase ostream
od lanova klase istream. Radi lakeg pamenja, moemo shvatiti da zadnja slova u

549

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:
ostream &ostream::seekp(streampos &pozicija);
istream &istream::seekg(streampos &pozicija);
ostream &ostream::seekp(streamoff &pomak, ios::seekdir smjer);
istream &istream::seekg(streamoff &pomak, ios::seekdir smjer);

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.
18.7.5. Binarni zapis i uitavanje

Brojevi se u memoriji raunala pohranjaju u binarnom formatu. Meutim, tekstovni je


format primjereniji ljudima, tako da se na zaslonu brojevi ispisuju u tekstovnom formatu
(osim kada brojeve ispisujemo u heksadekadskom ili oktalnom obliku). Isto vrijedi i za
unos podataka pomou tipkovnice. Izravna posljedica takve dvolinosti jest
neophodna pretvorba brojeva iz tekstovnog oblika u binarni oblik prilikom unosa, te
obrnuta pretvorba prilikom ispisa.
Iako se podaci u datoteke mogu pohranjivati u tekstovnom obliku i kao takvi itati
(kao to je u dosadanjim primjerima raeno), ne postoji sutinski jai argument da se
brojevi moraju pohranjivati ba u tom obliku. Jedini argument za tekstovno
pohranjivanje jest taj da se tako pohranjeni brojevi mogu proitati bilo kojim
programom za obradu teksta. Meutim, binarno pohranjeni brojevi se bre uitavaju iz
datoteke u memoriju raunala i upisuju u datoteku, jer nema nikakve pretvorbe podaci
se jednostavno preslikavaju bajt po bajt.
Uz veu brzinu, binarno pohranjivanje brojeva gotovo uvijek rezultira manjom
duljinom zapisa, kao posljedica efikasnijeg koritenja znakova u binarnom formatu. U
tekstovnom formatu se, primjerice, broj 1066 pamti tono tako, kao niz od etiri
znamenke ('1', '0', '6', '6') pri emu se za svaku znamenku potroi jedan bajt
(pamti se kd znaka). Naprotiv, taj broj se moe pohraniti u binarnom formatu kao niz
od samo dva bajta: nii bajt je 42, a vii bajt je 4. Razlika u korist binarnog zapisa je
vie nego oita! Za znakove i znakovne nizove nema razlike izmeu binarnog i
tekstovnog zapisa, jer se znakovi uvijek pohranjuju kao (najee) ASCII kdovi
pripadajuih znakova.
Binarni zapis i itanje datoteke pokazat emo na sljedeem primjeru. Zamislimo da
imamo poduzee koje se bavi proizvodnjom elektronikih miolovki. elimo napraviti
bazu podataka s imenima kupaca kojima periodiki (svaka dva mjeseca, a zimi ak
jednom mjeseno) aljemo nove baterije i barutno punjenje. Kako poduzee posluje vrlo
uspjeno u uvjetima trinog gospodarstva (Jeste li gledali Izbavitelja?), imat emo
vrlo dugaki listu, pa ako je elimo pregledavati, pogodno je imati ju sortiranu po
imenima kupaca. Kako osim ingenioznosti na podruju suvremenog uklanjanja
glodavaca posjedujemo i novosteeno znanje iz jezika C++ i nesumnjive sklonosti
pisanju programa s tokovima (psiholozi e im tek posvetiti mnoge knjige), napisat emo
kratak program kojim emo unositi imena pomou tipkovnice i upisivati ih u datoteku.
Pri tome e se podaci u datoteci pamtiti u vezanoj listi: ispred svakog naziva bit e
zapamen datoteni pokaziva na sljedei naziv. Prilikom upisa, novododani podatak e

551

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.
const int duljImena = 50;

// najvea dozvoljena duljina

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

bazaImena.write((char *) &pozSljed, sizeof(pos_type));


bazaImena.write(novoIme, strlen(novoIme) + 1);
// postavlja se na prethodni zapis u nizu i usmjerava ga
// na novododani zapis
bazaImena.seekp(pozPreth);
bazaImena.write((char *) &pozTemp, sizeof(pos_type));
// unos novog imena
cin.getline(novoIme, sizeof(novoIme));
}
cout << "Upisi su zavreni." << endl << endl;
// slijedi kd za ispis liste...

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:
// ispis lanova liste po abecedi
cout << "Dosad upisana imena:" << endl;
bazaImena.seekg(0);
bazaImena.read((char *) &pozSljed, sizeof(pos_type));
while (pozSljed) {
bazaImena.seekg(pozSljed);
bazaImena.read((char *) &pozSljed, sizeof(pos_type));
bazaImena.get(novoIme, duljImena, '\0');
cout << novoIme << endl;
}
cout << "*** Konac popisa ***" << endl;

Za itanje binarno zapisanog pokazivaa datoteke koristi se funkcijski lan read(). On


je komplementaran lanu write(): prvi argument mu je pokaziva char * na niz

553

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.

18.8. Tokovi vezani na znakovne nizove


U prethodnim odjeljcima smo vidjeli kako se tok moe vezati na datoteku. Podaci su se
prilikom zapisivanja pohranjivali u datoteku kao niz znakova. Prilikom itanja, taj se niz
znakova uitava sa vanjske jedinice na koju je datoteka pohranjena. Isto tako se tok
moe vezati i na znakovni niz pohranjen u memoriji raunala datoteka na disku je
zapravo niz bajtova te se moe promatrati kao znakovni niz. Za tokove nema razlike
itaju li se podaci iz datoteke ili znakovnog niza, jer tok zapravo barata s apstraktnim
spremnikom podataka u kojemu su podaci organizirani u nizove bajtova (od tuda i
dolazi naziv tok).
Tokovi vezani na znakovne nizove deklarirani su u zaglavlju sstream. Na
raspolaganju su nam dvije klase: istringstream (naslijeena od klase istream) za
itanje toka te ostringstream (naslijeena od klase ostream) za upis na tok. U
starijim implementacijama zaglavlje se zvalo strstream.h, a klase su se zvale
istrstream, odnosno ostrstream. Zbog kompatibilnosti s postojeim
implementacijama, standard predvia podrku i za te klase.
Koritenje tokova vezanih na znakovne nizove ilustrirat emo na primjeru
programa koji uitava sadraj datoteke text.txt u znakovni niz [Lippman91].
Program smo realizirali pomou zaglavlja strstream.h, budui da je prevoditelj kojeg
smo mi koristili imao samo to zaglavlje u biblioteci:
ifstream ulDatoteka("tekst.txt", ios::in);
ostrstream znTok;
char znak;
while (znTok && ulDatoteka.get(znak))
znTok.put(znak);
char *sadrzaj = znTok.str();

Za uitavanje sadraja datoteke inicijaliziramo ulazni tok ulDatoteka te uitavamo


znakove iz datoteke tekst.txt sve do njenog kraja. Uitane znakove upisujemo u tok

554

znTok klase ostrstream. Upisivanje u tok se obavlja pomou funkcijskog lana


put(). Taj funkcijski lan ne samo da zapisuje znakove u memoriju, nego istovremeno

poveava memorijski prostor neophodan za niz koji trebamo pohraniti.


Nakon to su uitani svi znakovi, poziva se funkcijski lan str() koji vraa
pokaziva na niza znakova pridruenih objektu klase ostrstream. Tim pozivom se
pokaziva sadrzaj preusmjerava na niz uitanih znakova pa se uitani znakovi mogu
obraivati kao obian znakovni niz (valja jedino napomenuti da taj niz ne mora biti
zakljuen nul-znakom to ovisi o sadraju datoteke).
Osim to vraa pokaziva na toku pridrueni znakovni niz, funkcijski lan str()
ujedno zamrzava sadraj toka. On jednostavno reeno, niz uitanih znakova preuzima
od ostrstream objekta i prosljeuje ga okolnom programu. Time svako daljnje
upisivanje podataka postaje onemogueno. Zbog toga lan str() treba pozvati nakon
to su u niz upisani svi podaci.
Izlaskom iz bloka u kojem je objekt klase ostrstream bio inicijaliziran
automatski se unitava cijeli objekt, zajedno sa sadrajem upisanog niza. Meutim,
nakon poziva lana str(), taj niz vie nije u nadlenosti ostrstream objekta, tako da
ga programer mora eksplicitno unititi. Primjerice, za gornji sluaj to bi se postiglo
naredbom:
delete sadrzaj;

Kako su klase ostrstream i istrstream naslijeene iz klasa ostream, odnosno


istrstream, dostupni su i svi njihovi funkcijski lanovi. To primjerice znai da se po
znakovnom nizu moemo etati pomou funkcijskih lanova seekg() i seekp(),
kao da se radi o datoteci na disku. Takoer automatski vrijede operatori umetanja << i
izluivanja >>.

18.9. Ulijeva li se svaki tok u more?


Napomenimo na kraju da u ovom poglavlju nisu spomenuti svi lanovi opisanih klasa
iznijeli smo samo najinteresantnije lanove. Osim toga, ispis na zaslon i uitavanje s
tipkovnice ulazno-izlaznim tokovima (onakvima kako su definirani standardom)
mogue je samo pod operacijskim sustavima s tekstovnim sueljem, kao to su UNIX
ili DOS. Operacijski sustavi s grafikim sueljem (MS Windows, X-Windows) iziskuju
posebne funkcije koje se isporuuju u bibliotekama s prevoditeljem.
No znanje steeno u ovom poglavlju je dosta vano i za (X) Windows programere.
Naime, mnoge komercijalno dostupne biblioteke (na primjer Microsoftova MFC
biblioteka) podravaju operacije koje vrlo nalikuju tokovima, a obavljaju poslove
pohranjivanja objekata biblioteke na disk ili sline operacije. Zbog svoje jednostavnosti
i intuitivnosti, model tokova je preuzet za razliite ulazno/izlazne poslove te je dosta
vano razumjeti ga.

555

19. Principi objektno orijentiranog dizajna


Ja slikam objekte kako ih mislim,
a ne kako ih vidim.
Pablo Picasso (1881-1973)
U knjizi J. Golding: Cubism (1959)

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.

19.1. Zato uope C++?


Ako ste izdrali itajui knjigu do ovog poglavlja, onda ste se upoznali sa svim
svojstvima objektno orijentiranog programiranja koje nudi jezik C++. Nauili ste
definirati klase, specificirati funkcijske lanove, preoptereivati operatore i jo tota
drugo. No ako ste novi brodolomac u vodama objektno orijentiranog programiranja, pa
ak ako ste ve proli objektno orijentirano vatreno krtenje i izdigli se iznad statusa
utokljunca (engl. green-horn), vrlo je vjerojatno da ste si prilikom itanja prethodnih
poglavlja postavili pitanje: A to je to uope objekt? I zato da se uope patim s
virtualnim funkcijskim lanovima kada se to sve moe napraviti i u obinom C-u, ili ak
u Pascalu?
Odgovor na to pitanje nije jednostavan. Napisano je mnogo knjiga o objektno
orijentiranom pristupu u kojima su razmatrani teoretski aspekti OOP-a (engl. ObjectOriented Programming). Istina je da se sve to se napie u C++ jeziku moe realizirati i
u obinom, bezobjektnom C ekvivalentu. Uostalom, na kraju balade, kompletan kd
se prevodi u strojni jezik, pa bismo isto mogli postii piui direktno strojne instrukcije.
Openiti odgovor na gornje pitanje moe se saeti u dva osnovna pojma: objektno
programiranje podrava apstrakciju podataka (engl. data abstraction) i bolju ponovnu
iskoristivost kda (engl. code reusability). Ti pojmovi nisu vezani iskljuivo za C++
jezik, ve i za preostale objektno orijentirane jezike, kao to su SmallTalk, Actor i drugi.
Tim vie, C++ podrava viestruki pristup programiranju: objekte moemo koristiti
samo za neke segmente programa ili ih ak uope ne moramo koristiti. Dapae, mogue

556

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.

19.2. Objektna paradigma


U starijim programskim jezicima, kao to su poetne verzije FORTRAN-a, programeri
nisu imali ba velike mogunosti modeliranja podataka. Na raspolaganju su im bile
varijable koje su uvale najee samo jednu brojanu vrijednost i polja koja su mogla
uvati nekoliko istovrsnih podataka. Oni sretniji su raspolagali prevoditeljem koji je
podravao znakovne nizove. Takvo to se smatralo ve rasipnitvom raunala su

557

imala vrlo usko podruje primjene, primarno za numerike proraune. Takoer,


programi pisani u tim jezicima bili su bez ikakve strukture te se programer morao
brinuti o svim aspektima rada raunala, gotovo kao da je sm program pisan u strojnom
jeziku.
No jezici Pascal i C unijeli su znaajnu novost u programiranje. Umjesto
programera, mnoge poslove organizacije podataka u tim jezicima obavlja prevoditelj.
To je postignuto strukturiranjem programa. Cijeli monolitni kd se razbija u vie
zasebnih funkcionalnih cjelina, koje se zatim meusobno povezuju. Uvedene su lokalne
varijable koje postoje samo u nekom segmentu programa, te se vanjski program o njima
ne mora brinuti.
U tim jezicima po prvi put se javlja mogunost strukturiranja podataka. U Pascalu
se to ini kljunom rijei record, a u C-u rijei struct. Takve strukture mogu
sadravati nekoliko podataka razliitih tipova koji zajedno ine smislenu cjelinu.
Program koji je koristio takve strukture u cijelosti je poznavao svaki lan strukture te je
pristupao svakom lanu strukture direktno. Opisani pristup je u mnogome
pojednostavnio nain pisanja sloenih programa. Programer nije promatrao sve
elemente strukture nezavisno, ve povezano u logiku cjelinu. Programiranje na taj
nain je proceduralno, no podaci su skupljeni u agregate.
Od agregata do objekata je vrlo malen korak. Naime, kada smo ve povezali srodne
podatke i omotali ih celofanom, zar ne bi bilo prikladno odmah definirati i dozvoljene
operacije nad njima te na taj nain dati smisao podacima i tako na njih staviti manicu?
Za svaki tip podataka s kojim radimo postoji odreeni smisleni skup operacija, dok neke
druge operacije za taj tip jednostavno nemaju smisla. Na primjer, nema smisla mnoiti
dva znakovna niza, ali ih ima smisla ispisivati ili zbrajati. Zbog toga, prije nego to se
uputimo u izradu tipa podataka koji e prikazivati znakovni niz, prvo emo se upitati to
zapravo s nizom elimo uiniti. Ako dobro shvatimo emu nam neki podatak slui, bit
e jednostavnije napisati adekvatnu implementaciju sa svim eljenim svojstvima. Podaci
i radnje nad njima se vie nee tretirati odvojeno i neovisno jedni od drugih. Takav
model podataka se ponekad naziva i model apstraktnih tipova podataka (engl. abstract
data types).
Kljuna rije u gornjem terminu je apstrakcija podataka. To je postupak kojim se
unutranja sloenost objekta skriva od okolnog svijeta. To je i logino: vanjski program
ne mora znati kako je znakovni niz interno predstavljen njega interesira iskljuivo
sadraj. Vanjskom programu su interesantna svojstva objekta te akcije koje on moe
uiniti, a ne nain na koji e se to provesti. Interna reprezentacija objekta je
implementacijski detalj koji je skriven od vanjskog svijeta. On se moe i promijeniti ako
se pokae da postojea implementacija nije dobra, no vano je da objekt u smislu svojih
svojstava ostane nepromijenjen. Okolni programi pristupaju objektu preko njegovog
javnog suelja koje definira izgled objekta prema okolini.
Ovakav postupak prikrivanja unutarnje strukture objekta naziva se skrivanje
podataka (engl. data hiding). Ono ima znaaj koji debelo prelazi granice objektno
orijentiranog programiranja. Sloeni raunarski sustavi se danas modeliraju upravo po
tom naelu. Tako na primjer operacijski sustav Windows skriva svoje strukture u kojima
se uvaju podaci o stanju raunala od prosjenog korisnika. Korisniki program pristupa

558

pojedinim uslugama sustava preko definiranog programerskog suelja, esto oznaenog


skraenicom API (engl. application programming interface).
Jezik C++ omoguava primjenu koncepcije skrivanja informacija pomou klasa
tipova podataka koji sadravaju i podatke i funkcije. Pomou dodjele prava pristupa
pojedinim segmentima objekta, mogue je razgraniiti javno suelje od implementacije.

19.3. Ponovna iskoristivost


Drugi pojam vezan za objektno programiranje je ponovna iskoristivost (engl.
reusability) kda. To je svojstvo koje omoguava da se jednom napravljeni dijelovi
programa ponovo iskoriste u nekom drugom projektu uz minimalne izmjene. Time se
moe znatno skratiti vrijeme potrebno za razvoj novih programa.
No to je tu udno?, upitat ete se: Zar je takvim bombastinim i kompliciranim
nazivom potrebno oznaavati jednostavno kopiranje segmenata kda? Naposljetku, u
obinim proceduralnim jezicima mogue je stvarati biblioteke funkcija koje se mogu
pozivati iz bilo kojeg drugog programa.
Gornja konstatacija je tona to se tie stvaranja obinih biblioteka funkcija, ali pod
pojmom ponovne iskoristivosti se danas podrazumijeva znatno vie. Naime, kako smo
ve zakljuili da je pristup programiranju pomou apstraktnih podataka znatno
jednostavniji i manje sklon pogrekama, ponovna iskoristivost se ne moe ograniiti
iskljuivo na stvaranje biblioteke funkcija. Umjesto toga, potrebno je stvoriti biblioteku
objekata koji se zatim koriste prema potrebi.
No niti to nije osnovni problem ponovne iskoristivosti. Ponovna iskoristivost znai
da se jednom napisani kd unutar istog programa koristi vie puta ili ak na vie naina.
Na primjer, este su potrebe za definiranje lista razliitih objekata. U tu svrhu moe se
definirati objekt Lista koji e definirati javno suelje (operacije Dodaj(), Ukloni(),
Trazi() i sline) i mehanizam liste. Sam objekt koji e se uvati u listi specificirat e
se naknadno. Takoer, ako je mehanizam liste potrebno proiriti novim svojstvima, kao
primjerice brisanje cijele liste, nee biti potrebno pisati cijeli kd iznova nego e se
jednostavno dodati samo eljena promjena. Osnovni mehanizmi koji to omoguavaju su
nasljeivanje i polimorfizam. Time se model programiranja pomou apstraktnih tipova
podataka izdie na nivo pravog objektnog programiranja.
Nasljeivanje je postupak kojim se iz postojee klase izvodi nova klasa u kojoj se
navode samo promjene u odnosu na osnovnu klasu. Polimorfizam je svojstvo koje
omoguava rukovanje podacima iji tip prilikom pisanja programa nije poznat. To je
zapravo samo jo daljnji korak u procesu enkapsulacije: tip objekta je upakiran sa
samim objektom te se koristi prilikom izvoenja kako bi se odredilo ponaanje objekta.
Jezik C++ podrava sva tri pristupa programiranju: proceduralno programiranje
proireno agregatima podataka, programiranje pomou apstraktnih tipova te pravo
objektno orijentirano programiranje. Na korisniku je da izabere pristup najprikladniji
problemu koji se rjeava. U nastavku e se razmotriti vaniji koraci u pisanju objektno
orijentiranih programa.

559

Principe objektno orijentiranog dizajna prikazat emo na primjeru izrade biblioteke


koja e opisivati interaktivno korisniko suelje. Ona e sadravati objekte koji e
omoguavati prikazivanje prozora na ekranu raunala. Prozori e se moi pomicati
pomou mia. Takoer e biti mogue svakom prozoru dodijeliti izbornik. Taj primjer je
izabran zato jer su relacije izmeu elemenata korisnikog suelja (prozori, izbornici) i
objekata koji modeliraju te elemente vrlo oite i razumljive.
Cilj ovog poglavlja nije ispisivanje izvornog kda biblioteke koja e stvarno
omoguavati otvaranje prozora. To i nije mogue, jer je za to potrebno mnogo podataka
o stvarnom raunalu za koje je biblioteka namijenjena. Takoer, iluzorno je za oekivati
da je mogue izraditi kvalitetnu biblioteku te njenu strukturu prikazati na desetak
stranica koje sainjavaju ovo poglavlje. Ono to je mogue, jest prikazati osnovnu ideju
bez specificiranja kda koji stvarno crta prozor ili izbornik. Biblioteka nee biti vezana
niti za jedno raunalo, pa ak nee specificirati da li se koristi u grafikom ili
tekstovnom nainu.

19.4. Korak 1: Pronalaenje odgovarajue apstrakcije


Da bismo principe objektnog programiranja pravilno primijenili u C++ jeziku, potrebno
je prvo pronai odgovarajui model za opisivanje naih podataka. Aplikacija koju
izraujemo sastoji se od niza razliito organiziranih struktura podataka. Te podatke je
potrebno prikazati objektnom apstrakcijom. Da bismo to uinili, neophodno je detaljno
istraiti koje su to apstrakcije i koja su njihova svojstva. Prije nego to ponemo pisati
klase, potrebno je ustanoviti s kojim emo klasama uope raditi.
Dobra je praksa najprije ispisati na komad papira to saetije tekst zadatka kojeg
smo si postavili te ga zatim nekoliko puta proitati i razmotriti. U sluaju korisnikog
suelja zadatak bi mogao biti ovako zadan:
Potrebno je napisati biblioteku u C++ jeziku koja e omoguavati prikazivanje
prozora i izbornika na ekranu raunala.
Iako je ovo vrlo povran opis samog problema, dovoljno je intuitivan za sve koji su ikad
imali ikakav kontakt s grafikim korisnikim sueljima, kao to su primjerice MS
Windows ili X-Windows. Iz gornjeg opisa potrebno je odrediti objekte koji su kandidati
za apstrakciju. Dobro je ispisati sve imenice iz teksta zadatka kako bi se pronali
kandidati za apstrakciju:

biblioteka
jezik
prozor
izbornik
ekran
raunalo

Potrebno je razmotriti listu i sa stanovita zadatka ocijeniti koji su objekti pogodni za


apstrakciju pomou klase prilikom rjeavanja problema.

560

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.

19.5. Korak 2: Definicija apstrakcije


U prethodnom koraku razluili smo nekoliko objekata koje bismo eljeli implementirati
pomou klasa. No jo smo vrlo daleko od samog pisanja kljune rijei class. Za sada
znamo premalo o pojedinom objektu da bismo ga mogli direktno opisati. Zbog toga
emo nadalje pokuati preciznije definirati svaku apstrakciju. Pri tome je vrlo vano
usredotoiti se na identifikaciju apstrakcije bez razmiljanja o implementaciji.
19.5.1. Definicija ekrana

Napisat emo kratku reenicu koja e dati definiciju ekrana:


Ekran je element raunala koji koristi katodnu cijev za prikaz slike u
razluivosti 640 toaka horizontalno i 480 toaka vertikalno, s maksimumom
prikazivanja od 16 boja istodobno.
Nakon to smo napisali gornju definiciju, pokuajmo je ponovo proitati i razmotriti da
li ona dobro opisuje eljenu apstrakciju.

561

Ve se na prvi pogled moe ustanoviti da je gornja definicija zapravo vrlo loa.


Umjesto da kae to ekran radi i s kojim objektima surauje, ona odmah uvodi niz
implementacijskih detalja koji nisu vani prilikom pokuaja ope definicije ekrana.
Nadalje, reeno je da ekran koristi katodnu cijev. To ne samo to ne mora uvijek biti
tono (sve se vie koriste ekrani s tekuim kristalima ili aktivnim elementima), nego
uope nije vano za na program. Zbog toga emo odbaciti gornju definiciju te pokuati
specificirati emu ekran slui:
Ekran je element raunala koji omoguava prikaz promjenjive slike.
Ovo je znatno bolje, ali nije dovoljno specifino. Sada ve imamo intuitivnu viziju
emu nam ekran slui, no moramo biti jo malo vie specifini kako bismo opisali nain
rada ekrana.
Prikaz slike na ekranu se odvija tako da se slika rastvori u niz toaka koje se
postave u horizontalnu i vertikalnu krialjku. Broj toaka u horizontalnom
smjeru se naziva horizontalna razluivost, a broj toaka u vertikalnom smjeru
vertikalna razluivost.
Sada ve znamo mnogo vie: ekran slui za prikaz slike. Slika je nova apstrakcija koja
se sastoji od toaka, a ima i odreene atribute: razluivost u horizontalnom i
vertikalnom smjeru. Toka je takoer nova apstrakcija, a evo i njene definicije:
Toka je osnovni element slike. Njeno glavno svojstvo je boja. Svakoj toki se
moe neovisno postaviti neka eljena boja.
Gornja definicija kae da toka zapravo modelira boju na odreenom dijelu slike.
Takoer, definicija naznaava da se atribut boje za pojedinu toku mora moi postaviti.
Iako u definiciji nije navedeno, bilo bi poeljno da se boja moe i proitati. Definicija
takoer kae da se boja za svaku toku moe postavljati neovisno. Za mnoge vrste
ekrana to nije tono. Naime, zbog svoje sklopovske grae veina dananjih videopodsustava omoguava prikaz samo nekog odreenog broja boja odjednom (16, 256, i
slino). Gornju definiciju treba promijeniti tako da se to svojstvo ugradi u definiciju
toke:
Svakoj toki se moe postaviti boja neovisno sve dok ukupan broj razliitih
boja svih toaka na ekranu ne premai ukupan broj boja koje se mogu
prikazati na ekranu.
Taj problem veina stvarnih ureaja za prikaz slike rjeava tako da se uvede takozvana
paleta boja. To je zapravo jedan popis trenutno aktivnih boja. Svaka boja predstavlja
jednu stavku u paleti dok toka umjesto da direktno specificira boju predstavlja samo
indeks u paletu. Na ovom mjestu bi sada bilo potrebno dati definiciju palete te zatim
iterirati kroz sve do sada dane definicije. To bi nas vrlo brzo odvelo duboko u detalje
hardverske implementacije pojedinih ureaja pa emo se zato zadovoljiti s ovakvim
pojednostavljenim modelom.
Napomenimo na kraju to podrazumijevamo pod ekranom. U uem smislu ekran je
zaslon raunala ili monitor. No kako ne bismo otili predaleko i cjepidlaili oko toga da

562

za prikaz slike treba monitor i grafika kartica, pod nazivom ekran podrazumijevat
emo cjelokupni podsustav raunala za prikaz slike.
19.5.2. Definicija prozora

Po gore navedenom principu provest emo definiciju apstrakcije prozora kako bismo
mogli bolje definirati na objektni model:
Prozor je podruje slike na ekranu koje prikazuje stanje izvoenja pojedine
aplikacije u raunalu.
Gornja definicija, iako moda na prvi pogled najoitije prikazuje ulogu prozora, nije
sasvim precizna. Naime, rei da je primarna uloga prozora prikazivanje stanja odreene
aplikacije je vrlo usko odreenje. U stvarnosti prozori se koriste za mnogo raznih drugih
poslova, kao to je prikazivanje dijalokih prozora ili ak modeliranje pojedinih kontrola
unutar prozora. Takoer, operacijski sustav MS Windows implementira padajue
izbornike pomou prozora. Zbog toga treba promijeniti gornju definiciju:
Prozor je pravokutno podruje slike na ekranu za prikaz odreenog skupa
logiki povezanih podataka.
Takva definicija znatno bolje opisuje to je prozor. Pri tome nita nije reeno o svrsi
prozora; on samo prikazuje podatke. Takav prozor moe posluiti za prikaz podataka
aplikacije koja se izvodi na raunalu, ali takoer moe posluiti i za prikaz izbornika.
Nadalje, prozora ima razliitih vrsta: osim prozora koji predstavljaju neko podruje na
ekranu, postoje prozori koji mogu imati okvir, polja za poveavanje, smanjenje i
zatvaranje pa i pridijeljen izbornik. Nazovimo takve prozore uokvirenim prozorima.
19.5.3. Definicija izbornika

Trei vaan element nae biblioteke elemenata grafikog suelja jesu izbornici (meniji).
Pokuajmo izrei kratku definiciju izbornika.
Izbornik je podruje na ekranu koje omoguava izbor neke od ponuenih
opcija.
Ova definicija dosta dobro ocrtava to je izbornik i emu slui, ali ona nita ne kae o
detaljima kako to izbornik radi. Zbog toga je potrebno definiciju proiriti i opisati to su
opcije te navesti nain na koji su opcije prikazane.
Pojedina opcija izbornika je kratak tekst koji ukratko opisuje akciju koju e
program poduzeti nakon njenog izbora. Takoer, opcija izbornika moe voditi
u novi podizbornik.
Sada ve znamo dosta o strukturi izbornika. No jo mnoga pitanja ostaju otvorena: kako
izbornik izgleda kada se prikae na ekranu, kako izgledaju pojedine opcije, kako se
izabire opcija miem i slino. Dapae, razvoj klase izbornika koja e se moi koristiti
je vrlo sloen i obuhvaat e razne parametre. U razvoj takve klase osim inenjera

563

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.

19.6. Korak 3: Definicija odnosa i veza izmeu klasa


Ovaj korak se redovito ne moe promatrati zasebno, ve zajedno s prethodnim
korakom. No mi smo ga istakli zasebno zato jer je prilikom odreivanja odnosa i veza
potrebno biti vrlo paljiv kako bi se izbjegle klasine zamke.
Ve iz primjera iznesenog u prethodnom koraku vidljivo je da rijetko koja klasa
postoji sama za sebe. Veina klasa na neki nain dolazi u interakciju s drugim klasama.
Zbog toga je za ispravan dizajn sustava vrlo vano ispravno definirati odnose izmeu
klasa. Postoje tri osnova tipa odnosa klasa:

Odnos biti u kojemu je apstrakcija opisana nekom klasom A istodobno i apstrakcija


opisana nekom drugom klasom B. Pri tome se moe rei da je A podskup od B.
Odnos posjedovati u kojemu neka apstrakcija opisana klasom A sadrava (posjeduje)
neku drugu apstrakciju opisanu klasom B.
Odnos koristiti u kojemu neka apstrakcija opisana klasom A koristi, ali ne posjeduje
neku apstrakciju opisanu klasom B.
Vrlo je vano razumjeti svojstva svakog od ovih tipova odnosa te emo ih razmotriti
pojedinano.
Odnos biti je odnos u kojem se neki objekt A istodobno moe promatrati i kao
objekt nadklase B. Na naem primjeru, LCD ekran opisan klasom LCDEkran jest
istodobno i obian ekran opisan klasom Ekran. Klasa LCDEkran je izvedena iz klase

564

Ekran, odnosno klasa Ekran je naslijeena te je dobivena klasa LCDEkran. Takav je


odnos prikazan na slici 19.1.

LCDEkran

Ekran

Slika 19.1. Relacija biti

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.
Potrebno je biti oprezan prilikom odreivanja odnosa izmeu pojedinih klasa
te pokuati vrlo precizno odrediti je li neki odnos tipa biti ili posjedovati.
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.
19.6.1. Odnosi objekata u korisnikom suelju

U nastavku emo pokuati primijeniti navedena pravila kako bismo precizno odredili
odnose izmeu objekata koje smo izveli u prethodnom poglavlju.
Poet emo s klasom Ekran. Ona opisuje fiziku izlaznu jedinicu koja slui za
komunikaciju s korisnikom. Svakom ekranu je pridruena slika koja se sastoji od niza
toaka. Moemo definirati klasu Slika koja e opisivati sliku na ekranu. Ona e
omoguavati radnje kao to su postavljanje boje odreene toke na ekranu, itanje boje
neke toke i slino. Takoer e sadravati mehanizme za podravanje razliitih
razluivosti.

566

Iz navedenog se moe lako zakljuiti da e svaki ekran posjedovati tono jednu


sliku (i ta slika e biti pridruena samo tom jednom ekranu). Dakle, veza je tipa jedan
na jedan te dok postoji ekran postoji i slika i obrnuto.
Kada smo pokuali razmotriti nain na koji funkcionira veina ekrana, rekli smo da
mnogi ekrani ne mogu prikazati sve boje odjednom. Zbog toga svaki ekran odrava
paletu boja tablicu u kojoj su navedene pojedine boje. Ustanovili smo da je paleta vrlo
pogodna za apstrakciju pa smo uveli klasu Paleta koja e omoguavati operacije kao
to su unos boje u paletu i iitavanje boje u pojedinom registru palete. Svaki Ekran e
sadravati tono jednu klasu Paleta. Ta e veza takoer biti jedan na jedan. Takoer,
dok postoji ekran postoji i paleta i obrnuto, pa je i ova veza slina prethodnoj.
Slika se sastoji od niza toaka. Toka je bila definirana kao najmanja jedinica
prikaza te e se prikazati klasom Tocka. Moemo rei da e svaka slika posjedovati
odreen broj toaka. Pokuajmo ustanoviti malo detaljnije tip te veze.
Broj toaka u pojedinoj slici ovisi o rezoluciji u kojoj grafiki podsustav raunala
trenutno radi. Zbog toga e i broj toaka koji e slika posjedovati varirati od sluaja do
sluaja. Klasa Slika mora ostvariti mehanizme kojima e prilagoditi svoje ponaanje
pojedinom modu rada raunala. Veza izmeu toaka i slike e biti vie na jednu.
Na ovom mjestu valja naglasiti smisao gornje veze. Mi nipoto ne pokuavamo
sugerirati da se problem grafikog suelja treba rijeiti tako da se u memoriji alocira
prostor za broj toaka jednak broju toaka kojim raspolae grafiki podsustav te da se
crtanje provodi postavljanjem boja pojedinih toaka. Takvo rjeenje e biti vrlo daleko
od idealnog: osim to e zauzimati veliku koliinu memorije (na primjer za razluivost
od 800 600 toaka u 16 boja potrebno bi bilo odvojiti najmanje
800 600 0.5 = 240000 bajtova), bit e iznimno sporo. Nema smisla odvajati dodatnu
memoriju kada to ve ini grafiki adapter za nas. Ovdje govorimo o apstrakcijama:
Slika se vee za 800 600 = 480000 objekata klase Tocka. No nita se ne kae o
implementaciji te veze. U gornjim definicijama nije nigdje specificirano da e se slika
realizirati tako da e odvojiti zaseban memorijski prostor za te toke. Ta veza je posve
apstraktna.
Klasa Slika mora omoguiti suelje kojim e se korisnicima klase (primjerice
klasama Ekran ili Prozor) mehanizam prikaza slike prezentirati na opisani nain. Ako
klasa Slika omoguava da se funkcijskim lanom pristupi odreenoj toki, onda ona
mora svojim unutarnjim mehanizmom to osigurati. eljeno se moe postii na vie
naina i odvajanje memorije za svaku toku je sigurno jedan od njih, no on je daleko od
idealnog. Jedna znatno bolja mogunost je da klasa Slika, nakon upita za odreenom
tokom na nekom mjestu na slici, proita vrijednost pohranjenu u memorijskom
spremniku ureaja za prikaz, tu vrijednost upakira u objekt klase Tocka te taj objekt
vrati pozivatelju. Takva implementacija e biti znatno kvalitetnija, jer nee traiti bilo
kakvu dodatnu dodjelu memorije. Pri tome je integritet apstrakcije ouvan: svaka slika
se moe promatrati kao niz odreenog broja toaka. Stvar je implementacije, a ne
apstrakcije, kako e se takav model realizirati.
Klasa Tocka e biti razmjerno jednostavna: ona modelira indeks boje iz palete te ne
sadri niti jednu drugu klasu.

567

Vratimo se na klasu Paleta. Ako pomnije razmotrimo njeno ustrojstvo, moemo


zakljuiti da se svaka paleta sastoji od niza boja. Zbog toga je prikladno uvesti klasu
Boja koja e opisivati pojedinu stavku u paleti. Takva klasa mora omoguavati
jednoznano definiranje pojedine boje. Najjednostavnije se ini uvesti klasian RGB
koordinatni sustav boja te rei da se Boja sastoji od tri podatkovna lana koji
specificiraju udio crvene, zelene i plave boje. No takva definicija previe zadire u
implementaciju. Iako je RGB sustav boja vjerojatno najprikladniji za koritenje na
ekranima raunala, to ne mora biti jedini izbor. Ponekad nam moe biti vrlo prikladno
da se boja prikae u HSB sustavu (gdje se svaka boja prikazuje pomou tona, zasienja i
svjetline) ili ak CMY sustavu (gdje se boja prikazuje pomou udjela modrozelene,
purpurne i ute). To moe biti korisno elimo li boje na ekranu uskladiti s bojama na
pisau. Ako se drimo gornje definicije, bit e teko uvesti novi koordinatni sustav boja
zato jer e implementacija biti poznata okolnom svijetu. Uvijek e postojati opasnost da
neki objekt pristupi direktno implementaciji, naruavajui integritet objekta. Bolje je
rei da klasa Boja osigurava mehanizme kojima se pojedina boja moe prikazati u bilo
kojem koordinatnom sustavu: RGB, HSB ili CMY. Njena implementacija pri tome
moe biti potpuno skrivena od okolnog svijeta.
Klasa Prozor e definirati osnovna svojstva prozora. No ona e istodobno sluiti i
kao osnovna klasa pomou koje e biti mogue izvoditi nove klase za opis prozora s
pojedinim svojstvima.
U prethodnom smo odjeljku napomenuli da e klasa Prozor oznaavati openiti
prozor bez okvira i elemenata koji omoguavaju posebne akcije, kao to su poveavanje
i smanjenje, zatvaranje i slino. Takoer, klasa Prozor ne podrava izbornike. No rekli
smo da emo taj problem rijeiti tako to emo openiti prozor dodatno tipizirati. Uvest
emo podklasu UokvireniProzor koja e dodati eljenu funkcionalnost.
Odnos klase UokvireniProzor i klase Prozor je tipian primjer relacije biti.
Svaki uokvireni prozor istodobno je i prozor, dok obrat ne vrijedi. UokvireniProzor
e se najbolje implementirati tako da se naslijedi klasa Prozor i doda eljena
funkcionalnost.
Svaki prozor ima niz dodatnih elemenata koji omoguavaju posebnu manipulaciju
prozorima: zatvaranje, poveanje, maksimizaciju, minimizaciju, sistemski izbornik i
slino. Zbog toga bi se moglo postaviti pitanje je li moda bilo pogodnije za svaki od tih
funkcionalnih elemenata izvesti zasebnu klasu koja e opem prozoru dodati eljeno
svojstvo. Prozor koji ima vie tih elemenata (primjerice i element za zatvaranje i
element za promjenu veliine) mogao bi se realizirati viestrukim nasljeivanjem
pojedinih klasa.
Postoji vie razloga zato takve implementacije treba izbjegavati. Gornjim
postupkom uvodi se velika konfuzija u hijerarhijsko stablo te se dodatno komplicira
dizajn korisnikog suelja. Moda najoitija pogreka je u tome to ako naslijedimo
klase koje uvode tri elementa, na primjer S_PromjenomVelicine, S_Izbornikom i
S_Zatvaranjem, rezultirajui objekt e imati u sebi tri podobjekta klase Prozor.
Takvo hijerarhijsko stablo se moe vidjeti na slici Error! Reference source not found..

568

Prozor

Prozor

Prozor

S_PromjenomVelicine

S_Izbornikom

S_Zatvaranjem

MojProzor

Slika 19.2. Pogreka prilikom izvoenja klase MojProzor

Ta mana bi se mogla ispraviti tako da se klasa Prozor uini virtualnom prilikom


izvoenja klase S_PromjenomVelicine, S_Izbornikom i S_Zatvaranjem. Time bi
sve klase izvedene iz tih klasa imale samo po jedan podobjekt Prozor, kako je
prikazano hijerarhijskim stablom na slici 19.3.
Prozor

S_PromjenomVelicine

S_Izbornikom

S_Zatvaranjem

MojProzor

Slika 19.3. Popravak pogreke koritenjem virtualnog nasljeivanja

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

Jo se jednostavnije rjeenje moe postii ako kaemo da je nazonost ili odsutnost


pojedinog elementa na prozoru jednostavno atribut prozora. Prilikom stvaranja prozora
potrebno je odrediti koji su elementi prisutni (na primjer, pomou dodatnog parametra
konstruktoru klase), a zatim se sam prozor brine za njihovo ispravno funkcioniranje. Na
taj nain smo razliito ponaanje pojedinih objekata klase rijeili parametrizacijom
objekata.
Preostaje nam jo razmotriti strukturu i veze izmeu objekata koji e sainjavati
izbornike. Izbornik emo predstaviti pomou klase Izbornik. Prvo mogue pitanje jest
kakva je veza izmeu izbornika i prozora. Ako se malo bolje razmisli, moe se vidjeti
da izbornik nije nita drugo nego prozor s posebnom namjenom. Uloga izbornika je
prikazivanje izbora u tono odreenom podruju na ekranu. Dakle, moglo bi se rei da
je Izbornik tip izveden iz tipa Prozor realizacija pomou javnog nasljeivanja
odmah pada napamet.
No javno nasljeivanje pretpostavlja da se cijelo suelje osnovne klase ukljui u
izvedenu klasu. Javno suelje izbornika e sigurno biti drukije nego javno suelje
prozora. Pravilnije je i jednostavnije za koritenje odvojiti suelje izbornika od suelja
prozora, jer se ta dva objekta dosta razlikuju po nainu koritenja. Zbog toga je bolje
Izbornik izvesti pomou privatnog nasljeivanja. Suelje prozora se time skriva, a
koritenje izbornika se omoguava pomou novog, prikladnijeg suelja.
Nadalje, izbornik nikada ne postoji sam za sebe. On je uvijek dodijeljen nekom
prozoru koji vodi rauna o prikazivanju izbornika i njegovom povezivanju s
aplikacijom. Ve smo ranije naveli da nee svaki prozor imati izbornik, nego samo
prozori definirani klasom UokvireniProzor. Pri tome prozor moe imati izbornik, ali
i ne mora. Konceptualno, prozor sadri izbornik, no to sadravanje se nee rjeavati tako
da se definira podatkovni lan tipa Izbornik. Bolje je vezu s izbornikom rijeiti
pokazivaem na izbornik. Nul-vrijednost pokazivaa moe oznaavati da prozor nema
izbornika.
Kao pouku gornjeg primjera vano je razumjeti da koncepcijske veze izmeu
objekata nisu niti na koji nain strogo vezane s nainom implementacije. Javno
nasljeivanje najee oznaava tipizaciju objekata, no to ne mora biti iskljuivo
pravilo. Takoer, posjedovanje objekta se u principu rjeava pomou podatkovnih
lanova, no moe se rijeiti i na bilo koji drugi nain. Bitno je ispravno postaviti veze
izmeu objekata te na kraju napraviti implementaciju koja e odgovarati apstraktnom
modelu.
Naposljetku, tu su nam jo klase Mis i Tipkovnica. Slino klasi Ekran, oni
modeliraju stvarne ureaje u raunalu. Pomou njih preostali objekti mogu doznati
trenutnu poziciju mia ili itati koje se tipke pritiu.

19.7. Korak 4: Definicija implementacijski zavisnih


apstrakcija
Apstrakcije koje smo do sada izveli i veze izmeu njih dosta dobro opisuju suelje nae
biblioteke klasa prema korisniku. No jo nita ne znamo o nainu na koji se postie

570

harmonian rad svih komponenti. Prilikom razmatranja mogunosti implementacije


moe se doi do novih apstrakcija. Da bi se pojasnio model koje te apstrakcije
zastupaju, potrebno je ponovo proi korake od 1 do 3.
U prethodnom odsjeku smo rekli da klase Mis i Tipkovnica omoguavaju
pojedinim objektima pristup do stvarnog ureaja u raunalu. Meutim, nita nije reeno
o tome kako e se to napraviti. Prvo mogue rjeenje koje pada na pamet moe biti da se
jednostavno kae da klasa Prozor koristi objekt klase Mis i objekt klase Tipkovnica
za itanje ulaznih podataka. No stvari nisu tako jednostavne.
Mi i tipkovnica su ureaji koji su zajedniki za sve programe koji se izvode na
raunalu. Zbog toga se u sustavima koji koriste prozore uvodi konvencija po kojoj se
tipkovnica privremeno dodjeljuje trenutno aktivnom prozoru. Prozor se moe postaviti
aktivnim pomou mia tako da se klikne tipkom mia unutar prozora. Takoer, pomaci
mia i pritisci na tipke mia se uvijek prosljeuju prozoru iznad kojeg se mi trenutno
nalazi.
Zbog navedenog je potrebno uvesti podsustav koji e obavljati gore opisane
poslove. On e odravati listu postojeih prozora te e stalno itati pomake mia. Ako se
klikne miem u podruju nekog prozora koji nije aktivan, on e automatski deaktivirati
do tog trenutka aktivan prozor i aktivirat e prozor iznad kojeg je tipka mia pritisnuta.
Takoer, podsustav e itati tipkovnicu te e automatski usmjeravati sve pritiske na
tipke u trenutno aktivan prozor. Dakle, radi se o nekoj vrsti posrednika izmeu stvarnih
ureaja i prozora.
Takav podsustav e se opet prikazati jednom klasom koja e se instancirati tono
jednom nakon pokretanja programa. Idealno bi zapravo bilo definirati takav podsustav
na razini operacijskog sustava, no radi jednostavnosti neemo se uputati u vode
sloenog sistemskog programiranja. Nazvat emo tu klasu Prometnik objekti te klase
se brinu za promet podataka unutar raunala.
Pokuajmo odrediti s kojim objektima klasa Prometnik dolazi u interakciju, te na
koji nain. Prvo i osnovno, prozori ne smiju imati direktan pristup do mia i tipkovnice.
Klasa Prometnik e imati iskljuivo pravo itanja tih podataka. Nadalje, ta klasa e biti
u relaciji koristiti sa svim prozorima na zaslonu. Svaki prozor se mora prilikom svog
stvaranja prijaviti u prometniku listu. Ta veza traje samo do trenutka kada se prozor
unitava tada je potrebno ukloniti prozor iz liste. U suprotnom, moe doi do velikih
komplikacija pokua li Prometnik obavijest o pritisnutoj tipki proslijediti
nepostojeem prozoru. Veza izmeu prometnika i prozora je, dakle, dvosmjerna.
Postavlja se pitanje na koji nain e se ostvariti veza izmeu prometnika i prozora.
Jedan od najprikladnijih naina za primjenu u C++ jeziku jest uvesti u klasu Prozor po
jedan funkcijski lan za svaki tip dogaaja koji prometnik moe signalizirati prozoru.
Tako emo imati lanove koji signaliziraju pokretanja mia, pritisak lijeve tipke mia,
pritisak desne tipke mia te pritisak na tipku. Po potrebi bi se prometnik mogao proiriti
na obraivanje dodatnih dogaaja kao to su protjecanje odreenog vremenskog
intervala ili neki sistemski dogaaj kao to je primjerice ubacivanje nove diskete u
disketnu jedinicu. Za svaki od tih dogaaja bilo bi potrebno uvesti novi funkcijski lan u
klasu Prozor kojeg e Prometnik pozivati po nastupu dogaaja.

571

19.8. Korak 5: Definicija suelja


Do sada smo ugrubo opisali hijerarhiju objekata koji e sainjavati biblioteku
korisnikog suelja. Razmotrili smo skup apstrakcija koje emo koristiti za na sustav
te smo pokuali to preciznije definirati svrhu pojedine apstrakcije. Sada smo doli do
toke kada je potrebno precizno definirati suelje pojedine klase te definirati nain na
koji e objekti meusobno komunicirati.
Ako bismo se eljeli strogo drati pravila objektnog programiranja, morali bismo
suelje definirati bez razmiljanja o implementaciji. No to esto nije sasvim mogue.
Dosljedno provoenje takvog pristupa rezultiralo bi nepotrebnim konverzijama
podataka izmeu unutarnjeg formata definiranog implementacijom i formata kojeg
koristi suelje. Nije uvijek dobar princip rtvovati uinkovitost programa (pogotovo ako
je brzina obrade podataka vrlo vana, kao na primjer kod aplikacije za obradu podataka
u stvarnom vremenu) da bi se potivalo objektno ustrojstvo programa. Takoer,
prilikom razmatranja o vezama izmeu objekata, neminovno se automatski razmilja i o
nainu implementacije i o elementima suelja.
Dijelove suelja smo dotakli u neformalnom obliku ve u prethodnim poglavljima.
Naime, pojedini koraci u razvoju objektnog modela se ne mogu provoditi izolirano, ve
su isprepleteni. Kada se govori o odnosima izmeu klasa, veina programera odmah
razmilja i o suelju i o implementaciji. Svrha postupka kojeg smo provodili do sada
nije bili sasvim odvojiti pojedine korake, ve naglasiti vane toke u razvoju.
Za uspjenu primjenu objektno orijentiranog pristupa modeliranju sustava vrlo je
vano to dosljednije provoditi gornji postupak kojim se implementacija razdvaja od
suelja. Taj postupak se naziva skrivanje podataka (engl. data hiding): svaki objekt
prua okolini upravo onoliko informacija koliko je potrebno da bi okolina mogla
uspjeno komunicirati s njime. Implementacijski detalji su skriveni, jer okolina o njima
niti ne mora voditi rauna.
Programi pisani tako da se strogo pridravaju principa skrivanja informacija vrlo
rijetko omoguavaju direktan pristup podatkovnim lanovima. Podatkovni lanovi su
implementacija, a suelje se najee osigurava pomou funkcijskih lanova. Klasa
Boja bi se tada mogla implementirati ovako:
class Boja {
private:
unsigned short udioR, udioG, udioB;
public:
Boja(unsigned short R = 0, unsigned short G = 0,
unsigned short B = 0) : udioR(R), udioG(G),
udioB(B) {}
// suelje za RGB sustav
unsigned short dajR() { return
unsigned short dajG() { return
unsigned short dajB() { return
void postaviRBG(unsigned short

udioR; }
udioG; }
udioB; }
R, unsigned short G,

572

unsigned short B) {
udioR = R; udioG = G; udioB = B;
}
// suelje za HSB sustav
unsigned short dajH();
unsigned short dajS();
unsigned short dajBr();

//
//
//
void postaviHSB(unsigned short
unsigned short

dajB je ve deklariran,
pa moramo koristiti naziv
dajBr
H, unsigned short S,
B);

// suelje za CMY sustav


unsigned short dajC();
unsigned short dajM();
unsigned short dajY();
void postaviCMY(unsigned short C, unsigned short M,
unsigned short Y);
};

U gornjem primjeru za implementaciju je odabran RGB sustav boja, no pristup


podatkovnim lanovima nije omoguen. Naprotiv, bojama se pristupa pomou
funkcijskih lanova. Kako su ti lanovi definirani kao umetnuti, dobar prevoditelj e
generirati kd koji se nee izvoditi sporije nego kd koji direktno pristupa podatkovnim
lanovima. Ako je potrebno boju proitati u nekom drugom sustavu, pozvat e se
funkcijski lan koji e provesti pretvorbu u taj sustav. Ti lanovi nisu ovdje definirani
te nisu umetnuti, jer je pretvorba sustava boja vrlo sloen postupak koji nema smisla
umetati na svako mjesto gdje se trai pristup primjerice H komponenti.
Prednost ovakvog pisanja programa je u tome to, ako se zbog nekog razloga
implementacija promijeni, suelje ostaje isto. Svi objekti koji koriste ovu klasu moi e
pristupati R, G i B lanovima neovisno i na isti nain. Stvar je implementacije da ostvari
nain da takve podatke uini dostupnima.
Uzmimo, na primjer, da piemo program koji vrlo intenzivno barata s bojama u
drugim koordinatnim sustavima. Zbog toga e este pretvorbe sustava imati utjecaja na
brzinu izvoenja programa. Programer koji razvija klasu Boja moe promijeniti
implementaciju tako da doda jo est podatkovnih lanova u kojima e se pamtiti
koordinate boje u svim sustavima. Prilikom postavljanja boje u bilo kojem sustavu,
automatski e se zadana boja preraunati u sve ostale te se tako uiniti brzo dostupnim
bez potrebe za naknadnim preraunavanjem. Ako korisnik klase sada pokua pristupati
bojama u drugom sustavu, primijetit e da se program bre izvodi. No kljuno je da se
bojama pristupa na isti nain. Da je pristup podatkovnim lanovima bio omoguen
korisnicima klase, ovakvo poboljanje ne bi bilo izvedivo jer bi nakon svakog
postavljanja podatkovnih lanova u RGB, korisnici morali sami pretvarati sustave i
voditi brigu o takvim kuanskim poslovima (Pa da, cijeli dan samo pretvaram
sustave, a na kraju me niti van ne izvodi...).

573

Obratite panju na konstruktor iz gornjeg primjera: on automatski postavlja boju u


RGB sustavu. Koritenjem takvog konstruktora omoguena je sintaksa po kojoj se
mogu zadavati konstante boja, kao u primjeru:
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.
Poziv virtualnog lana najee se realizira pomou indirekcije (poziva
preko pokazivaa) koji se izvodi sporije od direktnog poziva. Virtualan lan
se ne moe uiniti umetnutim, ime se jo vie degradira uinkovitost
programa.
Vratimo se na primjer klase Boja i promotrimo jesmo li ispravno odredili tip
funkcijskih lanova. Prvo je pitanje da li uope oekujemo da e ikad biti potrebno
naslijediti klasu Boja. Ta je klasa razvijena iskljuivo kao pomona klasa za klasu
Paleta. Teko je zamisliti koji bi bio smisao u nasljeivanju klase Boja. Zbog toga
nema niti smisla govoriti o virtualnim funkcijskim lanovima njima bismo samo
znatno usporili izvoenje programa.
Razmotrimo suelje klase Paleta. Velika je slinost izmeu palete i niza paleta
zapravo i jest niz boja. Zbog toga su operacije nad paletom sline operacijama s
nizovima: potrebno je definirati veliinu palete te osigurati pristup pojedinim
lanovima.
Veliina palete se moe definirati ve prilikom njenog stvaranja, dakle u
konstruktoru. Takoer, korisnik prilikom rada s raunalom moe htjeti promijeniti broj

574

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);
void BrojBoja(int brBoja);
int DajBrojBoja();
?? operator [](int indeks);

// nismo jo odredili
// povratni tip

};

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);
// ...
};

to smo dobili ovakvim rjeenjem? Vraanjem reference na objekt smo automatski


omoguili da se operator [] nae s lijeve strane znaka pridruivanja. Time bi bilo
omogueno postavljanje boja u paletu po sljedeem principu:
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);
void BrojBoja(int brBoja);
int DajBrojBoja();
Boja operator [](int indeks);
void PostaviBoju(int indeks, Boja &novaBoja);
};

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:
Tocka(int ind = 0); // konstruktor i konverzija
// int Tocka
operator int();
// konverzija Tocka int
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

Vratimo se na klasu Slika. Ona mora posjedovati mehanizme za itanje


razluivosti u x i y smjeru, postavljanje razluivosti te niz rutina za iscrtavanje
elemenata slike: toaka, linija, pravokutnika i sl. Iako e komercijalno dobavljiva klasa
Slika omoguavati rad i s egzotinijim grafikim elementima kao to su bit-mape,
ikone itd., radi jednostavnosti emo se ograniiti na toke, linije i pravokutnike.
Takoer, neemo komplicirati s mogunostima kao to su odsijecanje dijela slike izvan
nekog zadanog podruja, pomak i/ili rastezanje koordinatnog sustava i slino. Naprotiv,
operacija koja e proitati toku na odreenom mjestu na ekranu je neophodna. Na
kraju, Slika e imati konstruktor i destruktor koji e ispravno inicijalizirati objekt
poetnim nainom prikaza te ga uredno poistiti.
Valja jo razmisliti moraju li pojedini lanovi klase biti deklarirani virtualnima ili
ne, odnosno, oekuje li se nasljeivanje klase. Na prvi pogled veina e korisnika rei
da ne oekuje da e klasa Slika biti naslijeena: na kraju, slika je prikazana na ekranu
raunala i tu nema mnogo razmiljanja. No ne mora uvijek biti tako.
Na tritu postoji mnotvo grafikih kartica od kojih svaka ima niz razliitih
mogunosti. Ima smisla, stoga, klasu Slika vezati uz odreenu grafiku karticu te na
taj nain osigurati izvoenje svake operacije na adekvatan nain. Takvim pristupom se
slue mnogi suvremeni operacijski sustavi: operacije zajednike svim karticama izlue
se na jedno mjesto te se time definira ope korisniko suelje kojim se slue aplikacije.
Za svaku karticu se napie odgovarajui pogonitelj (engl. driver) koji za nju definira
nain na koji se pojedina operacija realizira. Iako se dananji operacijski sustavi ne
koriste objektnim tehnologijama za postizanje tog cilja, gore opisano rjeenje klase
Slika omoguava upravo to. Klasa Slika moe biti definirana kao apstraktna klasa
(to znai da su njeni funkcijski lanovi definirani kao iste virtualne funkcije njihova
definicija se preputa izvedenim klasama). Svrha klase Slika jest definiranje suelja;
svaka izvedena klasa predstavlja jedan pogonitelj.
Pomou gore navedenih relacija mogue je jednostavno ostvariti izvoenje
programa na jednom raunalu, a prikaz slika na drugom raunalu (slino XWindowsima na UNIX operacijskim sustavima). Moemo definirati klasu
MreznaSlika izvedenu iz klase Slika koja e umjesto iscrtavanja slike na ekranu
raunala podatke o slici upakirati i odgovarajuim protokolom poslati preko mree na
drugo raunalo koje e te podatke otpakirati i prikazati na zaslonu. Takav sustav moe
biti vrlo koristan, jer pomou njega moemo pokrenuti programe na vie raunala, a
njihovo izvoenje pratiti na jednom raunalu (moe biti vrlo praktino u sluaju
obavljanja sloenog prorauna koji se provodi na vie raunala u mrei).
Vano je shvatiti da klasa Slika, slino klasi Paleta, slui kao omota oko
suelja stvarnih sklopova u raunalu. Implementacija nije niti na koji nain
prejudicirana. Evo mogue deklaracije klase:
class Slika {
friend class Ekran;
protected:
void PostaviPaletu(Paleta *pal);
public:

577

Slika(int pocetniNacin);
virtual ~Slika();
virtual
virtual
virtual
virtual

int DajXRazlucivost();
int DajYRazlucivost();
int DajBrojBoja();
void PostaviNacinPrikaza(int nacin);

virtual void CrtajTocku(int x, int y, Tocka &toc);


virtual Tocka CitajTocku(int x, int y);
virtual void Linija(int x1, int y1, int x2, int y2,
int indeksBoje);
virtual void Kvadrat(int x1, int y1, int x2, int y2,
int indeksBoje);
};

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.

19.9. Korak 6: Implementacija


Naposljetku, navedene klase je potrebno implementirati, to znai dodati potrebne
podatkovne lanove, napisati kd funkcijskih lanova, ugraditi eljene algoritme i
slino. Ako smo postavili dobar temelj u prethodnim koracima, sama implementacija ne
bi trebala prouzroiti velike probleme.

579

Gornju opasku nipoto ne treba shvatiti na nain da e implementacija biti sama po


sebi jednostavna; sloenost implementacije e direktno ovisiti o sloenosti samog
modela koji elimo opisati pomou objekata. Ono to smo mislili rei gornjom tvrdnjom
jest da e biti jasno to se eli napraviti. Naime, ako smo proli dosljedno i temeljito
kroz prethodne korake, sada imamo ve dobru sliku o tome to koji objekt radi, pa ak i
intuitivnu predodbu o tome kako on to radi. No jo uvijek nemamo garanciju da ako
smo kvalitetno proveli prethodnih pet koraka, da e i implementacija biti kvalitetna.
Naprotiv, sada je sa stanovita suelja i eljenog cilja potrebno izabrati adekvatan
algoritam koji e osigurati eljene performanse sustava.
Prilikom pisanja implementacije vano je voditi rauna o tome da se potuje princip
skrivanja informacija. Korisnik klase mora znati to manje o ustrojstvu klase, vano je
javno suelje. Zbog toga, kvalitetna implementacija e voditi rauna o tome da to vie
skrije svoje detalje. U pisanju vaih C++ programa vodite se narodnom uzreicom:
skrivajte implementaciju kao zmija noge.
Osnovno to se pod time podrazumijeva jest skrivanje podatkovnih lanova.
Podatkovni lanovi su najosjetljiviji dio svakog objekta, jer oni zapravo predstavljaju
unutarnje stanje objekta. Svaki objekt treba osiguravati svoj integritet, to znai da e se
svaka promjena njegovog stanja obavljati tako da objekt u svakom trenutku bude
suvisao. Ako se dodjeljuje vrijednost pojedinom podatkovnom lanu, potrebno je
obaviti provjeru je li ta vrijednost unutar dozvoljenih granica. Zbog toga se sve dodjele
podatkovnim lanovima u principu obavljaju unutar funkcijskih lanova koji obavljaju
sve potrebne provjere.
Ako bi podatkovni lan imao javni pristup, tada bi nesmotreno (ili prljavo)
napisan program mogao promijeniti vrijednost nekog podatkovnog lana tako da objekt
vie ne bi imao smisla. To bi dalje moglo ugroziti funkcioniranje objekta, a preko njega
i funkcioniranje itavog sustava.

580

Prilog

581

571

A. Standardna biblioteka
Doite i odaberite neto iz moje biblioteke,
odagnajte tako svoju tugu.
William Shakespeare, Titus Andronicus (1590)

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.
Tablica A.1. Zaglavlja standardne C++ biblioteke
<algorithm>
<bitset>
<complex>
<deque>
<exception>
<fstream>
<functional>
<iomanip>

<ios>
<iosfwd>
<iostream>
<istream>
<iterator>
<limits>
<list>
<locale>

<map>
<memory>
<new>
<numeric>
<ostream>
<queue>
<set>
<sstream>

<stack>
<stdexcept>
<streambuf>
<string>
<typeinfo>
<utility>
<valarray>
<vector>

ak ne posebno paljiv itatelj e sigurno zamijetiti kako nazivi standardnih datoteka


nemaju nastavak .h koji smo spominjali kroz cijelu knjigu. Vjerujte, i mi smo ostali
iznenaeni kada smo otvorili C++ standard i vidjeli gornju tablicu. Nastavak .h je
jednostavno ukinut! Mnogi C++ prevoditelji nisu jo aurirali nove nazive datoteka, a
mi iskreno sumnjamo da e mnogi to ikad uiniti (jer e propasti kada im razjareni
programeri, kojima se njihovi postojei programi od nekoliko stotina tisua linija
izvornog kda nee moi prevesti, poalju 100000 e-mail protestnih poruka). Mi emo
571

572

samo u Prilogu koristiti ovakve nazive datoteka zaglavlja (jer ih prevoditelji jo ne


podravaju niti nam se da prolaziti kroz knjigu i mijenjati ih), a vama preputamo da
saznate da li prevoditelj kojeg koristite doslovce prati standard.
Standardna biblioteka C jezika je ukljuena i u C++ standard, s time da su
deklaracije i definicije koje ta biblioteka trai smjetene u zasebne datoteke zaglavlja.
Sve C datoteke zaglavlja su navedene u tablici A.2.
Tablica A.2. C++ zaglavlja za C biblioteke
<cassert>
<cctype>
<cerrno>
<cfloat>
<ciso646>

<climits>
<clocale>
<cmath>
<csetjmp>
<csignal>

<cstardg>
<cstddef>
<cstdio>
<cstdlib>
<cstring>

<ctime>
<cwchar>
<cwtype>

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

mjesne zemljopisne (localization) komponente deklarirane u zaglavlju locale, te u


standardnom C zaglavlju clocale.
kontejneri. U ovu kategoriju su smjetene komponente kakve nisu postojale u jeziku
C, odnosno njegovim bibliotekama. Te su komponente opisane u osam zaglavlja:
bits, deque, list, queue, stack, vector, map i set.
iteratori, deklarirani u zaglavlju iterator, ine zasebnu kategoriju komponenti koje
omoguavaju iteraciju kroz kontejnere, tokove i meuspremnike tokova.
algoritmi. U ovoj kategoriji se nalaze komponente neophodne za algoritamske
operacije na kontejnerima i drugim nizovima. Opisane su u zaglavlju algorithm, a
pridruene su im i funkcije bsearch() i qsort() iz C zaglavlja cstdlib.
numerike komponente, namijenjene za obavljanje numerikih operacija. Sadri
komponente za kompleksni tip podataka, numerika polja, te ope numerike
algoritme. Deklarirane su u zaglavljima complex, valarray i numeric, te u C
zaglavlju cmath.
ulazno-izlazne komponente, tj. ulazno-izlazni tokovi, deklarirani u zaglavljima
iosfwd, iostream, ios, streambuf, istream, ostream, iomanip, sstream,
fstream, kao i standardnim C zaglavljima cstdio, te cwchar.
U nastavku emo obraditi vanije komponente iz standardnih biblioteka. Nije nam
namjera obuhvatiti kompletne standardne biblioteke. To bi zahtijevalo jo barem
ovoliko stranica. Takoer, mnoge funkcije i klase nikada neete koristiti u standardnu
biblioteku je zaista ubaeno mnogo toga. Mnoge funkcije su posljedica podravanja
specifinih operacijskih sustava (na primjer, signali su preuzeti s UNIX sustava i
nemaju direktnu podrku u tom obliku u drugim sustavima). Zatim, mnoge funkcije, na
primjer, podrka za lokalizaciju programa, specifine su za pojedini sustav. Programer
koji eli napisati program usko povezan s operacijskim sustavom koristit e
lokalizacijska svojstva ugraena u sam sustav. Zbog svega toga, nakon to nauite C++,
odluite se za operacijski sustav i nabavite knjigu koja e opisati kako pozivati usluge
tog sustava. To e vam biti daleko korisnije nego prouavanje standardne biblioteke.
No ipak, neke stvari iz standardne biblioteke, kao to su kontejnerske klase i neke
standardne funkcije (esto upravo one naslijeene iz jezika C), mogu biti od koristi. Mi
emo u nastavku prikazati ono to smatramo bitnim i to se moe smjestiti na razuman
broj stranica. Za detaljni opis tih klasa itatelja upuujemo na ANSI Standard jezika
C++, na knjigu: P. J. Plauger, The Draft Standard C++ Library (Prentice-Hall, ISBN 013-117003-1, 1995) ili na neki od lanaka u asopisu C++ Report. Osim toga, mnogi
prevoditelji jo nemaju podrane sve te biblioteke, pa tako prije koritenja, provjerite
dokumentaciju prevoditelja.

A.1. Standardne makro funkcije i makro imena


U tablici A.3 navedene su makro funkcije i imena koje standard jezika C++ podrava.
Imena iza kojih slijedi naziv zaglavlja unutar zagrada <> definirana su u svim tim
zaglavljima, s time da su te definicije meusobno ekvivalentne. Sve su makro funkcije i

574

imena naslijeena iz programskog jezika C, a veinu standard podrava iskljuivo zbog


kompatibilnosti sa C-programima.
Tablica A.3. Standardne makro funkcije i imena
assert
BUFSIZ
CLOCKS_PER_SEC
EDOM
EOF
ERANGE
errno
EXIT_FAILURE
EXIT_SUCCESS
FILENAME_MAX
FOPEN_MAX
HUGE_VAL
LC_ALL
LC_COLLATE

LC_CTYPE
LC_MONETARY
LC_NUMERIC
LC_TIME
L_tmpnam
MB_CUR_MAX
NULL <cstddef>
NULL <cstdio>
NULL <cstring>
NULL <ctime>
NULL <cwchar>
offsetoff
RAND_MAX
SEEK_CUR

SEEK_END
SEEK_SET
setjmp
SIGABRT
SIGFPE
SIGILL
SIGINT
SIGSEGV
SIGTERM
SIG_DFL
SIG_ERR
SIG_IGN
stderr
stdin

stdout
TMP_MAX
va_arg
va_end
va_start
WCHAR_MAX
WCHAR_MIN
WEOF <cwchar>
WEOF <cwctype>
_IOFBF
_IOLBF
_IONBF

Slijede opisi nekih vanijih makro imena i funkcija:


#include <cassert>
void assert(int test);

// stari naziv: <assert.h>

Makro funkcija koja testira uvjet test. Ako je rezultat testa nula, prekida se izvoenje
programa pozivom funkcije abort(), a na jedinici za ispis pogreke ispisuje se poruka
s nazivom datoteke izvornog kda i brojem linije u kojoj je pogreka nastupila.
Djelovanje makro funkcije assert()se moe iskljuiti tako da se ispred
pretprocesorske naredbe za ukljuivanje zaglavlja cassert (ili assert.h) definira
makro ime NDEBUG (naredbom #define NDEBUG).
#include <cerrno>
#include <cmath>
EDOM, ERANGE
EDOM
ERANGE

// stari naziv: <errno.h>


// stari naziv: <math.h>

kd za pogreku domene funkcije.


kd pogreke za rezultat izvan opsega.

Gornja dva makro imena koriste se za indikaciju pogreke prilikom poziva


matematikih funkcija. Matematike funkcije prilikom svog izvoenja postavljaju
globalni objekt errno ime signaliziraju je li funkcija ispravno obavila svoj posao. Ako
prilikom poziva funkcije navedemo vrijednost za koju funkcija nije definirana (na
primjer, pokuamo izvaditi logaritam iz negativnog broja), u errno e se upisati EDOM,
a ako je rezultat funkcije uzrokuje preljev, upisat e se ERANGE.

575

#include <cstdio>
EOF

// stari naziv: <stdio.h>

Znakovna konstanta EOF (vrijednost 26 dekadski). U tekstovnim datotekama taj znak


oznaava kraj datoteke (End-Of-File).
#include <cerrno>
errno

// stari naziv: <errno.h>

Globalna cjelobrojna varijabla, koja slui za pohranjivanje kda pogreke. esto se


koristi kao provjera kod poziva matematikih funkcija, koje mu u sluaju pogreke
pridruuju vrijednost EDOM ili ERANGE.
#include <cstdlib>
EXIT_FAILURE, EXIT_SUCCESS
EXIT_FAILURE
EXIT_SUCCESS

// stari naziv: <stdlib.h>

nepravilan prekid programa.


normalan zavretak programa.

Konstante koje se navode kao argumenti za funkciju exit().


#include <cmath>
HUGE_VAL
HUGE_VAL

// stari naziv: <math.h>

preljev vrijednosti matematikih funkcija.

Ovu vrijednost vraaju matematike funkcije kada signaliziraju da je povratna


vrijednost prevelika za ugraeni opseg brojeva.
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cwchar>
NULL

// stari naziv: <stddef.h>


// stari naziv: <stdio.h>
// stari naziv: <string.h>
// stari naziv: <time.h>
// stari naziv: <wchar.h>

Vrijednost nul-pokazivaa.
#include <cstdlib>
RAND_MAX

// stari naziv: <stdlib.h>

Najvea vrijednost koju moe vratiti funkcija rand(). Funkcija rand() slui
generiranju pseudo-sluajnih brojeva.

576

#include <cstdarg>
void va_start(va_list ap, lastfix);
type va_arg(va_list ap, type);
void va_end(va_list ap);

// stari naziv: <stdarg.h>

va_start() postavlja polje ap (tipa va_list koji je takoer definiran u istom

va_arg()
va_end()

zaglavlju) tako da pokazuje na prvi neodreeni argument proslijeen


funkciji. Drugi argument je ime zadnjeg fiksnog argumenta koji se
prenosi funkciji s neodreenim argumentima. va_start() se mora
pozvati prije va_arg() i va_end().
vraa sljedei argument iz liste ap stvarno proslijeenih argumenata.
osigurava regularan povratak iz funkcije s neodreenim argumentima.
va_end() treba obavezno pozvati nakon to su makro funkcijom
va_arg() oitani eljeni proslijeeni argumenti.

Gornje makro funkcije slue za dohvaanje argumenata funkcija s neodreenim brojem


argumenata. Primjer primjene ovih makro funkcija dan je u odsjeku 5.4.8 posveenom
funkcijama s neodreenim argumentima.

A.2. Standardne vrijednosti


Jezik C++ nasljeuje 45 standardnih vrijednosti iz standardne biblioteke jezika C,
navedenih u tablici A.4. Vrijednosti definiraju duljine, najvee i najmanje vrijednost za
pojedine ugraene tipove. Za cjelobrojne tipove navedene su u zaglavlju limits.h, a
za realne u zaglavlju float.h.
Tablica A.4. Standardne vrijednosti
CHAR_BIT
CHAR_MAX
CHAR_MIN
DBL_DIG
DBL_EPSILON
DBL_MANT_DIG
DBL_MAX
DBL_MAX_10_EXP
DBL_MAX_EXP
DBL_MIN
DBL_MIN_10_EXP
DBL_MIN_EXP

FLT_DIG
FLT_EPSILON
FLT_MANT_DIG
FLT_MAX
FLT_MAX_10_EXP
FLT_MAX_EXP
FLT_MIN
FLT_MIN_10_EXP
FLT_MIN_EXP
FLT_RADIX
FLT_ROUNDS
INT_MAX

INT_MIN
LDBL_DIG
LDBL_EPSILON
LDBL_MANT_DIG
LDBL_MAX
LDBL_MAX_10_EXP
LDBL_MAX_EXP
LDBL_MIN
LDBL_MIN_10_EXP
LDBL_MIN_EXP
LONG_MAX
LONG_MIN

MB_LEN_MAX
SCHAR_MAX
SCHAR_MIN
SHRT_MAX
SHRT_MIN
UCHAR_MAX
UINT_MAX
ULONG_MAX
USHRT_MAX

A.3. Standardni tipovi


Standardna C++ biblioteka definira tipove navedene u tablici A.5. Neki od njih su
naslijeeni iz jezika C. Veina tipova vezanih uz tokove opisana je u poglavlju XX ove
knjige. Neke od preostalih tipova upoznat emo kroz opise funkcija koje ih koriste.

577

Tablica A.5. Standardni tipovi


clock_t
div_t
FILE
filebuf
fpos_t
ifstream
ios
istream
istringstream
jmp_buf
ldiv_t
mbstate_t
new_handler
ofstream
ostream
ostringstream

ptrdiff_t <cstddef>
sig_atomic_t
size_t <cstddef>
size_t <cstdio>
size_t <cstring>
size_t <ctime>
streambuf
streamoff
streampos
string
stringbuf
terminate_handler
time_t
unexpected_handler
va_list
wctrans_t

wctype_t
wfilebuf
wifstream
wint_t <cwchar>
wint_t <cwctype>
wios
wistream
wistringstream
wofstream
wostream
wostringstream
wstreambuf
wstreampos
wstring
wstringbuf

A.4. Standardne klase i strukture


Posebno mjesto u C++ biblioteci zauzima standardna biblioteka predloaka (Standard
Template Library, STL), tablica A.6. Radi se o 66 raznovrsnih klasa koje, izmeu
ostalog, ukljuuju klasu znakovnih nizova, kontejnerske klase, klasu kompleksnih
brojeva, klasu polja brojanih vrijednosti. Ovdje emo samo ukratko opisati te klase
potpuni opis svih tih klasa iziskivao bi jo stotinjak stranica teksta.
Kontejneri su openito namijenjeni za pohranjivanje drugih objekata. Oni
kontroliraju alokaciju i dealokaciju tih objekata pomou konstruktora, destruktora, te
operatora umetanja i brisanja. Kontejneri se mogu podijeliti u dvije grupe: nizove i
asocijativne kontejnere.
Nizovi slau konaan broj elemenata istog tipa u strogo linearnom rasporedu.
Kontejnerska biblioteka prua na raspolaganje tri tipa nizovnih kontejnera: vector,
list i deque (deklarirana u svojim pripadajuim zaglavljima, vidi tablicu A.1). Klasa
vector je kontejner koji se koristi u najopenitijim sluajevima ona omoguava
proizvoljan pristup pojedinim lanovima te jednostavna dodavanja i brisanja elemenata
na kraju niza; za dodavanja i brisanja unutar niza potrebno vrijeme raste linearno s
udaljenou elementa od kraja niza. Klasa list je pogodna u primjenama kada esto
dodajemo i briemo elemente niza. Trajanje operacija umetanja i brisanja elemenata ne
ovisi o njihovom poloaju u nizu, ali se elementi (za razliku od vector ili deque) ne
mogu dohvaati izravno. Klasa deque je najprikladnija za nizove u kojima se dodavanje
ili oduzimanje elemenata obavlja na poetku ili kraju; operacije dodavanja i brisanja
elemenata unutar niza iziskuju vrijeme proporcionalno udaljenosti od kraja niza.
Asocijativni kontejneri omoguavaju dohvaanje lanova prema nekom kljuu.
Biblioteka prua na raspolaganje etiri osnovna tipa asocijativnih kontejnera: set,
multiset, map i multimap (deklarirana u zaglavljima set i map). set je kontejner

578

Tablica
Tablica
A.7. Standardne
A.6. Standardna
strukture
biblioteka
definirane
predloaka
predlocima
bidirectional_iterator
allocator
binary_function
auto_ptr
divides
back_insert_iterator
equal_to
basic_filebuf
forward_iterator
basic_ifstream
greater
basic_ios
greater_equal
basic_istream
input_iterator
basic_istringstream
basic_ofstream
basic_ostream
basic_ostringstream
basic_streambuf
basic_string
basic_stringbuf
binary_negate
binder1st
binder2nd
bitset
codecvt
codecvt_byname
collate
collate_byname
complex
ctype
ctype_byname
deque
front_insert_iterator
gslice_array
indirect_array
insert_iterator
istreambuf_iterator
istream_iterator
list

ios_traits
map
negate
less
mask_arraynot_equal_to
less_equal
messages pair
logical_and
messages_byname
plus
logical_not
moneypunctrandom_access_iterator
logical_or
moneypunct_byname
string_char_traits
minus
money_get times
modulus money_put unary_function
multimap
multiset
numeric_limits
numpunct
num_get
num_put
ostreambuf_iterator
ostream_iterator
pointer_to_binary_function
pointer_to_unary_function
priority_queue
queue
raw_storage_iterator
reverse_bidirectional_iterator
reverse_iterator
set
slice_array
stack
time_get
time_get_byname
time_put
time_put_byname
unary_negate
valarray
vector

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

Tablica A.8. Standardne klase


bad_alloc
bad_cast
bad_exception
bad_typeid
basic_string<char>
basic_string<wchar_t>
complex<double>
complex<float>
complex<long double>
ctype<char>

ctype_byname<char>
domain_error
exception
gslice
invalid_argument
ios_base
length_error
locale
locale::facet
locale::id

logic_error
out_of_range
overflow_error
range_error
runtime_error
slice
type_info
vector<bool,allocator>

Tablica A.9. Standardne strukture

bidirectional_iterator_tag
codecvt_base
ctype_base
forward_iterator_tag
input_iterator_tag
ios_traits<char>
ios_traits<wchar_t>
lconv
money_base

money_base::pattern
nothrow
output_iterator
output_iterator_tag
random_access_iterator_tag
string_char_traits<char>
string_char_traits<wchar_t>
time_base
tm <ctime>

580

B. Standardne funkcije
Franklin: Jeste li ikada, gospodine Ravnatelju,
pomislili da su vai standardi moda malo zastarjeli?
Ravnatelj: Naravno da su zastarjeli. Standardi su
uvijek zastarjeli. To je ono to ih ini standardima.
Alen Bennet, engleski glumac i pisac,
Forty Years On (1969)

Standardna C++ biblioteka prua 208 standardnih funkcija iz C biblioteke (tablica B.1).
Veina matematikih funkcija je preoptereena za objekte klase complex. Takoer,

Tablica B.1. Standardne funkcije


abort
abs
acos
asctime
asin
atan
atan2
atexit
atof
atoi
atol
bsearch
btowc
calloc
ceil
clearerr
clock
cos
cosh
ctime
difftime
div
exit
exp
fabs
fclose
feof
ferror
fflush
fgetc

fgetpos
fgets
fgetwc
fgetws
floor
fmod
fopen
fprintf
fputc
fputs
fputwc
fputws
fread
free
freopen
frexp
fscanf
fseek
fsetpos
ftell
fwide
fwprintf
fwrite
fwscanf
getc
getchar
getenv
gets
getwc
getwchar

gmtime
isalnum
isalpha
iscntrl
isdigit
isgraph
islower
isprint
ispunct
isspace
isupper
iswalnum
iswalpha
iswcntrl
iswctype
iswdigit
iswgraph
iswlower
iswprint
iswpunct
iswspace
iswupper
iswxdigit
isxdigit
labs
ldexp
ldiv
localeconv
localtime
log

log10
longjmp
malloc
mblen
mbrlen
mbrtowc
mbsinit
mbsrtowcs
mbstowcs
mbtowc
memchr
memcmp
memcpy
memmove
memset
mktime
modf
perror
pow
printf
putc
puts
putwc
putwchar
qsort
raise
rand
realloc
remove
rename

rewind
scanf
setbuf
setlocale
setvbuf
signal
sin
sinh
sprintf
sqrt
srand
sscanf
strcat
strchr
strcmp
strcoll
strcpy
strcspn
strerror
strftime
strlen
strncat
strncmp
strncpy
stroul
strpbrk
strrchr
strspn
strstr
strtod

strtok
strtol
strxfrm
swprintf
swscanf
system
tan
tanh
time
tmpfile
tmpnam
tolower
toupper
towctrans
towlower
towupper
ungetc
ungetwc
vfwprintf
vprintf
vscanf
vsprintf
vswprintf
vwprintf
wcrtomb
wcscat
wcschr
wcscmp
wcscoll
wcscpy

wscspn
wcsftime
wcslen
wcsncat
wcsncmp
wcsncpy
wcspbrk
wcsrchr
wcsrtomba
wcsspn
wcsstr
wcstod
wcstok
wcstol
wcstomba
wcstoul
wcsxfrm
wctob
wctomb
wctrans
wctype
wmemchr
wmemcmp
wmemcpy
wmemmove
wmemset
wprintf
wscanf

Korak 6: Implementacija

581

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 Cfunkcije 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.

B.1. Funkcije vezane uz znakove i znakovne nizove


#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

povratna vrijednost false. Budui da su funkcije deklarirane pomou predloaka,


charT moe biti bilo koji tip koji se dade svesti na char ili wchar_t. Drugi argument
je referenca na objekt klase locale koja specificira mjesne (zemljopisne) parametre,
kao to su klasifikacija znakova, formati zapisa brojeva, datuma i vremena i sl. Time je
(teoretski) omoguena ispravna klasifikacija naih dijakritikih znakova jedino je
potrebno podesiti objekt klase locale.
isalnum()
isalpha()
iscntrl()
isdigit()
isgraph()
islower()
isprint()
ispunct()
isspace()
isupper()
isxdigit()

je li znak slovo ili dekadska znamenka.


je li znak slovo.
je li znak neki kontrolni znak (ASCII kdovi 0...31 i 127).
je li znak dekadska znamenka ('0'...'9').
je li znak znak koji se ispisuje (slovo, broj i interpunkcija), bez bjeline
' '.
je li znak malo slovo ('a'...'').
je li znak znak koji se ispisuje (slovo, broj i interpunkcija), ukljuujui
bjelinu ' '.
je li znak neki znak za interpunkciju ('.', ',', ';', ':'...).
je li znak praznina (bjelina ' ', tabulator'\t', novi redak '\n', povrat
'\r', pomak papira '\f').
je li znak veliko slovo ('A'...'').
je li znak heksadekadska znamenka ('0'...'9', 'a'...'f', 'A'... 'F').

#include <cctype>
int isalnum(int znak);
int isalpha(int znak);
int iscntrl(int znak);
int isdigit(int znak);
int isgraph(int znak);
int islower(int znak);
int isprint(int znak);
int ispunct(int znak);
int isspace(int znak);
int isupper(int znak);
int isxdigit(int znak);

// stari naziv: <ctype.h>

Ove funkcije su naslijeene iz standardne C biblioteke, gdje su deklarirane u zaglavlju


ctype.h, a povratna vrijednost je tipa int. Ove funkcije ne primaju kao parametar
objekt klase locale, nego se zemljopisna regija odreuje standardnom funkcijom
setlocale(). Pojedine funkcije imaju isto znaenje kao i nove C++ verzije, pa za
objanjenje pogledajte prethodnu grupu funkcija.

Korak 6: Implementacija

583

#include <cstring>
// stari naziv: <string.h>
char *strcat(char *pocetak, const char *nastavak);
char *strncat(char *pocetak, const char *nastavak, size_t maxDuljina);

Funkcija strcat() nadovezuje kopiju niza nastavak na niz pocetak. Funkcija


strncat() radi to isto, ali se preslikava do najvie maxDuljina znakova iz niza
nastavak. Obje funkcije dodaju zakljuni nul-znak na kraj skrpanog niza pocetak,
a kao rezultat vraaju pokaziva na pocetak. Prilikom koritenja valja paziti da je prije
poziva funkcije alociran dovoljan prostor za produljeni niz pocetak, jer funkcija ne
provodi nikakve provjere.
#include <cstring>
// stari naziv: <string.h>
const char *strchr(const char *niz, int znak);
char *strchr(char *niz, int znak);

Funkcija u nizu niz trai prvu pojavu znaka znak. Ako znak nije pronaen, kao rezultat
se vraa nul-pokaziva. U podruje pretraivanja ukljuen je i zakljuni nul-znak.
#include <cstring>
// stari naziv: <string.h>
int strcmp(const char *niz1, const char *niz2);
int strncmp(const char *niz1, const char *niz2, size_t maxDuljina);
int strcoll(char char *niz1, char *niz2);

Funkcija strcmp() usporeuje niz1 i niz2, znak po znak, sve dok su odgovarajui
znakovi meusobno jednaki ili dok ne naie na zakljuni nul-znak. Rezultat usporedbe
je:
< 0 ako je niz1 manji od niz2 (svrstano po abecedi niz1 dolazi prije niz2),
0 ako su niz1 i niz2 meusobno jednaki,
> 0 ako je niz1 vei od niz2 (svrstano po abecedi niz1 dolazi iza niz2).
Usporedba se radi po slijedu znakova koji se koristi na dotinom raunalu. Valja uoiti
da u najeem ASCII slijedu sva velika slova prethode malim slovima.
Funkcija strncmp() radi jednaku usporedbu kao i strcmp(), ali usporeuje samo
do najvie maxDuljina znakova.
Funkcija strcoll() radi usporedbu u skladu sa tekuom postavom zemljopisne
regije. Ta postava se moe podesiti funkcijom setlocale(), pri emu se kao
kategorija postavke navodi LC_COLLATE. Za detaljnije objanjenje pogledajte opis
funkcije setlocale().
#include <cstring>
// stari naziv: <string.h>
char *strcpy(char *odrediste, const char *izvornik);
char *strncpy(char *odrediste, const char *izvornik, size_t maxDuljina);

Funkcija strcpy() preslikava sadraj niza izvornik na mjesto gdje pokazuje


odrediste. Preslikavanje se prekida nakon to se prenese zakljuni nul-znak. Prije

584

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.
#include <cstring>
size_t *strlen(const char *niz);

// stari naziv: <string.h>

Funkcija izraunava i vraa duljinu niza niz, ne raunajui zakljuni nul-znak.


#include <cstring>
// stari naziv: <string.h>
const char *strpbrk(const char *niz, const char *znakovi);
char *strpbrk(char *niz, const char *znakovi);

Funkcija pretrauje niz niz do prve pojave bilo kojeg znaka iz niza znakovi. Kao
rezultat vraa pokaziva na prvu pojavu ili nul-pokaziva ako nijedan znak nije
pronaen.
#include <cstring>
// stari naziv: <string.h>
size_t strspn(const char *niz, const char *podNiz);
size_t strcspn(const char *niz, const char *podNiz);

Funkcija strspn() trai u nizu niz mjesto gdje se spominje prvi znak koji nije
naveden u nizu podNiz. Kao rezultat ona vraa indeks pronaenog znaka. Na primjer,
izvoenjem naredbe
cout << strspn("crnac", "nerc") << endl;

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:
cout << strspn("crnac", "antimon");

Korak 6: Implementacija

585

#include <cstring>
// stari naziv: <string.h>
const char *strstr(const char *niz, const char *podNiz);
char *strstr(char *niz, const char *podNiz);

Funkcija pretrauje niz i trai prvu pojavu niza podNiz (bez njegovog zakljunog nulznaka). Rezultat je pokaziva na poetak prve pojave niza podNiz ili nul-pokaziva ako
podNiz nije sadran u niz-u.
#include <cstring>
// stari naziv: <string.h>
const char *strrchr(const char *niz, int znak);
char *strrchr(char *niz, int znak);

Funkcija pretrauje niz i trai zadnju pojavu znaka znak. Funkcija pretrauje niz od
njegova kraja (ukljuujui i zakljuni nul-znak). Ako je znak pronaen, funkcija vraa
pokaziva na zadnju pojavu znaka u nizu; ako nema znak-a u niz-u, tada vraa nulpokaziva.
#include <cstring>
// stari naziv: <string.h>
char *strtok(char *niz, const char *granicnici);

Funkcija pretrauje niz i razbija ga na podnizove koji su razdvojeni nekim od znakova


iz niza granicnici. Rezultat poziva funkcije je pokaziva na poetak sljedeeg
podniza ili nul-pokaziva ako vie nema podnizova.
eli li se niz razbiti na podnizove, prilikom prvog poziva funkcije treba prenijeti
pokaziva na taj niz. Funkcija e na kraj podniza staviti zakljuni nul-znak. U sljedeim
pozivima, za prvi argument se navodi nul-pokaziva, a funkcija ponavlja postupak za
ostale podnizove, sve do kraja niza. Podniz granicnik se smije mijenjati za pojedine
pozive. Valja naglasiti da se pozivom funkcije strtok() mijenja sadraj niz-a
(graninici se nadomjetaju nul-znakovima). Evo jednostavnog primjera u kojem se
niz rastavlja na podnizove odvojene znakovima ',' ili ' ':
char niz[] = "I cvri,cvri cvrak";
// u prvom pozivu prosljeuje se pokaziva na poetak niza
char *podniz = strtok(niz, ", ");
if (podniz) {
do {
cout << podniz << endl;
// u daljnjim pozivima prvi argument je nul-pokaziva
podniz = strtok(NULL, ", ");
} while(podniz);
}

Izvoenjem gornjeg kda ispisat e se:


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.

B.2. Funkcije za meusobne pretvorbe nizova i brojeva


#include <cmath>
double atof(const char *niz);

// stari naziv: <math.h>

Pretvara znakovni niz niz u realni broj tipa double. Broju u znakovnom nizu smiju
prethoditi praznine. Broj smije biti zapisan u bilo kom formatu dozvoljenom za realne
brojeve, ukljuujui i znanstveni zapis. Prvi nedozvoljeni znak u nizu prekida
konverziju. Funkcija vraa pretvorenu vrijednost ulaznog niza. Ako nastupi brojani
preljev prilikom pretvorbe (primjerice ako je broj u znakovnom nizu prevelik ili
premali), funkcija vraa HUGE_VAL, te postavlja globalnu varijablu errno na ERANGE
(Range Error).
Funkcija atof() je slina funkciji strtod(), ali osigurava bolje prepoznavanje
pogreaka. Izvoenjem naredbi:
char *niz = " -1.23e-4 take money and run";
double broj = atof(niz);
cout << broj << endl;

na zaslonu e se ispisati broj 0.000123.

587

Korak 6: Implementacija

#include <cstdlib>
int atoi(const char *niz);
long atol(const char *niz);

// stari naziv: <stdlib.h>

Funkcija atoi() pretvara znakovni niz niz u cijeli broj tipa int, a funkcija atol() u
cijeli broj tipa long. Broju u znakovnom nizu smiju prethoditi praznine te predznak.
Prvi nedozvoljeni znak prekida konverziju. Funkcije ne provjeravaju pojavu preljeva.
Kao rezultat vraaju pretvoreni cijeli broj, a ako se pretvoreni broj ne moe svesti na
int, odnosno long, vraaju 0. Tako e se izvoenjem primjera
char *niz = "1234.56";
int numera = atoi(niz);
cout << numera << endl;

na zaslonu ispisati broj 1234.


#include <cstdlib>
// stari naziv: <stdlib.h>
double strtod(const char *niz, char **pokazKraja);

Funkcija strtod() pretvara znakovni niz u broj tipa double. U znakovnom nizu niz
koji sadri tekstovni prikaz realnog broja, samom broju smiju prethoditi praznine. Broj
smije biti prikazan u bilo kojem obliku dozvoljenom za prikaz realnih brojeva, bez
praznina unutar broja. Ako je pokazKraja prilikom ulaska u funkciju razliit od nulpokazivaa, tada ga funkcija usmjerava na znak koji je prekinuo slijed uitavanja. Slijed
uitavanja se prekida im se naie na neki znak koji ne moe biti sastavni dio broja.
Stoga e se izvoenjem sljedeeg primjera:
char *niz = " -1.23e-1hahaha", *pokazKraja;
double broj = strtod(niz, &pokazKraja);
// radi ispisa odsijeca se niz iza dozvoljenih znakova
*pokazKraja = '\0';
cout << "Broj " << niz << " je pretvoren u " << broj << endl;

na zaslonu ispisati tekst:


Broj

-1.23e-1 je pretvoren u 0.123

Funkcija strtod() slina je funkciji atof(), ali potonja bolje prepoznaje pogreke
prilikom pretvorbe.
#include <cstdlib>
// stari naziv: <stdlib.h>
long strtol(const char *niz, char **pokazKraja, int baza);

Funkcija pretvara znakovni niz u broj tipa long. U znakovnom nizu niz koji sadri
tekstovni prikaz cijelog broja koji se eli pretvoriti, samom broju smiju prethoditi
praznine. Takoer, neposredno ispred broja smiju se nalaziti predznak ('+' ili '-'),

588

Principi objektno orijentiranog dizajna

'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 nulpokazivaa, 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:
char *niz = " -01188", *pokazKraja;
int baza = 8;
long broj = strtol(niz, &pokazKraja, baza);
// radi ispisa odsijeca niz iza vaeih oktalnih znamenki
*pokazKraja = '\0';
cout << "Broj " << niz << " u bazi " << baza << " je "
<< broj << endl;

B.3. Funkcije vezane uz vrijeme i datum


Funkcije vezane uz vrijeme i datum definirane su u standardnom zaglavlju ctime (stari
naziv time.h). U tom zaglavlju su definirane i dva cjelobrojna tipa: clock_t i
time_t, kao i struktura tm. Valja paziti da neke funkcije obrauju kalendarsko vrijeme
koje se openito razlikuje od lokalnog vremena zbog naina kako se vrijeme pohranjuje
na raunalu ili zbog razlike u vremenskim zonama. Na primjer, na osobnim raunalima
je kalendarsko vrijeme broj sekundi proteklih od 1. sijenja 1970. u pono po GMT
(Greenwich Mean Time).
#include <ctime>
// stari naziv: <time.h>
char *asctime(const tm *vremenskiBlok);
char *ctime(const time_t *pokVrijeme);

Funkcije pretvaraju vrijeme koje im se prenosi kao argument, u znakovni niz oblika:
Wed Jan 15 01:23:45 1997\n\0

Za funkciju asctime() argument je struktura tipa tm (definirana u zaglavlju ctime, a


opisana na str. 590 uz opis funkcije localtime()), dok je povratna vrijednost
znakovni niz. Za funkciju ctime() argument je kalendarsko vrijeme (cjelobrojni tip
podatka kakvog vraa funkcija time()), a povratna vrijednost je znakovni niz u koji je
upisano lokalno vrijeme. U sljedeem primjeru obje naredbe za ispis e ispisati jednake
nizove:

589

Korak 6: Implementacija

time_t vura = time(NULL);


cout << asctime(localtime(&vura));
cout << ctime(&vura);

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.
#include <ctime>
clock_t clock(void);

// stari naziv: <time.h>

Funkcija kao rezultat vraa procesorsko vrijeme proteklo od poetka izvoenja


programa, a ako vrijeme nije dostupno, funkcija kao rezultat vraa -1. Rezultat je
cjelobrojnog tipa clock_t koji je pomou typedef deklariran u zaglavlju ctime.
elimo li to vrijeme pretvoriti u sekunde, valja ga podijeliti sa CLOCKS_PER_SEC
(standardno makro ime definirano u zaglavlju ctime) ono definira broj otkucaja
sistemskog sata u sekundi za raunalo na kojem radimo. Na primjer:
clock_t zacetak, konac;
zacetak = clock();
for (int i = 0; i < 10; i++) cout << i * 10 << endl;
konac = clock();
cout << "Svaki prolaz petlje trajao je u prosjeku "
<< (konac - zacetak) / CLK_TCK / 10. << " s" << endl;

#include <ctime>
// stari naziv: <time.h>
double difftime(time_t trenutak2, time_t trenutak1);

Funkcija vraa duljinu vremenskog intervala trenutak2 - trenutak1, izraenu u


sekundama. time_t je sinonim za cjelobrojni tip podatka kojeg kao rezultat vraa
funkcija time().
Argument je kalendarsko vrijeme (dobiveno na primjer pozivom funkcije time()), a
povratna vrijednost je struktura tm (iji je sadraj opisan kod funkcija gmtime() i
localtime()). Ta struktura je statiki alocirana, te se prepisuje pri ponovnom pozivu
funkcije.
#include <ctime>
// stari naziv: <time.h>
tm *gmtime(const time_t *pokVrijeme);
tm *localtime(const time_t *pokVrijeme);

Funkcija gmtime() pretvara kalendarsko vrijeme u GMT (Greenwich Mean Time), dok
funkcija localtime()pretvara kalendarsko vrijeme u lokalno vrijeme. Argument obje

590

Principi objektno orijentiranog dizajna

funkcije je kalendarsko vrijeme (dobiveno najee pozivom funkcije time()), a


rezultat funkcija je struktura tm koja sadri podatke o vremenu:
struct tm {
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
};

//
//
//
//
//

// sekundi nakon pune minute (0...59)


// minuta nakon punog sata (0...59)
sati nakon ponoi (0...23)
dan u mjesecu (1...31)
mjesec (0...11)
// godina poevi od 1900.
dana od nedjelje (0...6)
dana od poetka godine (0...365)
// 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;

#include <ctime>
time_t mktime(tm *pokVrijeme);

// stari naziv: <time.h>

Pretvara vrijeme pohranjeno u strukturi tm u kalendarsko vrijeme (tj. vrijeme u formatu


u kojem ga vraa funkcija time() u neku ruku je ta funkcija inverzna funkciji
localtime()). Ako je pretvorba vremena uspjeno provedena, funkcija vraa
strukturu time_t s upisanim podacima, a ako nije uspjela, rezultat funkcije je -1.

591

Korak 6: Implementacija

#include <ctime>
time_t time(time_t *vrijeme);

// stari naziv: <time.h>

Funkcija time() vraa kalendarsko vrijeme, a ako vrijeme nije dostupno, funkcija
vraa -1. Kao argument funkciji moe se proslijediti nul-pokaziva; ako se proslijedi
pokaziva na neki objekt tipa time_t, tada funkcija pridruuje rezultat tom objektu.
Valja spomenuti da se kalendarsko vrijeme moe razlikovati od lokalnog vremena, na
primjer zbog razlike u vremenskim zonama da bi se dobilo lokalno vrijeme treba
pozvati funkciju localtime() koja e kalendarsko vrijeme pretvoriti u lokalno.

B.4. Matematike funkcije


Veina matematikih funkcija naslijeena je iz biblioteka jezika C, s time da su dodatno
preoptereene za ostale tipove podataka. Deklaracije funkcija naslijeenih iz jezika C
su oznaene tamno, dok su deklaracije novododanih preoptereenih varijanti pisane
svjetlijim slovima. Takoer, matematike funkcije za koje su mogue operacije s
kompleksnim brojevima, definirane su pomou predloaka za klasu complex.
#include <cstdlib>
int abs(int x);
long abs(long x);
long labs(long x);

// stari naziv: <stdlib.h>

#include <cmath>
float abs(float x);
double abs(double x);
long double abs(long double x);
float fabs(float x);

// stari naziv: <math.h>

Funkcije kao rezultat vraaju apsolutnu vrijednost broja x. U zaglavlju cstdlib


deklarirane su cjelobrojne verzije funkcije, a u cmath su definirane realne varijante
(ovakva razdvojenost naslijeena je iz standardnih C biblioteka). Budui da u jeziku C
nema mogunosti preoptereivanja imena funkcija, u standardnim C bibliotekama
verzija za realne brojeve se zove fabs(), a verzija za long se zove labs().
#include <cmath>
float acos(float x);
double acos(double x);
long double acos(long double x);

// stari naziv: <math.h>

Funkcija rauna arkus kosinus argumenta x. Argument mora biti unutar intervala od 1
do +1, a funkcija vraa odgovarajuu vrijednost kuta u radijanima, unutar intervala od
0 do (3,1415926535...). Ako je argument izvan dozvoljenog intervala, funkcija
postavlja globalnu varijablu errno na EDOM (Domain Error).

592

Principi objektno orijentiranog dizajna

#include <cmath>
float asin(float x);
double asin(double x);
long double asin(long double x);

// stari naziv: <math.h>

Funkcija rauna arkus sinus argumenta x. Argument mora biti unutar intervala od 1 do
+1, a funkcija vraa odgovarajuu vrijednost kuta u radijanima, unutar intervala od
/2 do +/2. Ako je argument izvan dozvoljenog intervala, funkcija postavlja globalnu
varijablu errno na EDOM (Domain Error).
#include <cmath>
float atan(float x);
double atan(double x);
long double atan(long double x);

// stari naziv: <math.h>

Funkcija rauna arkus tangens argumenta x. Povratna vrijednost je kut izraen u


radijanima, unutar intervala od /2 do +/2.
#include <cmath>
// stari naziv: <math.h>
float atan2(float y, float x);
double atan2(double y, double x);
long double atan2(long double y, long double x);

Funkcija rauna arkus tangens omjera argumenata y/x. Za razliku od funkcije atan(),
ova funkcija vraa vrijednosti kuta u radijanima, za sva etiri kvadranta, unutar
intervala od do +. Ako su oba argumenta jednaka nuli, tada funkcija postavlja
globalnu varijablu errno na EDOM (Domain Error).
#include <cmath>
float ceil(float x);
double ceil(double x);
long double ceil(long double x);

// stari naziv: <math.h>

float floor(float x);


double floor(double x);
long double floor(long double x);

Funkcija ceil() zaokruuje argument x na najblii vei cijeli broj, a floor()


zaokruuje argument na najblii manji cijeli broj. Funkcije vraaju cjelobrojni dio
zaokruenih brojeva. Na primjer, izvoenje sljedeih naredbi:
double broj = 123.54;
cout << ceil(broj) << endl;
cout << floor(broj) << endl;

593

Korak 6: Implementacija

ispisat e brojeve 124 i 123.


#include <cmath>
float cos(float x);
double cos(double x);
long double cos(long double x);

// stari naziv: <math.h>

Funkcija rauna kosinus argumenta x (zadanog u radijanima). Funkcija vraa


odgovarajuu vrijednost unutar intervala od 1 do +1.
#include <cmath>
float cosh(float x);
double cosh(double x);
long double cosh(long double x);

// stari naziv: <math.h>

Funkcija rauna kosinus hiperbolni argumenta x. Funkcija vraa izraunatu vrijednost.


Ako bi tona vrijednost prouzroila brojani preljev, funkcija umjesto vrijednosti vraa
HUGE_VAL, a globalna varijabla errno se postavlja u ERANGE (Range Error).
#include <cstdlib>
// stari naziv: <stdlib.h>
div_t div(int djeljenik, int djelitelj);
ldiv_t div(long djeljenik, long djelitelj);
ldiv_t ldiv(long djeljenik, long djelitelj);

Funkcije izraunavaju kvocijent i ostatak dijeljenja dva cijela broja. Kao rezultat
vraaju standardno definirane tipove div_t, odnosno ldiv_t (definirane pomou
typedef u cstdlib). U sutini su to strukture koje se sastoje od dva cijela broja tipa
int:
typedef struct {
int quot;
// kvocijent
int rem;
// ostatak
} div_t;

odnosno long:
typedef struct {
long quot;
long rem;
} ldiv_t;

// kvocijent
// ostatak

Funkcija div(long, long) je preoptereena varijanta funkcije div(int, int). U


jeziku C umjesto nje se koristi funkcija ldiv(). Ilustrirajmo primjenu funkcije div()
sljedeim primjerom:

594

Principi objektno orijentiranog dizajna

div_t r = div(10, 3);


cout << "kvocijent: " << r.quot << endl
<< "ostatak:
" << r.rem << endl;

#include <cmath>
float exp(float x);
double exp(double x);
long double exp(long double x);

// stari naziv: <math.h>

Rauna potenciju ex za zadani argument x, gdje je e = 2,718218... baza prirodnog


logaritma. Funkcija vraa izraunatu vrijednost. Ako je vrijednost izvan opsega
vrijednosti koji se zadanim tipom moe prikazati, tada funkcija vraa vrijednost
HUGE_VAL, te postavlja globalnu varijablu errno na vrijednost ERANGE (Range Error).
#include <cmath>
// stari naziv: <math.h>
float fmod(float djeljenik, float djelitelj);
double fmod(double djeljenik, double djelitelj);

Rauna ostatak dijeljenja dva realna broja. Tako e izvoenje sljedeih naredbi:
double x = 5.0;
double y = 2.2;
cout << fmod(x, y) << endl;

na zaslonu ispisati broj 0.6.


#include <cmath>
// stari naziv: <math.h>
float frexp(float x, int *eksponent);
double frexp(double x, int *eksponent);
long double frexp(long double x, int *eksponent);

Rastavlja broj x u mantisu i eksponent na bazu 2, tako da je x = mantisa 2eksponent.


Funkcija kao rezultat vraa mantisu veu ili jednaku 0.5, a manju od 1. Eksponent se
pohranjuje na mjesto na koje pokazuje drugi argument funkcije. Primjerice, izvoenje
sljedeih naredbi:
double broj = 4.;
int eksponent;
double mantisa = frexp(broj, &eksponent);
cout << broj << " = " << mantisa << " * 2 na " << eksponent
<< endl;

ispisat e:
4 = 0.5 * 2 na 3

595

Korak 6: Implementacija

#include <cmath>
// stari naziv: <math.h>
float ldexp(float x, int eksponent);
double ldexp(double x, int eksponent);
long double ldexp(long double x, int eksponent);

Funkcija rauna vrijednost x 2eksponent, pri emu su x i eksponent argumenti koji se


prenose funkciji. Inverzna ovoj funkciji je funkcija frexp().
#include <cmath>
float log(float x);
double log(double x);
long double log(long double x);

// stari naziv: <math.h>

float log10(float x);


double log10(double x);
long double log10(long double x);

Funkcija log() vraa prirodni logaritam, dok funkcija log10() vraa dekadski
logaritam argumenta x. Ako je argument realan i manji od 0, tada funkcije postavljaju
globalnu varijablu errno na vrijednost EDOM (Domain Error). Ako je argument jednak
0, funkcija vraa vrijednost minus HUGE_VAL te globalnu varijablu errno postavlja na
vrijednost ERANGE (Range Error).
#include <cmath>
// stari naziv: <math.h>
float modf(float x, float *cijeliDio);
double modf(double x, double *cijeliDio);
long double modf(long double x, long double *cijeliDio);

Funkcija rastavlja broj x na njegov cijeli i decimalni dio. Povratna vrijednost funkcije je
decimalni dio, dok se cijeli dio prenosi preko pokazivaa koji se prosljeuje funkciji kao
drugi argument. Tako e izvoenje naredbi:
double broj = 94.3;
double cijeli;
double decimalni = modf(broj, &cijeli);
cout << broj << " = " << decimalni << " + " << cijeli << endl;

ispisati na zaslonu:
94.3 = 0.3 + 94

596

Principi objektno orijentiranog dizajna

#include <cmath>
// stari naziv: <math.h>
float pow(float baza, float eksponent);
float pow(float baza, int eksponent);
double pow(double baza, double eksponent);
double pow(double baza, int eksponent);
long double pow(long double baza, long double eksponent);
long double pow(long double baza, int eksponent);

Rauna potenciju bazaeksponent. Ako je raun uspjeno proveden, funkcija kao rezultat
vraa izraunatu potenciju. Ako je rezultat izvan opsega moguih vrijednosti za
rezultirajui tip, funkcija vraa kao rezultat HUGE_VAL te postavlja globalnu varijablu
errno na vrijednost ERANGE (Range Error). Ako se funkciji prenese realna baza manja
od 0 ili ako su oba argumenta funkciji jednaki 0, funkcija postavlja vrijednost errno na
x
EDOM (Domain Error). Za raunanje potencija e praktinije je koristiti funkciju exp(),
a za raunanje kvadratnog korijena na raspolaganju je funkcija sqrt().
#include <cstdlib>
int rand();
void srand(unsigned int klica);

// stari naziv: <stdlib.h>

Funkcija rand() kao rezultat vraa pseudosluajni broj u intervalu od 0 do RAND_MAX.


Simbolika konstanta RAND_MAX definirana je u zaglavlju cstdlib.
Ako se generator sluajnih brojeva ne inicijalizira nekim sluajnim brojem, svako
izvoenje programa u kojem se poziva funkcija rand() rezultirat e uvijek istim
slijedom sluajnih brojeva, koji su jednoliko rasporeeni u intervalu od 0 do RAND_MAX.
Tako e program:
#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:

597

Korak 6: Implementacija

#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;
}

svako pokretanje programa rezultirat e razliitim nizom brojeva od 1 do 6.


#include <cmath>
float sin(float x);
double sin(double x);
long double sin(long double x);

// stari naziv: <math.h>

Funkcija rauna sinus argumenta x (zadanog u radijanima). Funkcija vraa


odgovarajuu vrijednost unutar intervala od 1 do +1.
#include <cmath>
float sinh(float x);
double sinh(double x);
long double sinh(long double x);

// stari naziv: <math.h>

Funkcija rauna sinus hiperbolni argumenta x. Funkcija vraa izraunatu vrijednost.


Ako bi tona vrijednost prouzroila brojani preljev, funkcija vraa kao rezultat
HUGE_VAL, a globalna varijabla errno se postavlja u ERANGE (Range Error).
#include <cmath>
float sqrt(float x);
double sqrt(double x);

// stari naziv: <math.h>

Rauna kvadratni korijen argumenta x. Ako je argument pozitivan realni broj, funkcija
vraa pozitivni korijen. Ako je x realan i manji od nule, funkcija postavlja vrijednost
globalne varijable errno na EDOM (Domain Error).
#include <cmath>
float tan(float x);
double tan(double x);
long double tan(long double x);

// stari naziv: <math.h>

Funkcija rauna i kao rezultat vraa tangens kuta x (zadanog u radijanima).

598

Principi objektno orijentiranog dizajna

#include <cmath>
float tanh(float x);
double tanh(double x);
long double tanh(long double x);

// stari naziv: <math.h>

Funkcija rauna i kao rezultat vraa tangens hiperbolni argumenta x.

B.5. Ostale funkcije


#include <cstdlib>
void abort();

// stari naziv: <stdlib.h>

Trenutano prekida izvoenje programa, bez zatvaranja datoteka otvorenih tijekom


izvoenja programa. Procesu koji je pokrenuo program (najee je to operacijski
sustav) vraa vrijednost 3. Ova funkcija poziva se prvenstveno u sluajevima kada se
eli sprijeiti da program zatvori aktivne datoteke. Za uredan prekid programa se
umjesto abort() koristi funkcija exit().
#include <cstdlib>
int atexit(void (*funkcija)(void));

// stari naziv: <stdlib.h>

Funkcija atexit() registrira funkciju na koju se pokaziva prenosi kao argument. To


znai da e se ta funkcija pozvati prilikom urednog zavretka programa, naredbom
return iz main() ili pozivom funkcije exit(). Ako je vie funkcija registrirano, one
se pozivaju redoslijedom obrnutim od redoslijeda kojim su registrirane. Zbog toga e
izvoenje sljedeeg programa:
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

#include <cstdlib>
// stari naziv: <stdlib.h>
void *bsearch(const void *kljuc, const void *baza, size_t brElem,
size_t sirina, int (*usporedba)(const void *, const void *));

Funkcija obavlja binarno pretraivanje sortiranog polja na koje pokazuje baza. Kao
rezultat vraa pokaziva na prvi lan polja koji zadovoljava klju na koji pokazuje
kljuc; ako nije uspjela pronai lan koji zadovoljava klju, kao rezultat funkcija vraa
nul-pokaziva. Broj elemenata u polju definiran je argumentom brElem, a veliina
pojedinog lana u polju argumentom sirina.
Funkcija na koju pokazuje usporedba koristi se za usporedbu lanova polja sa
zadanim kljuem. Poredbena funkcija mora biti oblika:
int ime_funkcije(const void *arg1, const void *arg2);

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>
// dodano da bi se prevario prevoditelj, jer funkcija
// bsearch() oekuje pokaziva na funkciju s argumentima
// tipa void *
typedef int (*fpok)(const void *, const void *);
int usporedba(const int *p1, const int *p2) {
return(*p1 - *p2);
}
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

Uoimo u gornjem primjeru definiciju typedef kojom je uveden sinonim za pokaziva


na funkciju usporedbe. Ovo je neophodno, jer funkcija bsearch() oekuje pokaziva
na funkciju s oba argumenta tipa void *, a mi joj prosljeujemo pokaziva na funkciju
koja ima kao argumente int *.
#include <cstdlib>
void exit(int status);

// stari naziv: <stdlib.h>

Funkcija exit() uredno prekida izvoenje programa. Prije prekida, svi se


meuspremnici prazne, datoteke se zatvaraju, te se pozivaju sve izlazne funkcije, koje
su bile proslijeene funkcijom atexit().
Varijabla status koja se prosljeuje funkciji slui da bi se pozivajui program
(najee je to sam operacijski sustav), izvijestio o eventualnoj pogreki. Argument
moe biti EXIT_FAILURE ili EXIT_SUCCESS makro imena definirana u zaglavlju
cstdlib.
Funkcija exit() uglavnom se koristi kao izlaz u sluaju nude ako je nastupila
neka fatalna pogreka u programu koja onemoguava daljnje izvoenje programa. U
jeziku C++ njena upotreba je minimalizirana zahvaljujui iznimkama i njihovom
hvatanju.
#include <clocale>
// stari naziv: <locale.h>
lconv *localeconv();
char *setlocale(int kategorija, const char *mjesto);

Funkcija localeconv() vraa trenutanu postavu lokalnih zemljopisnih kategorija


(formata datuma, vremena, realnih brojeva). Podaci su pohranjeni u strukturi tipa
lconv, koja je deklarirana u zaglavlju clocale:
struct lconv {
char *decimal_point;
char *thousands_sep;
char
char
char
char

*grouping;
*int_curr_symbol;
*currency_symbol;
*mon_decimal_point;

char *mon_thousands_sep;
char
char
char
char

*mon_grouping;
*positive_sign;
*negative_sign;
int_frac_digits;

char frac_digits;

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

znak za razdvajanje cijelih i


decimalnih mjesta u brojevima
znak za razdvajanje grupa
znamenki po tisuu
veliina svake grupe znamenki
meunarodna oznaka valute
lokalna oznaka valute
znak za razdvajanje cijelih i
dec. mjesta u novanim iznosima
znak za razdvajanje grupa
znamenki u novanim iznosima
veliina svake grupe
oznaka za pozitivne nov.iznose
oznaka za negativne nov.iznose
broj decimalnih znamenki u meunarodnom prikazu nov.iznosa
broj decimalnih znamenki u

601

Korak 6: Implementacija

char p_cs_precedes;
char p_sep_by_space;
char n_cs_precedes;
char n_sep_by_space;
char p_sign_posn;
char n_sign_posn;

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

lokalnom prikazu nov.iznosa


=1 ako lokalna oznaka valute
prethodi pozitivnom nov.iznosu
=1 ako bjelina odvaja pozitivni
nov.iznos od oznake valute
=1 ako lokalna oznaka valute
prethodi negativnom nov.iznosu
=1 ako bjelina odvaja negativni
nov.iznos od oznake valute
gdje smjestiti pozitivni
predznak u novanim iznosima
gdje smjestiti negativni
predznak u novanim iznosima

};

Sadraj te strukture moe se promijeniti samo pomou funkcije setlocale(). Prvi


argument pri pozivu funkcije jest kategorija na koju se promjena odnosi:
LC_ALL
obuhvaa sve kategorije.
LC_COLLATE
utjee na funkcije strcoll() i strxfrm().
LC_CTYPE
utjee na funkcije za rukovanje jednim znakom.
LC_MONETARY format ispisa novanih iznosa; vraa ga funkcija setlocaleconv().
LC_NUMERIC
znak za razdvajanje cijelih i decimalnih mjesta (npr. decimalna toka
ili zarez).
LC_TIME
utjee na strftime() funkciju za ispis tekueg datuma u obliku
znakovnog niza.
Drugi argument funkciji setlocale() jest pokaziva na znakovni niz koji specificira
lokalnu postavu. Koji su nizovi podrani, ovisi o implementaciji biblioteke.
#include <cstring>
// stari naziv: <string.h>
void *memchr(void *niz, int znak, size_t duljina);
const void *memchr(const void *niz, int znak, size_t duljina);

Pretrauje niz bajtova duljine duljina, poevi od lokacije na koju pokazuje niz i
trai znak znak. Kao rezultat vraa pokaziva na prvu pojavu znaka, a ako ga ne
pronae, povratna vrijednost je nul-pokaziva.
#include <cstring>
// stari naziv: <string.h>
void *memcpy(void *odrediste, const void *izvornik, size_t duljina);
void *memmove(void *odrediste, const void *izvornik, size_t duljina);

Obje funkcije preslikavaju blok duljine duljina bajtova s mjesta u memoriji na koje
pokazuje pokaziva izvornik, na mjesto na koje pokazuje odrediste. Meutim, ako
se izvorni i odredini blokovi preklapaju, ponaanje funkcije memcpy()je nedefinirano,
dok e funkcija memmove() preslikati blok korektno. Primjerice, provjerite to ete
dobiti ispisom polja abcd[] nakon sljedeih naredbi:

602

Principi objektno orijentiranog dizajna

char abcd[] = "abcdefghijlkmnopqrstuvwxyz";


memcpy(abcd + 5, abcd, 12);

#include <cstring>
// stari naziv: <string.h>
int memcmp(const void *niz1, const void *niz2, size_t duljina);

Usporeuje prvih duljina bajtova oba niza, a kao rezultat usporedbe vraa:
< 0 ako je niz1 manji od niz2 (svrstano po abecedi niz1 dolazi prije niz2),
0 ako su niz1 i niz2 meusobno jednaki,
> 0 ako je niz1 vei od niz2 (svrstano po abecedi niz1 dolazi iza niz2).
Usporedba se radi po slijedu znakova koji se koristi na dotinom raunalu.
#include <cstring>
// stari naziv: <string.h>
void *memset(void *niz, int znak, size_t duljina);

Blok duljine duljina bajtova, poevi od lokacije na koju pokazuje niz, funkcija
popunjava znakom znak. Kao rezultat, funkcija vraa pokaziva na poetak niza.
#include <cstdlib>
// stari naziv: <stdlib.h>
void *qsort(void *baza, size_t brElem, size_t sirina,
int (*usporedba)(const void *, const void *));

Sortira polje baza[0]...baza[brElem - 1] koje ine podaci irine sirina. Za


usporedbu meu lanovima polja koristi se funkcija na koju pokazuje usporedba;
funkcija mora imati identina svojstva kao i funkcija koja se poziva u funkciji
bsearch():
int ime_funkcije(const void *arg1, const void *arg2);

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 usporedba(const void *p1, const void *p2) {
return(*(char*)p2 - *(char*)p1);
}
int main() {
char podaci[] = "adiorwgoerg";

Korak 6: Implementacija

603

int brelem = (sizeof(podaci) - 1) / sizeof(podaci[0]);


qsort(podaci, brelem, sizeof(podaci[0]), usporedba);
cout << podaci << endl;
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.
#include <csignal>
// stari naziv: <signal.h>
int raise(int dojava);
void (*signal(int dojava, void (*rukovatelj)(int)))(int);

Funkcijom signal() definira se koja e funkcija biti pozvana ako nastupi neka
izvanredna situacija, na primjer ako se primi signal prekida (interrupt) sa neke vanjske
jedinice ili pogreka tijekom izvoenja programa. Prvi argument je tip dojave na koju se
definicija nove funkcije rukovatelj() odnosi. Standardno su definirani (u zaglavlju
csignal) sljedei tipovi:
SIGABRT nepravilan prekid, primjerice uslijed abort();
SIGFPE
aritmetika pogreka, primjerice zbog dijeljenja s nulom ili preljev;
SIGILL
neispravna instrukcija;
SIGINT
interaktivno upozorenje, primjerice prekid;
SIGSEGV nepravilni pristup memoriji, na primjer pristup nedozvoljenom podruju
memorije;
SIGTERM program je zaprimio zahtjev za zavretak.
Funkcija rukovatelj() je zaduena za obradu dojave. To moe biti funkcija koju
definira korisnik, ili dva predefinirana rukovatelja:
SIG_DFL zakljui izvoenje programa;
SIG_IGN ignorira ovaj tip dojave.
Rukovateljska funkcija prihvaa jedan cjelobrojni argument tip dojave. Ako je poziv
funkcije signal() bio uspjean, funkcija kao rezultat vraa pokaziva na prethodni
rukovatelj navedenog tipa dojave; ako poziv nije bio uspjean, funkcija vraa SIG_ERR.
Valja voditi rauna da se pozivom rukovatelja, brie dojava te ponovno treba instalirati
funkciju za rukovanje.
Funkcija raise() alje dojavu signala dojava programu. Ako je odailjanje bilo
uspjeno, funkcija vraa vrijednost razliitu od nule. U sljedeem primjeru ilustrirana je
primjena obje funkcije:
void hvataljka(int dojava) {
signal(SIGFPE, hvataljka); // ponovno instalira hvataljku
cout << "No, no: zna da nije dozvoljeno dijeljenje s "
"nulom" << endl;

604

Principi objektno orijentiranog dizajna

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 <new>
// stari naziv: <new.h>
new_handler set_new_handler(new_handler funkcija);

Funkcija set_new_handler() definira koja e funkcija biti pozvana ako globalni


operatori new() ili new[]() ne uspiju alocirati traeni memorijski prostor. Ako nije
posebno zadano, operatori e baciti iznimku tipa bad_alloc pozivom funkcije
set_new_handler() ovakvo ponaanje se moe promijeniti. Funkcija koja obrauje
nedostatak memorijskog prostora kao parametar prima podatak tipa size_t koji
pokazuje koliko se memorije trailo, a kao rezultat vraa int. Ako funkcija vrati nulu,
time se signalizira operatoru new da dodatne memorije nema barem u zahtjevanoj
koliini. Vrijednost razliita od nule signalizira da se uspjelo pronai dodatne memorije
te da operator new moe ponoviti alokaciju.
#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.

605

Korak 6: Implementacija

Funkcija terminate() e prekinuti izvoenje programa pozivom funkcije abort().


Ako ne elimo prekid programa na ovaj nain, moemo sami zadati funkciji pomou
funkcije set_terminate(). Prilikom poziva funkcije set_terminate(), ona kao
rezultat vraa pokaziva na prethodno instaliranu funkciju.
Pokuaj da se funkcijom set_terminate() instalira funkcija koja nee prekinuti
izvoenje programa, nego e se pokuati vratiti se pozivajuem kdu, rezultirat e
pogrekom prilikom izvoenja.
#include <cstdlib>
int system(const char *naredba);

// stari naziv: <stdlib.h>

Funkcija alje naredbu okruju iz kojeg je program pokrenut (najee je to operacijski


sustav). Primjerice, naredba:
system("blabla");

e pokrenuti program blabla; nakon okonanja tog programa, izvoenje e se


nastaviti naredbom koja slijedi iza poziva funkcije system().
#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

C. Rjenik ee koritenih pojmova


Hrvatsko-engleski rjenik
anonimna unija
apstrakcija podataka
apstraktna klasa
argument
asembler
automatska smjetajna klasa
automatski objekt
bacanje iznimke
bajt
bezimena unija
bezimeni imenik
biblioteka
blok pokuaja
cijeli broj
cjelobrojna promocija
isti virtualni funkcijski lan
datoteni imenik
datoteno podruje
datoteka
datoteka zaglavlja
definicija funkcije
deklaracija unaprijed
destruktor
dinamiki objekt
dinamiki poziv
dinamiko povezivanje
diskriminanta unije
djelomina specijalizacija
dodjela tipa
dominacija
duboka kopija
enkapsulacija
formalni argument
funkcijski lan
globalno podruje
hrpa
hvatati
identifikacija tipova tijekom izvoenja

anonymous union
data abstraction
abstract class
argument
assembler
automatic storage class
automatic object
throwing an exception
byte
nameless union
nameless namespace
library
try block
integer
integral promotion
pure virtual function member
directory
file scope
file
header file
function definition
forward declaration
destructor
dynamic object
dynamic call
dynamic binding
union discriminant
partial specialization
type cast
dominance
deep copy
encapsulation
formal argument
member functions
global scope
heap
catch
run-time type identification

607

Korak 6: Implementacija

imenik
implementacija objekta
instanciranje predloka
integrirana razvojne okoline
iskljuivi ili
isprazniti
izbornik
izlazni tok
iznimka
izvedbeni program
izvedena klasa
izvorni kd
javni
javna osnovna klasa
javno suelje
kasno povezivanje
klasa
konstantnost
konstruktor
konstruktor kopije
kontejnerska klasa
kurzor
lvrijednost
makro funkcija
makro ime
manipulator
meupohranjivanje
meuspremnik
memorijska napuklina
metoda
mjesto instantacije
najdalje izvedena klasa
nasljeivanje
neimenovani privremeni objekt
nevezano prijateljstvo
nul-pokaziva
nul-znak
objektni kd
objektno orijentirano programiranje
obnavljajue pridruivanje
odbaciti konstantnost
odmatanje stoga
omota
operator izluivanja

namespace
implementation
template instantiation
integrated development environment,
IDE
exclusive or
flush
menu
output stream
exception
executable
derived class
source code
public
public base class
public interface
late binding
class
constness
constructor
copy constructor
container class
cursor
lvalue
macro function
macro name
manipulator
buffering
buffer
memory leak
methods
point of instantiation
most derived class
inheritance
unnamed temporary
unbound template friendship
null-pointer
null-character
object code
object oriented programming
update assignment
cast away constness
stack unwinding
wrapper
extraction operator

608

Principi objektno orijentiranog dizajna

operator umetanja
operator za indeksiranje
operator za odreivanje podruja
operator za pristup lanu
oporavak od iznimke
osnovna klasa
pametni pokaziva
parametar
plitka kopija
pobrojenje
pobrojani tip
podatkovni segment
podizanje iznimke
podrazumijevana
vrijednost
argumenta
podrazumijevani konstruktor
podruje
pogonitelj
pogonjeno dogaajima
pogreka
pogreka pri izvoenju
pogreka pri povezivanju
pogreka pri prevoenju
pogreno
pokaziva
pokaziva datoteke
pokaziva instrukcija
pokaziva na lan klase
pokaziva stoga
polimorfizam
polje bitova
polje podataka
pomak udesno
pomak ulijevo
ponovnu iskoristivost kda
popratna pojava
posebna sekvenca
potpis funkcije
poveziva
povratna vrijednost
pravila provjere tipa
pravilo od palca
prazan
predloak
predloak funkcije

insertion operator
indexing operator
scope resolution operator
member selection operator
exception recovery
base class
smart pointer
parameter
shallow copy
enumeration
enumerated type
data segment
raising an exception
default argument value
default constructor
scope
driver
event-driven
bug
run-time error
link-time error
compile-time error
false
pointer
file pointer
instruction pointer
class member pointer
stack pointer
polimorphysm
bit-fields
array
shift right
shift left
code reusability
side-effect
escape sequence
function signature
linker
return value
type-checking rules
rule of the thumb
void
template
function template

609

Korak 6: Implementacija

predloak klase
prekid
prekomjerno bujanje kda
preoptereenje funkcije
preoptereenje operatora
pretvorba nanie
pretvorba navie
prevoditelj
prijatelj klase
prijenos po referenci
prijenos po vrijednosti
privatna osnovna klasa
privatni
program za lociranje pogreaka
program za praenje efikasnosti kda
program za ureivanje teksta
programersko suelje
promjenjiv
proirenje imenika
prototip funkcije
razluivanje podruja
realni broj
registar
registarska smjetajna klasa
rekurzija
relacijski operator
rukovanje iznimkama
skrivanje podataka
skupljanje smea
smjetajna klasa
specijalizacija predloka
sredinja procesorska jedinica
standardna biblioteka predloaka
stanje
statiki objekt
statiki poziv
statiko povezivanje
stog
struktura
stvarni argument
tijelo funkcije
tip
tip povezivanja
tono
tok

class template
interrupt
code bloat
function overloading
operator overloading
downcast
upcast
compiler
friend of a class
pass by reference
pass by value
private base class
private
debugger
profiler
text editor
application programming interface
volatile
namespace extension
function prototype
scope resolution
floating-point number
register
register storage class
recursion
relational operator
exception handling
data hiding
garbage collection
storage class
template specialization
central processing unit, CPU
standard template library
state
static object
static call
static binding
stack
structure
actual argument
function body
type
linkage
true
stream

610

Principi objektno orijentiranog dizajna

ugnijeena klasa
ulazni tok
umetnuta definicija
umetnuta funkcija
unija
unutarnje povezivanje
vanjsko povezivanje
vezana lista
vezano prijateljstvo
virtualna osnovna klasa
virtualni funkcijski lan
virtualni poziv
visea referenca
visei pokaziva
viestruko nasljeivanje
zagaenje globalnog podruja
zaobilaenje prilikom nasljeivanja
zastavica
zatien
zatiena osnovna klasa
znakovni niz

nested class
input stream
inline definition
inline function
union
internal linkage
external linkage
linked list
bound template friendship
virtual base class
virtual function member
virtual call
dangling reference
dangling pointer
multiple inheritance
global scope pollution
overriding
flag
protected
protected base class
character string

611

Korak 6: Implementacija

Englesko-hrvatski rjenik
abstract class
actual argument
anonymous union
application programming interface
argument
array
assembler
automatic object
automatic storage class
base class
bit-fields
bound template friendship
buffer
buffering
bug
byte
cast away constness
catch
central processing unit, CPU
character string
class
class member pointer
class template
code bloat
code reusability
compile-time error
compiler
constness
constructor
container class
copy constructor
cursor
dangling pointer
dangling reference
data abstraction
data hiding
data segment
debugger
deep copy
default argument value
default constructor

apstraktna klasa
stvarni argument
anonimna unija
programersko suelje
argument
polje podataka
asembler
automatski objekt
automatska smjetajna klasa
osnovna klasa
polje bitova
vezano prijateljstvo
meuspremnik
meupohranjivanje
pogreka
bajt
odbaciti konstantnost
hvatati
sredinja procesorska jedinica
znakovni niz
klasa
pokaziva na lan klase
predloak klase
prekomjerno bujanje kda
ponovnu iskoristivost kda
pogreka pri prevoenju
prevoditelj
konstantnost
konstruktor
kontejnerska klasa
konstruktor kopije
kurzor
visei pokaziva
visea referenca
apstrakcija podataka
skrivanje podataka
podatkovni segment
program za lociranje pogreaka
duboka kopija
podrazumijevana
vrijednost
argumenta
podrazumijevani konstruktor

612

Principi objektno orijentiranog dizajna

derived class
destructor
directory
dominance
downcast
driver
dynamic binding
dynamic call
dynamic object
encapsulation
enumerated type
enumeration
escape sequence
event-driven
exception
exception handling
exception recovery
exclusive or
executable
external linkage
extraction operator
false
file
file pointer
file scope
flag
floating-point number
flush
formal argument
forward declaration
friend of a class
function body
function definition
function overloading
function prototype
function signature
function template
garbage collection
global scope
global scope pollution
header file
heap
implementation
indexing operator
inheritance

izvedena klasa
destruktor
datoteni imenik
dominacija
pretvorba nanie
pogonitelj
dinamiko povezivanje
dinamiki poziv
dinamiki objekt
enkapsulacija
pobrojani tip
pobrojenje
posebna sekvenca
pogonjeno dogaajima
iznimka
rukovanje iznimkama
oporavak od iznimke
iskljuivi ili
izvedbeni program
vanjsko povezivanje
operator izluivanja
pogreno
datoteka
pokaziva datoteke
datoteno podruje
zastavica
realni broj
isprazniti
formalni argument
deklaracija unaprijed
prijatelj klase
tijelo funkcije
definicija funkcije
preoptereenje funkcije
prototip funkcije
potpis funkcije
predloak funkcije
skupljanje smea
globalno podruje
zagaenje globalnog podruja
datoteka zaglavlja
hrpa
implementacija objekta
operator za indeksiranje
nasljeivanje

613

Korak 6: Implementacija

inline definition
inline function
input stream
insertion operator
instruction pointer
integer
integral promotion
integrated development environment,
IDE
internal linkage
interrupt
late binding
library
linkage
linked list
linker
link-time error
lvalue
macro function
macro name
manipulator
member functions
member selection operator
memory leak
menu
methods
most derived class
multiple inheritance
nameless namespace
nameless union
namespace
namespace extension
nested class
null-character
null-pointer
object code
object oriented programming
operator overloading
output stream
overriding
parameter
partial specialization
pass by reference
pass by value
point of instantiation

umetnuta definicija
umetnuta funkcija
ulazni tok
operator umetanja
pokaziva instrukcija
cijeli broj
cjelobrojna promocija
integrirana razvojne okoline
unutarnje povezivanje
prekid
kasno povezivanje
biblioteka
tip povezivanja
vezana lista
poveziva
pogreka pri povezivanju
lvrijednost
makro funkcija
makro ime
manipulator
funkcijski lan
operator za pristup lanu
memorijska napuklina
izbornik
metoda
najdalje izvedena klasa
viestruko nasljeivanje
bezimeni imenik
bezimena unija
imenik
proirenje imenika
ugnijeena klasa
nul-znak
nul-pokaziva
objektni kd
objektno orijentirano programiranje
preoptereenje operatora
izlazni tok
zaobilaenje prilikom nasljeivanja
parametar
djelomina specijalizacija
prijenos po referenci
prijenos po vrijednosti
mjesto instantacije

614

Principi objektno orijentiranog dizajna

pointer
polymorphism
private
private base class
profiler
protected
protected base class
public
public base class
public interface
pure virtual function member
raising an exception
recursion
register
register storage class
relational operator
return value
rule of the thumb
run-time error
run-time type identification
scope
scope resolution
scope resolution operator
shallow copy
shift left
shift right
side-effect
smart pointer
source code
stack
stack pointer
stack unwinding
standard template library, STL
state
static binding
static call
static object
storage class
stream
structure
template
template instantiation
template specialization
text editor
throwing an exception

pokaziva
polimorfizam
privatni
privatna osnovna klasa
program za praenje efikasnosti kda
zatien
zatiena osnovna klasa
javni
javna osnovna klasa
javno suelje
isti virtualni funkcijski lan
podizanje iznimke
rekurzija
registar
registarska smjetajna klasa
relacijski operator
povratna vrijednost
pravilo od palca
pogreka pri izvoenju
identifikacija tipova tijekom izvoenja
podruje
razluivanje podruja
operator za odreivanje podruja
plitka kopija
pomak ulijevo
pomak udesno
popratna pojava
pametni pokaziva
izvorni kd
stog
pokaziva stoga
odmatanje stoga
standardna biblioteka predloaka
stanje
statiko povezivanje
statiki poziv
statiki objekt
smjetajna klasa
tok
struktura
predloak
instanciranje predloka
specijalizacija predloka
program za ureivanje teksta
bacanje iznimke

615

Korak 6: Implementacija

true
try block
type
type cast
type-checking rules
unbound template friendship
union
union discriminant
unnamed temporary
upcast
update assignment
virtual base class
virtual call
virtual function member
void
volatile
wrapper

tono
blok pokuaja
tip
dodjela tipa
pravila provjere tipa
nevezano prijateljstvo
unija
diskriminanta unije
neimenovani privremeni objekt
pretvorba navie
obnavljajue pridruivanje
virtualna osnovna klasa
virtualni poziv
virtualni funkcijski lan
prazan
promjenjiv
omota

616

Principi objektno orijentiranog dizajna

Abecedno kazalo
#
!, 57
!=, 59
" (#include), 475
" (znakovni niz), 26, 60, 139
"C", 509
"C++", 511
#, 480
##, 480
%, 46
%:, 70
%:%:, 70
%=, 69
%>, 70
&, 63, 114
&&, 57
&=, 69
', 60
(), 76, 152, 154, 310
preoptereenje, 317
* (mnoenje), 46
* (pokaziva), 114
*=, 69
+, 46
++, 45, 125
preoptereenje, 321
+=, 69
,, 74, 88
-, 46
--, 45, 125
preoptereenje, 321
-=, 69
->, 221, 310
preoptereenje, 318
->*, 274, 278
., 221
.*, 274, 278
... (funkcije)
argumenti funkcije, 176
... (iznimke)

hvatanje iznimke, 454


/, 46
/* */ (komentar), 29
// (komentar), 29
/=, 69
:, 95, 297
::, 186, 223, 259, 262, 270, 342, 435, 436,
437
i virtualni lan, 379
:>, 70
;, 25, 219
<, 59
<%, 70
<:, 70
<< (izlazni operator), 26, 517
preoptereenje, 519
razlikovanje << i >>, 28
<< (pomak ulijevo), 65
<<=, 69
<=, 59
<> (#include), 475
<> (predloak), 211, 392, 398, 409, 412
=, 39, 69, 310
i konstruktor konverzije, 370
nasljeivanje, 369
preoptereenje, 313
=0, 380
==, 59
>, 59
>=, 59
>> (pomak udesno), 65
>> (ulazni operator), 27, 521
preoptereenje, 523
razlikovanje << i >>, 28
>>=, 69
?:, 83
??!, 71
??', 71
??(, 71
??), 71
??-, 71
??/, 71

617

Korak 6: Implementacija

??<, 71
??=, 71
??>, 71
[], 104, 310
preoptereenje, 315
\ (nastavak reda), 31, 474
\ (posebna, escape sekvenca), 60
\", 60
\', 60
\0, 60, 140
\?, 60
\\, 60
^, 64
^=, 69
{}, 77, 153
|, 64
|=, 69
||, 57
~, 62, 246

A
\a, 60
abort(), 574, 598
abs(), 591
abstract class. Vidi apstraktna klasa
acos(), 591
adjustfield, 533
algorithm, 573
and, 70
and_eq, 70
anonymous unions. Vidi unija, anonimna
ANSI C++, 18
API, 558
app, 545
application programming interface. Vidi
programersko suelje (API)
apstrakcija
definicija, 560
definicija javnog suelja, 571
implementacija, 578
implementacijski zavisna, 569
ocjenjivanje, 559
odnosi i veze, 563
pronalaenje, 559
apstrakcija podataka, 555
apstraktna klasa, 330, 380
argc, 202

argument
formalni, 154
klasa kao, 363
konstantni, 172
konstruktora, 238, 240
konverzija, 163
neodreeni, 176
podrazumijevani, 173, 192, 201
pokaziva kao, 164
polje kao, 168
predloka funkcije, 211, 392
predloka funkcije, konstantni izraz, 396
predloka klase, 409
predloka klase, konstantni izraz, 420
preoptereene funkcije, 191
prijenos po referenci, 165
prijenos po vrijednosti, 162, 181
privremeni objekt za prijenos po
vrijednosti, 284
redoslijed izraunavanja, 163
referenca kao, 165
stvarni, 154
tono podudaranje, 364
znakovni niz kao, 169
argv, 202
array. Vidi polje
ASCII, 61
asctime(), 588
asembler, 10, 511
ukljuivanje u C++ kd, 511
asin(), 592
asm, 511
assert(), 236, 248, 483, 574
assert.h, 236, 483
atan(), 205, 592
atan2(), 204, 592
ate, 545
atexit(), 598
atof(), 203, 586
atoi(), 587
atol(), 587
auto, 182
automatic storage class. Vidi smjetajna
klasa, automatska
automatski objekti, 128

B
\b, 60

618

bad(), 516
bad_cast, 469
badbit, 516
base class. Vidi osnovna klasa
basefield, 533
basic_string, 572, 578
before(), 466
beg, 549
bezimeni imenik, 437
biblioteka, 22
funkcija, 204, 580. Vidi standardna
funkcija
standardna, 571
binary, 545
bitand, 70
bit-fields. Vidi polje bitova
bitor, 70
bitovni operator
i, 62, 63
ili, 62, 64
iskljuivi ili, 62, 64
komplement, 62
pomak udesno, 65
pomak ulijevo, 65
bits, 573
blok
hvatanja, 448, 452, 453
hvatanja, odreivanje odgovarajueg
bloka, 453
pokuaja, 448, 452
blok naredbi, 38, 77
bool, 57
boolalpha, 533
bound template friendship. Vidi vezano
prijateljstvo
break, 84, 93
broj
cijeli, 40
dekadski, 41
heksadekadski, 41
oktalni, 41
preljev, 46
realni, 40
s pominom decimalnom tokom. Vidi
broj, realni
brojana konstanta, 35, 51
buffer. Vidi meuspremnik
buffering. Vidi meupohranjivanje

Principi objektno orijentiranog dizajna

C
.c, 22, 495
case, 84
cassert, 572
cast. Vidi dodjela tipa
catch, 448, 450
catching an exception. Vidi iznimka,
hvatanje
cctype, 572
ceil(), 592
cerr, 516
cerrno, 572
char, 44, 60
cijeli broj, 40
dijeljenje, 48
cin, 27, 515, 521
class, 211, 218, 392, 409. Vidi klasa
class member pointer. Vidi pokaziva, na
lan klase
class template. Vidi predloak klase
clear(), 516, 548
clocale, 573
clock(), 589
clock_t, 588, 589
CLOCKS_PER_SEC, 589
clog, 516
close(), 545
cmath, 573
code reusability. Vidi ponovna iskoristivost
compiler. Vidi prevoditelj
compl, 70
complex, 573, 578
complex.h, 205
const, 53, 136, 172, 413. Vidi simbolika
konstanta
argument funkcije, 172
const_cast, 471
funkcijski lan, 253
odbacivanje konstantnosti, 139
podatkovni lan, 244
pokaziva, 137
pokaziva na, 137
pokaziva na const, 138
razlika const i #define, 477
reference na, 145
const_cast, 471
constructor. Vidi konstruktor
container class. Vidi kontejnerska klasa

Korak 6: Implementacija

continue, 94
copy constructor. Vidi konstruktor,
konstruktor kopije
cos(), 593
cosh(), 593
cout, 26, 516, 517
.cp, 22, 495
_ _cplusplus, 478, 510
.cpp, 22, 495
CRC kartica, 563
csetjmp, 572
csignal, 572
cstdarg, 572
cstdio, 573
cstdlib, 572, 573
cstring, 572
ctime, 572, 588
ctime(), 588
ctype.h, 528
cur, 549
cwchar, 572, 573
cwctype, 572

C
lan
funkcijski. Vidi funkcijski lan
imenika, koritenje, 437
iskljuivanje iz nasljeivanja, 353
javni, 226
podatkovni. Vidi podatkovni lan
podrazumijevano pravo pristupa, 227
pokaziva na, 271
polja bitova, 297
pravo pristupa, 226
pristup, 220
privatni, 226
puno ime, 223, 261
razlika statikog povezivanja i statikog
lana, 491
virtualne osnovne klase, 385
zatieni, 226

D
dangling pointer. Vidi pokaziva, visei
dangling reference. Vidi referenca, visea
data abstraction. Vidi apstrakcija podataka
data hiding. Vidi skrivanje podataka

619

data segment. Vidi podatkovni segment


_ _DATE_ _, 478
datoteno podruje, 262
datoteka
binarna, 550
itanje iz, 541
mod otvaranja, 545
otvaranje, 544
pisanje u, 541
pokaziva, 548
zatvaranje, 545
datoteka izvornog kda, 486
datoteka zaglavlja, 155, 494. Vidi zaglavlje
i statiki objekt, 258
to u nju ne staviti, 497
to u nju staviti, 495
debugger, 23, 53
simboliki, 23
dec, 533, 538
deep copy. Vidi duboka kopija
default, 84
default argument. Vidi argument,
podrazumijevani
default constructor. Vidi konstruktor,
podrazumijevani
#define, 53, 476, 479
razlika #define i const, 477
trajanje definicije, 477
definicija
funkcije, 153
funkcijskog lana klase, 223
lokalne klase, 270
podatkovnog lana, 220
predloka funkcije, 392, 393
predloka klase, 409
preoptereenog operatora, 309
specijalizacija predloka klase, 417
specijalizacije predloka funkcije, 405
definiranje goto oznake, 95
deklaracija, 38
apstraktne klase, 380
const pokazivaa, 137
const pokazivaa na const, 138
istog virtualnog lana, 380
itanje deklaracije pokazivaa, 138
extern, 158
funkcija s podrazumijevanim
argumentom, 175
funkcije, 152, 494

620

funkcije s neodreenim argumentima,


176
globalnog objekta, 184, 248
imenika, 435
izvedene klase, 335
klase, 218, 232
klase unaprijed, 220
mjesto navoenja, 39
objekta, 232
pokazivaa na const, 137
pokazivaa na funkcijski lan, 278
pokazivaa na podatkovni lan, 272
polja, 102
polja bitova, 297
polja znakovnih nizova, 143
predloka funkcije, 393
razlika deklariranja struktura u C i C++
jezicima, 293
razlika deklariranja unija u C i C++
jezicima, 294
reference, 144
reference na const, 145
statikog funkcijskog lana, 260
statikog objekta, 248
statikog podatkovnog lana, 258
unutar bloka, 77
using, 353, 439
using, unutar klase, 441
virtual, 375
virtualne osnovne klase, 384
znakovnog niza, 139
delete, 129, 247, 381
i polje objekata, 250
nasljeivanje, 371
preoptereenje, 323
delete [], 129, 250
i polje objekata, 250
preoptereenje, 324
deque, 573, 577
derived class. Vidi izvedena klasa
destructor. Vidi destruktor
destruktor, 216, 246
apstraktne klase, 382
dealokacija memorije objekta, 246
deinicijalizacija, 356
deinicijalizacija objekta, 246
eksplicitan poziv, 252
i virtualni poziv, 379
izvedene klase, 356
poziv, 246

Principi objektno orijentiranog dizajna

poziv za globalne i statike objekte, 249


redoslijed pozivanja, 356, 389
virtualni, 380
difftime(), 589
dijeljenje cijelih brojeva, 48
dinamika alokacija, 128
i iznimke, 455
operator delete, 129
operator delete [], 129
operator new, 128
operator new [], 129
polja, 129
polja objekata, 250
dinamiki objekt, 128
alokacija, 238
inicijalizacija, 238
dinamiki poziv, 375
dinamiko povezivanje, 373
direktiva
pretprocesorska. Vidi pretprocesor,
naredba
using, 443
using, unutar imenika, 444
div(), 593
div_t, 593
dodjela tipa, 50, 400. Vidi konverzija
dominacija, 387
dominance. Vidi dominacija
double, 43, 44
do-while, 92
downcast. Vidi pretvorba nanie
driver. Vidi pogonitelj
duboka kopija, 242
dynamic binding. Vidi povezivanje,
dinamiko
dynamic call. Vidi poziv, dinamiki
dynamic_cast, 468
bad_cast, 469

E
EDOM, 574
#elif, 482
#else, 482
else, 80
encapsulation. Vidi enkapsulacija
end, 549
#endif, 248, 482
endl, 521, 538

Korak 6: Implementacija

ends, 521, 538


enkapsulacija, 12, 13, 215, 513
enum, 55
EOF, 526, 575
eof(), 516
eofbit, 516, 548
ERANGE, 574
errno, 574, 575
#error, 484
escape sequence. Vidi posebna sekvenca
exception, 572. Vidi iznimka
exception handling. Vidi rukovanje
iznimkama
exception recovery. Vidi oporavak od
iznimke
executable. Vidi izvedbeni kd
exit(), 210, 249, 544, 600
EXIT_FAILURE, 575, 600
EXIT_SUCCESS, 575, 600
exp(), 193, 594
explicit, 302
extern, 158, 185, 394, 413, 492
extern "C", 509
extern "C++", 511
external linkage. Vidi povezivanje, vanjsko

F
\f, 60
fabs(), 199, 591
fail(), 516
failbit, 516, 522
false, 57
_ _FILE_ _, 266, 326, 478, 485
FILE, 126
file pointer. Vidi pokaziva datoteke
file scope. Vidi datoteno podruje
fill(), 531
fixed, 533, 535
flag. Vidi zastavica
flags(), 532
float, 44
float.h, 205
floatfield, 533
floating-point. Vidi realni broj
floor(), 592
flush. Vidi meuspremnik, pranjenje
flush, 520, 538

621

flush(), 520
fmod(), 594
fopen(), 126
for, 86
forward declaration. Vidi klasa, deklaracija
unaprijed
free(), 325
frexp(), 594
friend, 230
razlika prijateljstva i nasljeivanja, 350
unutar deklaracije klase, 231
friend of a class. Vidi prijatelj klase
fstream, 515, 541, 546, 573
fstream.h, 91, 205, 541
function overloading. Vidi funkcija,
preoptereenje
function template. Vidi predloak funkcije
function template instantiation. Vidi
predloak funkcije, instantacija
function template specialization. Vidi
predloak funkcije, specijalizacija
funkcija, 150
argc, 202
argument, 150, 161
argv, 202
bez argumenata, 161
datoteka zaglavlja, 155
definicija, 153
deklaracija, 152
formalni argument, 154
inline, 489
konstantni argument, 172
konverzija argumenata, 163
konverzija rezultata, 158
lista moguih iznimaka, 458
main(), 24, 202, 249, 453, 530
mijenjanje pokazivakih argumenata,
166
operatorska, 309
parametar. Vidi argument. Vidi funkcija,
argument
podrazumijevani argument, 173
pokaziva kao argument, 164
pokaziva na, 196
polje kao argument, 168
potpis, 153
povratna vrijednost, 150, 159, 179
poziv, 154
poziv operatorske funkcije, 310
pozivanje C funkcije, 509

622

predloak. Vidi predloak funkcije


preoptereenje, 189
preoptereenje predloka, 403
prijenos argumenta po referenci, 165
prijenos argumenta po vrijednosti, 162
prototip, 152, 494
provjera argumenta, 154
razluivanje preoptereenih funkcija, 191
redoslijed deklaracije i poziva funkcije,
155
referenca kao argument, 165
rekurzivna, 195
rezultat, 288
s neodreenim argumentom, 176
s podrazumijevanim argumentom, 192,
201
standardna. Vidi standardna funkcija
stvarni argument, 154
tijelo, 153
tip, 158
umetnuta, 188, 489
void, 159
vraanje vrijednosti, 153, 158
znakovni niz kao argument, 169
funkcijski lan, 221
const, 253
isti virtualni, 380
inline, 226
iskljuivanje iz nasljeivanja, 353
javni, 226
konstantni, 253
podrazumijevano pravo pristupa, 227
pokaziva na, 277
poziv, 223, 372
pravo pristupa, 226
pristup, 223
privatni, 226
puno ime, 223, 261
razlika statikog lana i statikog poziva,
376
razlika statikog povezivanja i statikog
lana, 491
statiki, 260
statiki, poziv, 261
umetnuti, 225
virtualne osnovne klase, 385
virtualni, 374
volatile, 257
vraanje reference na objekt, 224
zatieni, 226

Principi objektno orijentiranog dizajna

G
garbage collection. Vidi skupljanje smea
gcount(), 526, 552
get(), 524
getline(), 526
global scope. Vidi globalno podruje
global scope pollution. Vidi zagaenje
globalnog podruja
globano podruje, 262
gmtime(), 589
good(), 516
goodbit, 516
goto, 94
definiranje oznake, 95
grananje toka, 77, 79, 84

H
.h, 495
header file. Vidi zaglavlje
heap. Vidi hrpa
hex, 63, 533, 538
hijerarhija operatora, 74
.hpp, 495
hrpa, 128, 213
HUGE_VAL, 575

I
IDE, 22
identifikacija tipa, 463, 465
before(), 466
dynamic_cast, 468
name(), 466
type_info, 465
typeid, 465
typeinfo.h, 465
usporedba, 465
identifikator, 35
dozvoljeno ime, 35
#if, 481
if, 79
#ifdef, 481
#ifndef, 248, 481
ifstream, 91, 515, 541
ignore(), 526
imenik, 434

Korak 6: Implementacija

alternativno ime, 436


bezimeni, 437
definicija lana, 435
deklaracija, 435
deklaracija lana, 435
deklaracija using, 439
direktiva using, 443
pristup lanu, 437
pristup lanu bezimenog imenika, 438
proirivanje, 436
proirivanje i deklaracija using, 440
proirivanje i direktiva using, 444
puni naziv lana, 435
puni naziv ugnjeenog lana, 436
ugnjeivanje, 436
umjesto static, 437
using direktiva unutar imenika, 444
using namespace, 443
implementacija klase, 229
implementacija objekta, 215, 578
implementation. Vidi objekt,
implementacija
in, 545
#include, 156, 475, 494
", 475
<>, 475
inheritance. Vidi nasljeivanje
inicijalizacija, 39
const objekta, 54
dinamiki alociranog objekta, 128
izvedene klase, 355
konstantnog lana, 244
pokazivaa, 116
polja, 103
polja objekata, 250
polja znakovnih nizova, 143
redoslijed inicijalizacije, 237, 356
reference, 145
reference kao lana, 243
specijalizirana, statikog lana predloka,
420
statikog lana u predloku klase, 419
statikog objekta, 186
statikog podatkovnog lana, 258
u konstruktoru, 237
virtualne osnovne klase, 387
viedimenzionalnog polja, 109
znakovnog niza, 139
inline, 188, 226, 394, 489
inline function. Vidi funkcija, umetnuta

623

input stream. Vidi tok, ulazni


insertion operator. Vidi tok, operator
umetanja
instruction pointer. Vidi pokaziva
instrukcija
int, 41, 44
integer. Vidi cijeli broj
integral promotion. Vidi tip, cjelobrojna
promocija
integrirana razvojna okolina, 22
internal, 533
internal linkage. Vidi povezivanje,
unutarnje
iomanip, 573
iomanip.h, 205, 538
ios, 515, 573
iosfwd, 573
iostate, 516
iostream, 573
iostream.h, 26, 205, 513, 515, 517, 521
isalnum(), 529, 582
isalpha(), 529, 582
iscntrl(), 582
isdigit(), 528, 582
isgraph(), 582
islower(), 582
isprint(), 582
ispunct(), 582
isspace(), 528, 582
istream, 515, 546, 573
istringstream, 553
istrstream, 553
isupper(), 582
isxdigit(), 582
iterator, 573
izlazni tok, 26. Vidi tok, izlazni
iznimka, 446
bacanje, 449
bad_cast, 469
blok hvatanja, 448, 452, 453
blok pokuaja, 448, 452
dojava pogreke iz konstruktora, 460
hvatanje, 450
hvatanje objekta ili reference, 457
i dinamika alokacija, 455
konverzija prilikom hvatanja, 450
lista moguih iznimaka, 458
neoekivana iznimka, 459
neuhvaena, 453

624

Principi objektno orijentiranog dizajna

odmatanje stoga, 452


odreivanje bloka hvatanja, 453
podizanje, 448
prosljeivanje, 454
terminate(), 453
tijek obrade, 451
tip, 449
tipa ..., 454
unexpected(), 459
izvedbeni kd, 20
izvedena klasa, 332, 335
deklaracija, 335
destruktor, 356
inicijalizacija, 355
konstruktor, 355
konverzija pokazivaa na, 350
najdalje izvedena, 388
podrazumijevani konstruktor, 356
podruje, 341, 360
pravo pristupa, 344
izvorni kd, 21
u vie datoteka, 486

J
javno suelje, 215, 229
definicija, 571
javni pristup lanu, 229
jezik
Actor, 555
Ada, 12
Algol68, 12
asembler, 11, 511
BASIC, 11
BCPL, 11
C, 11, 14
C i C++, usporedba, 14
C s klasama, 11
C++, 555
Clu, 12
COBOL, 11
FORTRAN, 11, 556
PASCAL, 315
Simula, 11
SmallTalk, 470, 555

K
kasno povezivanje, 373
klasa, 37, 215

apstraktna, 330, 380


definicija unutar deklaracije friend,
231
deinicijalizacija osnovne klase, 356
deklaracija, 218, 232
deklaracija izvedene klase, 335
deklaracija unaprijed, 220
deklaracija using, 441
destruktor, 216
destruktor izvedene klase, 356
eksplicitni konstruktor, 302
funkcijski lan, 221, 372
implementacija, 229
inicijalizacija lana, 237
inicijalizacija konstantnog lana, 244
inicijalizacija osnovne klase, 355
inicijalizacija reference kao lana, 243
izvedena, 332, 335
javna osnovna, 335, 345, 365
javno suelje, 229
kao argument, 363
konstantni funkcijski lan, 253
konstruktor, 216, 233, 281
konstruktor kopije, 241, 285
kontejnerska, 408. Vidi kontejnerska
klasa
konverzija konstruktorom, 300
konverzija prilikom nasljeivanja, 357
lokalna, 270
najdalje izvedena, 388
nasljeena, pravo pristupa, 344
nasljeivanje instance predloka, 427
nasljeivanje predloka iz, 428
operator ., 221
operator ::, 223
osnovna, 332, 335
podatkovni lan, 219
podrazumijevani konstruktor, 239
podrazumijevano pravo pristupa, 227
podruje imena, 219, 262
podruje izvedene klase, 341, 360
podruje ugnjeene klase, 268
pokaziva this, 224
prava pristupa lokalne klase, 270
pravo pristupa, 226
predloak. Vidi predloak klase
predloak nasljeuje predloak, 428
prijatelj klase, 216, 230
pristup lanu, 220
pristup nasljeenom lanu, 341

625

Korak 6: Implementacija

pristup ugnjeenoj klasi, 269


privatna osnovna, 335, 347
puni naziv predloka klase, 413
razlika izmeu klasa i struktura, 293
razlika klase i objekta, 215
redoslijed inicijalizacije lanova, 237
statiki funkcijski lan, 260
statiki podatkovni lan, 257
tijelo, 218
type_info, 465
ugnjeena, 265
umetnuti funkcijski lan, 225
virtualna osnovna, 384
volatile funkcijski lan, 257
zaglavlje, 218
zatiena osnovna, 335, 349
kljuna rije, 35
kd
izvedbeni, 20
izvorni, 21, 486
objektni, 22, 486
komentar, 29
upute za komentiranje, 30
konstanta. Vidi simbolika konstanta
konstruktor, 216, 233
dojava pogreke, 460
eksplicitan poziv, 281
eksplicitni, 302
explicit, 302
i alokacija memorije, 236
i polja objekata, 250
i virtualni poziv, 379
inicijalizacija lana, 237
inicijalizacija dinamikog objekta, 238
inicijalizacija konstantnog lana, 244
inicijalizacija osnovne klase, 355
inicijalizacija reference kao lana, 243
inicijalizacijska lista, 355
izvedene klase, 355
konstruktor konverzije i operator =, 370
konstruktor kopije, 233, 241, 285
konstruktor kopije, i iznimke, 458
konstruktor kopije, poziv, 241
konverzija konstruktorom, 300
konverzija konstruktorom i nasljeivanje,
370
podrazumijevani, 233, 239
podrazumijevani, izvedene klase, 356
poziv, 234, 238, 240
poziv za globalne i statike objekte, 249

prosljeivanje parametara, 240


redoslijed pozivanja, 356, 389
kontejnerska klasa, 408
asocijativni kontejner, 577
deque, 577
list, 577
map, 578
multiset, 578
nizovi, 577
pamenje elemenata ili pamenje
pokazivaa, 411
set, 577
vector, 577
vezana lista, 336, 408
konverzija. Vidi dodjela tipa
argumenta kod instantacije predloka,
400
bad_cast, 469
const_cast, 471
dynamic_cast, 468
eksplicitna, 302
i preoptereenje operatora, 311
iznimke prilikom hvatanja, 450
konstruktorom, 300
konstruktorom i nasljeivanje, 370
korisniki definirana, 301, 366
nanie, 467
nanie, sigurna, 468
navie, 467
operator konverzije, 302
pokazivaa, 347, 348, 350, 364, 371
razluivanje operatora konverzije, 304
reference, 347, 348, 350
reinterpret_cast, 473
standardna, 347, 348, 350, 357, 364
static_cast, 471
statika, 471
trivijalna, 191, 364
ugraenih tipova, 40
kvalifikator
const, 53, 136, 172, 413
volatile, 54, 139, 413

L
labs(), 591
late binding. Vidi povezivanje, kasno
LC_ALL, 601
LC_COLLATE, 601

626

Principi objektno orijentiranog dizajna

LC_CTYPE, 601
LC_MONETARY, 601
LC_NUMERIC, 601
LC_TIME, 601
lconv, 600
ldexp(), 595
ldiv(), 593
ldiv_t, 593
left, 533
limits.h, 43, 205
#line, 485
_ _LINE_ _, 266, 326, 478, 485
linked list. Vidi vezana lista
linker. Vidi poveziva
list, 573, 577
lista. Vidi vezana lista
locale, 573
locale.h, 205
localeconv(), 600
localtime(), 589
log(), 193, 595
log10(), 595
logiki operator
i, 57
ili, 57
negacija, 57
logiki tip, 57
lokalna varijabla, 78
long, 41, 44
long double, 43, 44
long int, 41, 44
lvrijednost, 39, 127
promjenjiva, 39

M
macro name. Vidi makro ime
main(), 24, 202, 249, 453, 530
makro funkcija, 391, 479
problemi s parametrima, 480
standardna, 573
makro ime, 476
standardno, 573
malloc(), 325
manipulator, 537
dec, 538
dodavanje, 539
endl, 521, 538
ends, 521, 538

flush, 520, 538


hex, 538
implementacija, 538
oct, 538
resetiosflag(), 538
setbase(), 538
setfill(), 538
setiosflags(), 538
setprecision(), 538
setw(), 531, 538
ws, 538
map, 573, 577, 578
math.h, 43, 46, 193, 199, 203, 204, 205
meupohranjivanje, 514
streambuf, 515
meuspremnik, 514
pranjenje, 514
member function. Vidi funkcijski lan
member selection operator. Vidi operator,
za pristup lanu
memchr(), 601
memcmp(), 602
memcpy(), 601
memmove(), 601
memorijska napuklina, 251
memory, 572
memory leak. Vidi memorijska napuklina
memset(), 602
method. Vidi metoda
metoda, 221
mktime(), 590
mod toka, 545
app, 545
ate, 545
binary, 545
in, 545
nocreate, 545
noreplace, 545
out, 545
trunc, 545
modf(), 595
modul, 486
most derived class. Vidi najdalje izvedena
klasa
multiple inheritance. Vidi nasljeivanje,
viestruko
multiset, 578
mutable, 255

627

Korak 6: Implementacija

N
\n, 60
najdalje izvedena klasa, 388
name(), 466
nameless namespace. Vidi imenik,
bezimeni
nameless union. Vidi unija, bezimena
namespace, 435. Vidi imenik
namespace extension. Vidi imenik,
proirivanje
naredba
break, 84, 93
case, 84
continue, 94
default, 84
do-while, 92
else, 80
for, 86
goto, 94
goto, definiranje oznake, 95
if, 79
nastavak u sljedei redak, 31
pisanje, 30
pretprocesorska, 474
return, 153, 158
strukturiranje, 95
switch, 84
throw, 449
throw, bez parametara, 454
while, 90
nasljeivanje, 12, 14, 217, 332
deinicijalizacija izvedene klase, 356
deklaracija izvedene klase, 335
deklaracija using, 441
deklaracija virtualne osnovne klase, 384
i podruje, 360
i predloak klase, 427
i preoptereenje, 363
i pripadnost, 354
i ugnjeeni tipovi, 361
inicijalizacija osnovne klase, 355
instance predloka, 427
iskljuivanje lana, 353
javno, 345
konstruktor izvedene klase, 355
konstruktora konverzije, 370
lista osnovnih klasa, 335
operatora =, 369

operatora delete, 371


operatora new, 371
podruje izvedene klase, 341
polimorfizam, 371
predloka iz konkretne klase, 428
predloka iz predloka, 428
preoptereenog operatora, 368
pristup nasljeenom lanu, 341
private, 335
privatno, 347
protected, 335
public, 335
razlika nasljeivanja i preoptereenja,
361, 368
razlika nasljeivanja i prijateljstva, 350
standardna konverzija, 357
tip nasljeivanja, 335
virtualna osnovna klasa, 384
viestruko, 335, 360
zatieno, 344, 349
NDEBUG, 248, 483, 574
nested class. Vidi klasa, ugnjeena
nevezano prijateljstvo, 427
nevirtualni lan, 377
::new, 324
new, 128, 572
alokacija dinamikog objekta, 238
alokacija na eljenom mjestu, 251
globalna verzija, 324
nasljeivanje, 371
preoptereenje, 323
::new [], 324
new [], 129, 134
globalna verzija, 324
i polja objekata, 250
preoptereenje, 323
new.h, 251
nocreate, 545
noreplace, 545
not, 70
not_eq, 70
NULL, 119, 575
nul-pokaziva, 119, 575
nul-znak, 140
numeric, 573

O
.o, 22

628

.obj, 22
object. Vidi objekt
object oriented programming. Vidi objektno
orijentirano programiranje
objekt, 37, 215
automatski, 128
deklaracija, 232
deklariran na osnovu strukture, 292
dinamika alokacija, 238
dinamiki, 128
globalni, 182, 185, 248
implementacija, 215
inicijalizacija, 356
kao argument, 363
lokalni, 181, 185
polje objekata, 249
predloka klase, 413
privremeni, 241, 280, 315
privremeni, eksplicitno stvoren, 282
privremeni, prilikom vraanja iz
funkcije, 288
privremeni, za prijenos po vrijednosti,
284
razlika objekta i klase, 215
smjetajne klase globalnog objekta, 185
statiki, 248, 413
statiki, u funkciji, 186
stvaranje, 234
svojstvene operacije, 372
tono podudaranje tipova, 364
trivijalna konverzija, 364
vanjski, 413
objektni kd, 22, 486
objektno orijentirana paradigma, 12, 556
objektno orijentirano programiranje, 215,
555
oct, 533, 538
odmatanje stoga, 452
odnos
biti, 563
jedan na jedan, 565
jedan na vie, 565
korisiti, 564
posjedovati, 564
vie na jedan, 565
vie na vie, 565
ofstream, 515, 541
omota, 575
OOP, 555
open(), 544

Principi objektno orijentiranog dizajna

operator, 308
!, 57
!=, 59
#, 480
##, 480
%, 46
%:, 70
%:%:, 70
%=, 69
%>, 70
&, 63
& (dohvaanje adrese), 114
&&, 57
&=, 69
(), 154
(), preoptereenje, 317
*, 46
* (deklaracija pokazivaa), 114, 120
* (dereferenciranje), 115
*=, 69
+, 46
++, 45, 125
++, preoptereenje, 321
+=, 69
,, 74, 88
-, 46
--, 45, 125
--, preoptereenje, 321
-=, 69
->, 221
->, preoptereenje, 318
->*, 274, 278
., 221
.*, 274, 278
/, 46
/=, 69
::, 186, 223, 259, 262, 270, 435, 436,
437
::, i izvedena klasa, 342
::, i virtualni lan, 379
:>, 70
<, 59
<%, 70
<:, 70
<<, 26, 28
<< (izlazni operator), 517
<< (pomak ulijevo), 65

Korak 6: Implementacija

<<=, 69
<=, 59
=, 39, 69
= i konstruktor konverzije, 370
=, nasljeivanje, 369
=, preoptereenje, 313
==, 59
>, 59
>=, 59
>>, 27
>>, 28
>> (pomak udesno), 65
>> (ulazni operator), 521
>>=, 69
?:, 83
[], 104
[], preoptereenje, 315
^, 64
^=, 69
|, 64
|=, 69
||, 57
~, 62
alternativne oznake, 35, 70
and, 70
and_eq, 70
aritmetiki, 45
binarni, 45, 308
bitand, 70
bitor, 70
bitovni, 62
compl, 70
const_cast, 471
definicija preoptereenog operatora, 309
dekrement, 45
delete, 129, 247, 381
delete, nasljeivanje, 371
delete, preoptereenje, 323
delete [], 129, 250
delete [], preoptereenje, 324
dodjele tipa, 50, 139, 400
dozvoljeni operatori za preoptereenje,
307
dynamic_cast, 468
hijerarhija, 74
inkrement, 45
izluivanja, 521
konverzije, 302

629

logiki, 57
new, 128
new, nasljeivanje, 371
new, preoptereenje, 323
new [], 129, 134, 250
new [], preoptereenje, 323
not, 70
not_eq, 70
obnavljajueg pridruivanja, 69, 125
operator, 308
or, 70
or_eq, 70
poredbeni, 58, 125
postfiks, 45
poziv operatorske funkcije, 310
prefiks, 45
preoptereenje, 299, 307
preoptereenje i nasljeivanje, 368
pridruivanja, 39
razlika = i ==, 59, 83
razlikovanje << i >>, 28
razluivanje operatora konverzije, 304
redoslijed izvoenja operatora, 74
reinterpret_cast, 473
sizeof, 43, 73
static_cast, 471
typeid, 465
umetanja, 517
unarni, 45, 308
uvjetni, 83
xor, 70
xor_eq, 70
za odreivanje podruja, 186, 270, 342,
379
za pristup lanu, 220
za razluivanje imena, 223
za razluivanje podruja, 435, 436, 437
operator overloading. Vidi operator,
preoptereenje
oporavak od iznimke, 448
or, 70
or_eq, 70
osnovna klasa, 332, 335
deinicijalizacija, 356
dominacija, 387
inicijalizacija, 355
javna, 335, 345, 365
konverzija pokazivaa, 358

630

Principi objektno orijentiranog dizajna

konverzija pokazivaa na, 347, 348, 364,


371
privatna, 335, 347
virtualna, 384
virtualna, deklaracija, 384
virtualna, i pravo pristupa, 386
virtualna, inicijalizacija, 387
virtualna, pristup lanu, 385
zatiena, 335, 349
ostream, 515, 546, 573
ostrstream, 553
out, 545
output stream. Vidi tok, izlazni
overriding. Vidi zaobilaenje prilikom
nasljeivanja

P
pametni pokaziva, 320
parametar. Vidi argument
partial specialization. Vidi predloak
funkcije, djelomina specijalizacija
peek(), 527
petlja, 77
beskonana, 88
do-while, 92, 93
for, 86, 93
razlika for i while, 91
s uvjetom na kraju, 92
s uvjetom na poetku, 86, 90
ugnjeivanje, 89
while, 90, 93
plitka kopija, 242
pobrojenje, 55
ugnjeeno u predloak, 423
podatkovni lan, 219
inicijalizacija konstantnog lana, 244
inicijalizacija konstruktorom, 237
inicijalizacija reference kao lana, 243
iskljuivanje iz nasljeivanja, 353
javni, 226
mutable, 255
podrazumijevano pravo pristupa, 227
pokaziva na, 272
pravo pristupa, 226
pristup, 220
privatni, 226
puno ime, 223, 261
razlika statikog povezivanja i statikog
lana, 491

redoslijed inicijalizacije, 237


statiki, 257
statiki i datoteka zaglavlja, 258
statiki, inicijalizacija, 258
statiki, pristup, 259
statiki, u predloku klase, 419
statiki, u predloku klase, inicijalizacija,
419
statiki, u predloku, specijalizirana
inicijalizacija, 420
virtualne osnovne klase, 385
zatieni, 226
podatkovni segment, 213
podruje, 78, 262
i nasljeivanje, 360, 368
imenik, 435
podruje klase, 219
pravila za razluivanje, 264
problem spajanja podruja, 434
ugnjeene klase, 268
zagaenje globalnog podruja, 435
pogonitelj, 576
pogreka
prilikom izvoenja, 22
prilikom povezivanja, 22
prilikom prevoenja, 22
point of instantiation
Vidi predloak funkcije, mjesto
instantacije.
Vidi predloak klase, mjesto instantacije.
pointer. Vidi pokaziva
pointer aliasing, 242
pokaziva, 114
aritmetika, 124
const na const objekt, 138
deklaracija, 114, 120
dekrement, 125
dereferencirani, poziv lana, 379
dodavanje broja, 124
dodjela const pokazivaa, 138
dozvoljene operacije s pokazivaima na
lan, 274
inicijalizacija, 116, 118
inkrement, 125
kao povratna vrijednost funkcije, 179
konstantni, 137
konverzija u pokaziva na osnovnu
klasu, 358, 364, 371
meusobne operacije, 117
mijenjanje unutar funkcije, 166

Korak 6: Implementacija

na const objekt, 137


na lan klase, 271
na lan, implementacija, 274
na lan, koritenje, 274, 278
na funkcijski lan, 277
na funkcijski lan, deklaracija, 278
na funkciju, 196
na funkciju s podrazumijevanim
argumentom, 201
na podatkovni lan, 272
na podatkovni lan, deklaracija, 272
na pokaziva, 134, 167
neinicijalizirani, 118
NULL, 119
nul-pokaziva, 119
odbacivanje konstantnosti, 139
oduzimanje pokazivaa, 125
operator &, 114
operator * (deklaracija), 114, 120
operator * (dereferenciranje), 115
preusmjeravanje, 116
sinonim za tip, 148
slinost s referencom, 145
standardna konverzija, 347, 348, 350
this, 224, 261, 277
usporedba, 125
usporedba s nulom, 125
veza s poljem, 121
virtualni poziv, 378
visei, 148, 286
void *, 118
vptr, 375
zauzee memorije, 119
pokaziva datoteke, 548
beg, 549
cur, 549
end, 549
seekdir, 549
streamoff, 549
streampos, 548
pokaziva instrukcija, 213
pokaziva na virtualnu tablicu, 375
pokaziva stoga, 213
polimorfizam, 12, 14, 218, 371, 463
polimorphysm. Vidi polimorfizam
polje, 102
adresa poetnog lana, 121
lanovi, 102
deklaracija, 102
dinamika alokacija, 129

631

dinamika alokacija viedimenzionalnog


polja, 133
dvodimenzionalno, 108
indeks, 102, 104
inicijalizacija, 103
jednodimenzionalno, 102
kao argument funkcije, 168
kao povratna vrijednost funkcije, 181
nedozvoljeni indeks, 107
objekata, 249
objekata, dinamiki alocirano, 250
objekata, inicijalizacija, 250
pohranjivanje u memoriju, 123
raspon indeksa, 122
veza s pokazivaem, 121
viedimenzionalno, 109, 113
viedimenzionalno, inicijalizacija, 109
viedimenzionalno, razliitih duljina
redaka, 135
zauzee memorije, 113
znakovnih nizova, 143
polje bitova, 297
ponovna iskoristivost, 14, 555, 558
popratne pojave, 163
posebna sekvenca, 60
", 60
\', 60
\0, 60
\?, 60
\\, 60
\a, 60
\b, 60
\ddd, 60
\f, 60
\n, 60
\r, 60
\t, 60
\v, 60
\xddd, 60
postfiks operator, 45
potpis funkcije, 153
poveziva, 21
povezivanje, 486
dinamiko, 373
kasno, 373
s C funkcijama, 510
s drugim programskim jezicima, 508
statiko, 373, 491
unutarnje, 185, 489
vanjsko, 185, 489, 492

632

povezivanje kda, 21
pow(), 46, 163, 596
poziv
C funkcije, 509
destruktora, 246
dinamiki, 375
iz destruktora, 379
iz konstruktora, 379
konstruktora, 234, 238, 240
konstruktora kopije, 241
preko objekta, 379
preko pokazivaa, 378
preko reference, 378
razlika statikog poziva i statikog lana,
376
rekurzivan, 195
statiki, 376
statiki, unato virtualnoj deklaraciji, 379
virtualni, 375
virtualni iz lana klase, 378
virtualnog destruktora, 381
poziv funkcije, 154
#pragma, 484
pravo pristupa, 226
definicija unutar deklaracije friend,
231
friend, 230
i deklaracija using, 442
i konstruktor, 245
i virtualna osnovna klasa, 386
javno, 226
lokalne klase, 270
podrazumijevano, 227
prilikom nasljeivanja, 344
privatno, 226
zatieno, 226, 344
praznina, 30, 31
precision(), 535
predloak funkcije, 211, 391
<>, 211, 392, 398
definicija, 392
definicija funkcije, 393
deklaracija funkcije, 393
djelomina specijalizacija, 405
extern, 394
formalni argument, 392, 403
inline, 394
instantacija, 394, 395
instantacija, eksplicitna, 402
instantacija, implicitna, 397

Principi objektno orijentiranog dizajna

konstantan argument, 396


konverzija argumenta, 400
mjesto instantacije, 429
navoenje argumenata, 395, 398
nevezano prijateljstvo, 427
odreivanje funkcije, 399
odreivanje specijalizirane funkcije, 406
organizacija kda, 497
podrazumijevani argument, 396, 412
povratna vrijednost, 394
predloak funkcijskog lana, 424
preoptereenje, 403
provjera sintakse, 400
rezultat funkcije, 404
specijalizacija, 405
static, 394
stvarni argument, 394
typename, 395
upotreba, 393
usporeivanje argumenata, 399
vezano prijateljstvo, 426
predloak klase, 408
<>, 409
argument, 409
const, 413
definicija, 409
djelomina specijalizacija, 418
extern, 413
instantacija konstantnim izrazom, 421
instantacija, eksplicitna, 416
instantacija, implicitna, 412
izraz kao argument, 409
konstantan izraz kao argument, 420
mjesto instantacije, 429
nasljeivanje, 427
nasljeivanje instance, 427
nasljeivanje konkretne klase, 428
nevezano prijateljstvo, 427
organizacija kda, 497
predloak funkcijskog lana, 424
predloak nasljeuje predloak, 428
prijateljstvo, 426
pristup ugnjeenom tipu, 423
provjera sintakse, 413
puni naziv klase, 413
specijalizacija, 416
specijalizacija cijele klase, 417
specijalizacija funkcijskog lana, 417
static, 413
statiki lan, 419

633

Korak 6: Implementacija

statiki lan, inicijalizacija, 419


statiki lan, specijalizirana
inicijalizacija, 420
tip ugnjeen u predloak, 422
ugnjeena pobrojenja, 423
ugnjeeni, 423
vezano prijateljstvo, 426
volatile, 413
prefiks L za dugi znak, 62
prefiks operator, 45
prekid, 54
preljev, 46
preoptereenje
funkcije, 189
i nasljeivanje, 363
operatora, 299, 307
operatora --, 321
operatora (), 317
operatora [], 315
operatora ++, 321
operatora <<, 519
operatora =, 313
operatora ->, 318
operatora >>, 523
operatora delete, 323
operatora delete [], 324
operatora i nasljeivanje, 368
operatora new, 323
operatora new [], 323
operatora, unutar ili izvan klase, 310
poziv operatorske funkcije, 310
predloka funkcije, 403
razlika preoptereenja i nasljeivanja,
361, 368
tono podudaranje tipova, 364
pretprocesor, 474
", 475
#, 480
##, 480
#define, 53, 476, 479
#define, trajanje definicije, 477
#elif, 482
#else, 482
#endif, 482
#error, 484
#if, 481
#ifdef, 481
#ifndef, 481
#include, 156, 475, 494

#line, 485
#pragma, 484
#undef, 478
\ (nastavak reda), 474
_ _cplusplus, 478, 510
_ _DATE_ _, 478
_ _FILE_ _, 266, 326, 478, 485
_ _LINE_ _, 266, 326, 478, 485
_ _STDC_ _, 478
_ _TIME_ _, 478
<>, 475
makro funkcija, 391, 479
makro ime, 476
naredba, 474
NDEBUG, 483
pretvorba nanie, 467
pretvorba navie, 467
prevoditelj, 21
prevoenje kda, 21
prijatelj klase, 216, 230
i predloci, 426
printf(), 176
private, 226, 335, 347, 384
private base class. Vidi osnovna klasa,
privatna
programersko suelje (API), 558
programiranje
objektno orijentirano, 13
pogonjeno dogaajima, 13
proceduralno, 12
projekt, 487
protected, 226, 335, 344, 349, 384. Vidi
pravo pristupa, zatieno
protected base class. Vidi osnovna klasa,
zatiena
prototip funkcije, 152, 494
public, 221, 226, 335, 345, 384
public base class. Vidi osnovna klasa, javna
public interface. Vidi javno suelje
pure virtual function member. Vidi
funkcijski lan, isti virtualni
put(), 520
putback(), 527

Q
qsort(), 602
queue, 573

634

Principi objektno orijentiranog dizajna

R
\r, 60
raise(), 603
raising an exception. Vidi iznimka,
podizanje
rand(), 93, 187, 575, 596
RAND_MAX, 93, 575, 596
randomize(), 93
razlika klase i objekta, 215
rdstate(), 516
read(), 526, 552
realni broj, 40. Vidi tip, double
redoslijed izvoenja operatora, 74
referenca, 144
deklaracija, 144
inicijalizacija, 145
kao povratna vrijednost funkcije, 179
na konstantan objekt, 145
na pokaziva, 168
slinost s pokazivaem, 145
standardna konverzija, 347, 348, 350
trivijalna konverzija, 364
virtualni poziv, 378
visea, 411
za hakere, 145
registarska smjetajna klasa. Vidi
smjetajna klasa, registarska
register, 182
reinterpret_cast, 473
rekurzija, 195
relacija
biti, 354, 563
jedan na jedan, 565
jedan na vie, 565
korisiti, 564
posjedovati, 564
sadri, 354
vie na jedan, 565
vie na vie, 565
resetiosflag(), 538
return, 153, 158
reusability. Vidi ponovna iskoristivost
right, 533
rukovanje iznimkama, 448
run-time type identification. Vidi
identifikacija tipa

S
scientific, 533, 535
scope. Vidi podruje
scope resolution operator. Vidi operator, za
odreivanje podruja
seekdir, 549
seekg(), 529, 548
seekp(), 521, 548
set, 573, 577
set_new_handler(), 604
set_terminate(), 605
set_unexpected(), 605
setbase(), 538
setf(), 532
setfill(), 538
setiosflags(), 538
setlocale(), 601
setprecision(), 538
setw(), 90, 538
shallow copy. Vidi plitka kopija
short, 44
short int, 44
showbase, 533
showpoint, 533, 535, 536
showpos, 533
side-effects. Vidi popratne pojave
SIG_DFL, 603
SIG_ERR, 603
SIG_IGN, 603
SIGABRT, 603
SIGFPE, 603
SIGILL, 603
SIGINT, 603
signal(), 603
SIGSEGV, 603
SIGTERM, 603
simbolika konstanta, 52, 489. Vidi const
inicijalizacija, 54
sin(), 199, 597
sinh(), 597
size_t, 74, 323, 371
sizeof, 43, 73
skipws, 533
skrivanje podataka, 12, 557, 571
skupljanje smea, 11, 320
smart pointer. Vidi pametni pokaziva
smjetajna klasa, 181

635

Korak 6: Implementacija

automatska, 182
imenik umjesto static, 437
mutable, 255
registarska, 182
statika, 185
vanjska, 185
source code. Vidi izvorni kd
specijalizacija predloka funkcije, 405
sqrt(), 204, 597
srand(), 596
sstream, 553, 573
stack, 573. Vidi stog
stack pointer. Vidi pokaziva stoga
stack unwinding. Vidi iznimka, odmatanje
stoga
standard C++, 18
makro funkcije, 573
makro imena, 573
standardna biblioteka predloaka, 577
standardne klase, 578
standardne strukture, 578
standardne vrijednosti, 576
standardni tipovi, 576
zaglavlja, 571
Standard Template Library. Vidi standardna
biblioteka predloaka
standardna biblioteka, 571
standardna biblioteka predloaka, 577
standardna funkcija, 580
abort(), 574, 598
abs(), 591
acos(), 591
asctime(), 588
asin(), 592
atan(), 205, 592
atan2(), 204, 592
atexit(), 598
atof(), 203, 586
atoi(), 587
atol(), 587
ceil(), 592
clock(), 589
cos(), 593
cosh(), 593
ctime(), 588
difftime(), 589
div(), 593
exit(), 210, 249, 544, 600
exp(), 193, 594

fabs(), 199, 591


floor(), 592
fmod(), 594
free(), 325
frexp(), 594
gmtime(), 589
isalnum(), 582
isalpha(), 529, 582
iscntrl(), 582
isdigit(), 528, 582
isgraph(), 582
islower(), 582
isprint(), 582
ispunct(), 582
isspace(), 528, 582
isupper(), 582
isxdigit(), 582
labs(), 591
ldexp(), 595
ldiv(), 593
localeconv(), 600
localtime(), 589
log(), 193, 595
log10(), 595
malloc(), 325
memchr(), 601
memcmp(), 602
memcpy(), 601
memmove(), 601
memset(), 602
mktime(), 590
modf(), 595
pow(), 46, 163, 596
printf(), 176
qsort(), 602
raise(), 603
rand(), 187, 575, 596
set_new_handler(), 604
set_terminate(), 605
set_unexpected(), 605
setlocale(), 601
signal(), 603
sin(), 199, 597
sinh(), 597
sqrt(), 204, 597
srand(), 596
strcat(), 208, 583

636

strchr(), 583
strcmp(), 209, 583
strcoll(), 583
strcpy(), 207, 583
strcspn(), 584
strlen(), 169, 207, 584
strlwr(), 209
strncat(), 583
strncmp(), 583
strncpy(), 584
strpbrk(), 584
strrchr(), 585
strspn(), 584
strstr(), 585
strtod(), 587
strtok(), 585
strtol(), 587
system(), 605
tan(), 597
tanh(), 598
terminate(), 453, 604
time(), 591
tolower(), 586
toupper(), 586
unexpected(), 459, 605
standardna klasa, 578
basic_string, 578
complex, 578
valarray, 573
standardna makro funkcija, 573
assert(), 236, 248, 483, 574
va_arg(), 179, 576
va_end(), 179, 576
va_start(), 179, 576
standardna struktura, 578
standardna vrijednost, 576
standardni tip, 576
standardno makro ime, 573
standardno zaglavlje, 571
state. Vidi tok, stanje
static, 185, 186, 258, 394, 413, 491
razlika statikog povezivanja i statikog
lana, 491
static binding. Vidi povezivanje, statiko
static call. Vidi poziv, statiki
static_cast, 471
statiki poziv, 376

Principi objektno orijentiranog dizajna

razlika statikog poziva i statikog lana,


376
unato virtualnoj deklaraciji, 379
statiko povezivanje, 373, 491
stdarg.h, 178, 205
_ _STDC_ _, 478
stddef.h, 74
stdexcept, 572
stdlib.h, 93, 187, 205, 210, 325
STL. Vidi standardna biblioteka predloaka
stog, 128, 213
storage class. Vidi smjetajna klasa
strcat(), 208, 583
strchr(), 583
strcmp(), 209, 583
strcoll(), 583
strcpy(), 207, 583
strcspn(), 584
streambuf, 515, 529, 573
streamoff, 549
streampos, 548
string. Vidi znakovni niz
string (klasa), 572
string (zaglavlje), 572, 578
string.h, 142, 205, 206
strlen(), 169, 207, 584
strlwr(), 209
strncat(), 583
strncmp(), 583
strncpy(), 584
Stroustrup, Bjarne, 11
strpbrk(), 584
strrchr(), 585
strspn(), 584
strstr(), 585
strstream.h, 553
strtod(), 587
strtok(), 585
strtol(), 587
struct, 292
structure. Vidi struktura
struktura, 292
razlika izmeu struktura i klasa, 293
razlika u C i C++ jezicima, 293
strukturiranje kda, 95
suelje. Vidi javno suelje
sufiks brojane konstante, 51
f, 51

637

Korak 6: Implementacija

F, 51
l, 51
L, 51
u, 51
U, 51
switch, 84
system(), 605

T
\t, 60
tablica virtualnih lanova, 375
tan(), 597
tanh(), 598
tellg(), 529, 548
tellp(), 521, 548
template
Vidi predloak funkcije.
Vidi predloak klase.
template, 211, 392, 405, 409
terminate(), 453, 604
this, 224, 261, 277
throw, 449
bez parametara, 454
lista moguih iznimaka, 458
throwing an exception. Vidi iznimka,
bacanje
tie(), 530
_ _TIME_ _, 478
time(), 591
time.h, 205, 588
time_t, 588
tip, 37
bool, 57
broj, 40
char, 44, 60
char *, 139
cjelobrojna promocija, 47
double, 43, 44
float, 42, 44
identifikacija. Vidi identifikacija tipa
int, 41, 44
iznimke, 449
klasa, 215
logiki, 57
long, 41, 44
long double, 43, 44
long int, 41, 44

pobrojani, 55
pokaziva. Vidi pokaziva
pokaziva na lan klase, 271
pokaziva na funkciju, 196
polje, 102
pravila konverzije, 40, 47
pravila provjere tipa, 40
razlika izmeu char i char *, 142
referenca. Vidi referenca
short, 44
short int, 44
sinonim za pokazivaki tip, 148
type_info, 465
typedef, 71, 490
ugnjeen u predloku, 422
ugnjeeni, i nasljeivanje, 361
ugraeni, 40
unsigned, 44
usporedba, 465
void, 159
wchar_t, 62, 515
wchar_t *, 143
znakovni, 60
znakovni niz, 60, 139
tm, 590
tok
bad(), 516
badbit, 516
beg, 549
binarno pisanje i itanje, 550
cerr, 516
cin, 27, 515, 521
clear(), 516, 548
clog, 516
close(), 545
cout, 26, 516, 517
cur, 549
itanje i pisanje u datoteku, 541
end, 549
eof(), 516
eofbit, 516, 548
fail(), 516
failbit, 516, 522
fill(), 531
flags(), 532
flush(), 520
fstream, 515, 541, 546
gcount(), 526, 552

638

get(), 524
getline(), 526
good(), 516
goodbit, 516
ifstream, 91, 515, 541
ifstream, konstruktor, 544
ignore(), 526
ios, 515
iostate, 516
isalnum(), 529
ispis korisnikih tipova, 519
istream, 515, 546
istringstream, 553
istrstream, 553
izlazni, 26, 513
konvezija u void *, 516
main(), 530
manipulator. Vidi manipulator
meupohranjivanje, 514
mod otvaranja, 545. Vidi mod
ofstream, 515, 541
ofstream, konstruktor, 544
open(), 544
operator !, 517
operator izluivanja, 521
operator umetanja, 517
ostream, 515, 546
ostrstream, 553
otvaranje datoteke, 544
peek(), 527
precision(), 535
put(), 520, 554
putback(), 527
rdstate(), 516
read(), 526, 552
seekdir, 549
seekg(), 529, 548
seekp(), 521, 548
setf(), 532
setw(), 90
stablo nasljeivanja, 515
stanje, 516
str(), 554
streambuf, 515, 529
streamoff, 549
streampos, 548
irina ispisa, 530
tellg(), 529, 548

Principi objektno orijentiranog dizajna

tellp(), 521, 548


testiranje stanja, 516
tie(), 530
ulazni, 27, 513
unsetf(), 532
upis korisnikih tipova, 523
vezanje cin i cout, 530
vezivanje, 529
werr, 516
width(), 530
wifstream, 515
win, 516
wios, 515
wistream, 515
wlog, 516
wofstream, 515
wostream, 515
wout, 516
write(), 520, 552
wstreambuf, 515
zastavica, 532. Vidi zastavica
zatvaranje datoteke, 545
znak za popunjavanje, 531
tolower(), 586
toupper(), 586
trigraf nizovi, 71
trigraph. Vidi trigraf nizovi
true, 57
trunc, 545
try, 448
try block. Vidi blok, pokuaja
type cast. Vidi dodjela tipa
type_info, 465
before(), 466
name(), 466
typedef, 71, 148, 490
typeid, 465
typeinfo, 572
typeinfo.h, 465
typename, 395

U
ugnjeena klasa. Vidi klasa, ugnjeena
ugnjeivanje imenika. Vidi imenik,
ugnjeivanje
ukljuivanje asemblerskog kda, 511
ulazni tok, 27. Vidi tok, ulazni

639

Korak 6: Implementacija

unbound template friendship. Vidi nevezano


prijateljstvo
#undef, 478
unexpected(), 459, 605
unija, 294
anonimna, 296
bezimena, 296
diskriminanta unije, 295
razlika u C i C++ jezicima, 294
union, 294. Vidi unija
union discriminant. Vidi unija,
diskriminanta unije
unitbuf, 533
unnamed temporary. Vidi objekt,
privremeni
unsetf(), 532
unsigned, 44
upcast. Vidi pretvorba navie
upozorenje pri prevoenju i povezivanju,
23
uppercase, 533, 536
using
deklaracija, 439
deklaracija i proirivanje, 440
deklaracija unutar klase, 441
direktiva, 443
direktiva i proirivanje, 444
direktiva u sklopu imenika, 444
using declaration. Vidi using,
deklaracija
usporedbe, 58
utility, 572
uvjetni operator, 83
uvjetno prevoenje, 481
pronalaenje pogreaka, 483

V
\v, 60
va_arg(), 179, 576
va_end(), 179, 576
va_list, 178, 576
va_start(), 179, 576
valarray, 573
values.h, 43
vanjsko povezivanje, 492
varijabla, 35, 37
deklaracija, 38
lokalna, 78

podruje, 78
pridruivanje vrijednosti, 38
vidljivost unutar bloka, 77
vector, 573, 577
vezana lista, 336
izbacivanje lana, 339
pokaziva glava, 337
pokaziva rep, 337
pomou predloaka, 408
umetanje lana, 337
vezano prijateljstvo, 426
virtual, 375, 381, 384, 556
deklaracija virtualne osnovne klase, 384
virtual base class. Vidi virtualna osnovna
klasa
virtual call. Vidi virtualni poziv
virtual function member. Vidi virtualni,
funkcijski lan
virtualna osnovna klasa, 384
deklaracija, 384
dominacija, 387
i pravo pristupa, 386
inicijalizacija, 387
pristup lanu, 385
virtualni
isti virtualni funkcijski lan, 380
destruktor, 380
funkcijski lan, 343, 374
virtualni poziv, 375
iz destruktora, 379
iz konstruktora, 379
odreivanje lana, 378
preko pokazivaa, 378
preko reference, 378
zaobilaenje, eksplicitno, 379
viestruko nasljeivanje, 335
void, 159
volatile, 54, 139, 413
const_cast, 471
funkcijski lan, 257
vptr, 375
vtable, 375

W
wchar_t, 62, 515
wchar_t *, 143
werr, 516
while, 90

640

Principi objektno orijentiranog dizajna

width(), 530
wifstream, 515
win, 516
wios, 515
wistream, 515
wlog, 516
wofstream, 515
wostream, 515
wout, 516
wrapper. Vidi omota
write(), 520, 552
ws, 538
wstreambuf, 515
wstring, 572

X
xor, 70
xor_eq, 70

Z
zagaenje globalnog podruja, 435
zaglavlje, 26, 155, 494
algorithm, 573
assert.h, 236, 483
bits, 573
cassert, 572
cctype, 572
cerrno, 572
clocale, 573
cmath, 573
complex, 573, 578
complex.h, 205, 206
csetjmp, 572
csignal, 572
cstdarg, 572
cstdio, 573
cstdlib, 572, 573
cstring, 572
ctime, 572, 588
ctype.h, 528
cwchar, 572, 573
cwctype, 572
deque, 573, 577
exception, 572
float.h, 205

fstream, 573
fstream.h, 91, 206, 541
i statiki objekt, 258
iomanip, 573
iomanip.h, 205, 206, 538
ios, 573
iosfwd, 573
iostream, 573
iostream.h, 26, 205, 206, 513, 515,
517, 521
istream, 573
iterator, 573
limits.h, 43, 205, 206
list, 573, 577
locale, 573
locale.h, 205, 206
map, 573, 577
math.h, 43, 46, 193, 199, 203, 204,
205, 206
memory, 572
new, 572
new.h, 251
numeric, 573
ostream, 573
queue, 573
set, 573, 577
sstream, 553, 573
stack, 573
standardna, 571
standardne datoteke, 205
stdarg.h, 178, 205, 206
stddef.h, 323
stdexcept, 572
stdio.h, 126
stdlib.h, 93, 187, 205, 206, 210, 325
streambuf, 573
string, 572, 578
string.h, 142, 205, 206
strstream.h, 553
to u nj ne staviti, 497
to u nj staviti, 495
time.h, 205, 206, 588
typeinfo, 572
typeinfo.h, 465
utility, 572
valarray, 573
values.h, 43
vector, 573, 577

Korak 6: Implementacija

zaobilaenje prilikom nasljeivanja, 333


zastavica, 532
boolalpha, 533
dec, 533
fixed, 533, 535
grupa adjustfield, 533
grupa basefield, 533
grupa floatfield, 533
hex, 533
internal, 533
left, 533
oct, 533
right, 533
scientific, 533, 535
showbase, 533

641

showpoint, 533, 535, 536


showpos, 533
skipws, 533
unitbuf, 533
uppercase, 533, 536
znakovni niz, 60, 139
funkcije za manipulaciju, 206
kao argument funkcije, 169
kao povratna vrijednost funkcije, 181
polje znakovnih nizova, 143
rastavljanje, 31
zakljuni nul-znak, 140
Error! Cannot open file referenced on
page 641

You might also like