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


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 4.1. 4.2. 4.3. 4.4.

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

6.3. 6.4. 6.5. 6.6. 6.7.

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


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

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.

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


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

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.

Detaljnije o kljunoj rijei catch ...............................................................453 Navoenje liste moguih iznimaka ...........................................................458 Obrada pogreaka konstruktora ................................................................460

15.

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


Statiki i dinamiki tipovi............................................................................463 Operator typeid ......................................................................................465 Sigurna pretvorba ......................................................................................467 Ostali operatori konverzije .........................................................................471 Promjena konstantnosti objekta.............................................................471 Statike dodjele .....................................................................................471

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

16.

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


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

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.

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


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

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

18.

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


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.1. 18.2. 18.3. 18.4. 18.4.1. 18.4.2. 18.4.3. 18.5.

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.

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

19.

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


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

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.

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= a + b ;

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

int int

a a

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

a a

int int 5 5

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; i = 0x20; cout << i << endl; // oktalni prikaz broja 32

// 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
char short int int long int float double long double

bajtova 1 2 2 (4) 4 4 8 10

raspon vrijednosti 32768 do 32767 32768 do 32767 2147483648 do 2147483647 2147483648 do 2147483647 3,41038 do 3,41038 3,41038 do 3,41038 1,710308 do 1,710308 1,710308 do 1,710308 i i

tonost

7 dec. znamenki 15 dec. znamenki 18 dec. znamenki

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

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

1 2

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

int

-32767 -32768 32767

32766

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 cijeli

sufiks
L, l U, u F, f L, l

rezultirajui tip
int long int unsigned int double float long double

decimalni

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

a .i. b

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

tono pogreno tono pogreno engleskog

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

56

Tablica 4.8. Stanja za logiki ili

a .ili. b

tono tono pogreno pogreno

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

tono pogreno tono pogreno

(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

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

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

// 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; cout << (a + 1) << endl; b = a + 1; cout << b << endl; // ASCII kd 104

// ispisuje 105

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

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

komplement binarni i binarni ili iskljuivi ili pomakni ulijevo pomakni udesno

0
B7

0
B6

1
B5

0
B4

0
B3

1
B2

0
B1

1 ~00100101 = 11011010
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.

00100101 & 00001111 = 00000101

B7

B6

B5

B4

B3

B2

B1

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.

00100101 | 00001111 = 00101111

B7

B6

B5

B4

B3

B2

B1

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

00100101 ^ 00001111 = 00101010

B7

B6

B5

B4

B3

B2

B1

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 B5 B4 B3 B2 B1 B0 B7 B6 B5 B4 B3 B2 B1 B0

0 0 0 1 0 0 1 0 1
B7

B6

B5

B4

B3

B2

B1

B0

0
B7 B6 B5 B4 B3 B2 B1 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 B6 B5 B4 B3 B2 B1 B0

0
B7

0
B6

0
B5

0
B4

0
B3

1
B2

0
B1

0
B0

Baud1200 |

0
B7

0
B6

0
B5

0
B4

1
B3

0
B2

0
B1

0
B0

Bitova8 |

0
B7

0
B6

1
B5

0
B4

0
B3

0
B2

0
B1

0
B0

StopB2 |

ParitetNjet

bez64748 pariteta
B7 B6 B5

8 bitova podataka 678


B4 B3 B2 B1 B0

0
1442443

14243

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

B6

B5

B4

B3

B2

B1

B0

0
B7

0
B6

0
B5

0
B4

0
B3

1
B2

1
B1

1
B0

00000111 &

0
B7

0
B6

1
B5

0
B4

1
B3

1
B2

0
B1

0
B0

SerCom

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 B4 B3

2
B2

3
B1

4
B0

144424443

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

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

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
and or not bitand bitor xor

osnovna
~ != &= |= ^=

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
:: :: -> . [] () ++ -sizeof ++ -* & + - ! ~ new delete typeid () ->* .* * / % + << >> < > <= >= == != & ^ | && || ?: = *= /= += -= &= ^= |= %= >>= <<= ,

znaenje globalno podruje podruje klase izbor lana indeksiranje poziv funkcije uveaj nakon umanji nakon 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 razdvajanje

pridruivanje 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 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 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; long int fjel = 1; for (int i = 2; i <= n; i++) fjel *= i; cout << n << "! = " << fjel; return 0; }

// manji od 13!

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

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


i =0

x 2i +1

x3

x5

x7

x9

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
x[0]: float[5] x x[1]: float x[2]: float x[3]: float x[4]: float

float 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).
x[0]: float[5] x x[1]: float 4.7 x[2]: float 23.4

x, te se lanovima tog polja

unutar vitiastih zagrada i navedena, prevoditelj e iz 5 i rezervirati odgovarajui

x[3]: float 12.

x[4]: float -7.2e12

float 1342.5

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:
f ( x ) Li ( x ) f i = L0 ( x ) f 0 + L1 ( x ) f 1 + Ln ( x ) f n ,
i =0 n

pri emu je Li(x) Lagrangeov koeficijent:

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

y x3 , f3

xn , fn x1 , f1 x 2 , f2 x
Slika 6.3. Lagrangeova interpolacija

x0 , f0

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.

14243

80.

40.

1442443
int b[3]

50.

60.

10.

1442443
int a[3]

20.

30.

int c[2]

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 1. redak 2. redak 3. redak 214 ... ... 2. stupac ... ... 101 3. stupac ... ... ... 4. stupac ... ... ... 5. stupac ... ... ...

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 11

int 12

int 13

int 14

int 15

Tablica Tablica Tablica Tablica Tablica [1][0]: [1][1]: [1][2]: [1][3]: [1][4]: int 21 int 22 int 23 int 24 int 25

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

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 koeficijentima a32) , odnosno a42) , da bismo iz njih uklonili nepoznanicu x2. 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] a[0][1]

2. redak 644474448
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

n n

int int 5 5

b)

int* int*

kazalo : kazalo

int* int*

int int

n n

int int 5 5

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*

kazalo : kazalo

int* int*

int int

n n

int int 8 8 int int 3 3

a)

int int

m m

int* int*

kazalo : kazalo

int* int*

int int

n n

int int 8 8 int int 3 3

int int

m m

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; nesvrstan = &n; nesvrstan = pokx;

// pokaziva na void // 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

x[0]: float[5] float[5] x x : float float

x[1]: float float

x[2]: float float

x[3]: float float

x[4]: 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] a float[3][4] a : a[0][0]: a[0][1]: a[0][2]: a[0][3]: float float 1.1 1.1 float (*a)[0] float (*a)[0] float (*a)[1] float (*a)[1] 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

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

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 ( xi - ) i , x y 1 x ( xi - )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 1 nx = xi , n i= 0

1 1 ny = 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** float** :

float*[0]

float*[1]

float*[redaka] float*[redaka]

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

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' 'a' 'u' 's' 't' ' ' 'V' 'r' 'a' 'n' '' 'i' '' '\0' 'F' 'a' 'u' 's' 't' ' ' 'V' 'r' 'a' 'n' '' 'i' '' '\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 i 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 i reference: oboje omoguavaju posredan pristup int i int int nekom objektu. Reference se i implementiraju kao 5 5 int& iref : pokazivai. Meutim, pokaziva se moe int& 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; { int j = 100; pok = &j; } // ... // varijabla j vie ne postoji! cout << *pok << endl; // upitan ispis return 0; } // lokalna varijabla u bloku // pokaziva usmjeravamo na nju // pokaziva

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(); int main() { // ... funkcijaPrva(); // ... funkcijaDruga(); } funkcijaPrva(){ // ... } funkcijaDruga(){ // ... funkcijaPrva(); // ... } // definicija funkcije // deklaracije funkcija

// 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; } int main() { char *korisnik; UnesiIme(&korisnik); cout << korisnik << endl; delete [] korisnik; } // parametar je pokaziva // na pokaziva // pristup preko pokazivaa

// 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; } int main() { char *korisnik; UnesiIme(korisnik); // parametar je referenca // na pokaziva // pristup preko reference

// 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; int main() { dan = 30; mjesec = 1; godina = 1997; mjesec = noviMjesec(); return 1; } int noviMjesec() { if (++mjesec > 12) { mjesec = 1; godina++; } return mjesec; }

// globalne varijable

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; int main() { float a = 50; cout << a << endl; }

// globalna varijabla

// 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; int main() { float a = 50; cout << a << endl; cout << ::a << endl; } // globalna varijabla

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

x0

& f ( x) dx = h

f ( xn ) f ( x0 ) + f ( x1 ) + f ( x2 ) + + f ( xn 1 ) + 2 2

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 = I2 = ... 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);

h0 f ( x0 ) f ( x2 ) 2 + f ( x1 ) + 2 2 h0 f ( x0 ) f ( x4 ) 2 + f ( x1 ) + f ( x2 ) + f ( x3 ) + 2 4

f ( x)

x0

x1

x2

xn1

xn

Slika 7.3. Trapezno pravilo.

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
complex.h fstream.h float.h iomanip.h iostream.h limits.h locale.h math.h stdarg.h stdlib.h string.h time.h

Opis
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

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
m a l i \0

strcpy(oba, prvi); char *oba


m a l i \0

Slika 7.4. Djelovanje funkcije strcpy().

208

char *oba char *praznina

a l i \0 \0

strcat(oba, praznina); char *oba


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

Tablica B
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"); return 0; }

// // // //

konverzija prilikom inicijalizacije konverzija prilikom prenoenja parametara

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

Poligon

Krug

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

Poligon

Krug

Pravokutnik

TekstObj

TKrug

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
public protected private

Pravo pristupa u osnovnoj klasi


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

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

Ovakvom

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; template <class T> class XX<T, char>; // opi predloak // 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.)); } namespace Ispis { void Ispisi(Vektor &); } int main() { Ispisi(Vektor(5., 6.)); return 0; } // OK: lan je sada dio // imenika

// OK // pogreka: lan jo nije // ubaen u imenik

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

Tablica 16.1. Pretprocesorske naredbe

#if #ifdef #ifndef #elif #else #endif

#include #define #undef #line #error #pragma

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; #define POZDRAV "Hvala, tebi takoer." cout << POZDRAV << endl;

// Dobar dan!

// 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); // neto tu zaudara...

pretprocesor razviti u kd

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() { // ... } double lokalnaKonstanta = 210.; // varijabla s vanjskim pov. // funkcija s vanjskim // povezivanjem

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; svecnst2.cpp // samo deklaracija definicija je negdje drugdje: extern const float pi; cout << pi << endl; // ispisuje 3.14159... // definicija

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() { //... } bastian.cpp extern int bwv; extern int toccata(); float fuga() { // samo deklaracija // takoer // deklaracija & definicija

// definicija funkcije

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
// deklaracija // klase Tocka

pravac.h
#include "tocka.h" // deklaracija // klase Pravac

kruznica.h
#include "tocka.h" #include "pravac.h" // deklaracija i // definicija // klase Kruznica

tocka.cpp
#include "tocka.h"

pravac.cpp
#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" #include "pravac.h" #include "kruznica.h" // deklaracije // funkcija iz // presjek.cpp

int main() { //... }

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

double dy); a; } b; } c; }

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.); PozoviPaIspisi(k, p); return 0; } postavljamo toku polumjer krunice definiramo krunicu pravac kroz sredite krunice pod 45 stupnjeva

// // // // //

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
// deklaracija // klase Tocka

pravac.h
#include "tocka.h" // 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
left right internal dec hex oct fixed scientific showpos showpoint showbase uppercase skipws boolalpha unitbuf

grupa
adjustfield adjustfield adjustfield basefield basefield basefield floatfield floatfield

znaenje
poravnanje ulijevo poravnanje udesno predznak lijevo, ostatak desno 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 ispis brojeva u znanstvenoj notaciji ispis predznaka + pozitivnim brojevima ispis decimalne toke za sve realne brojeve ispis prefiksa koji ukazuje na bazu cijelih brojeva zamjena nekih malih slova (npr. 'e', 'x') velikim slovima (npr. 'E', 'X') preskakanje praznina pri uitavanju umetanje i izluivanje bool tipa u slovanom obliku pranjenje izlaznog toka nakon svake izlazne operacije

// 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
setw(int) setfill(int) dec hex oct setbase(int) setprecision(int) setiosflags(long) resetiosflag(long) flush endl ends ws

zaglavlje
iomanip.h iomanip.h iostream.h iostream.h iostream.h iomanip.h iomanip.h iomanip.h iomanip.h iostream.h iostream.h iostream.h iostream.h

znaenje
irina polja za ispis/uitavanje znak za popunjavanje praznina dekadski prikaz heksadekadski prikaz oktalni prikaz postavi bazu broj znamenki postavlja zastavicu brie zastavicu 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
app ate binary in nocreate noreplace out trunc

znaenje
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 A.7. Standardne strukture definirane predlocima Tablica A.6. Standardna biblioteka predloaka
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_array not_equal_to less_equal messages pair logical_and messages_byname plus logical_not moneypunct random_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').
// stari naziv: <ctype.h>

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

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.

Korak 6: Implementacija

587

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

Korak 6: Implementacija

589

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.

Korak 6: Implementacija

591

#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); #include <cmath> float abs(float x); double abs(double x); long double abs(long double x); float fabs(float x); // stari naziv: <stdlib.h>

// 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); float floor(float x); double floor(double x); long double floor(long double x); // stari naziv: <math.h>

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;

Korak 6: Implementacija

593

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

Korak 6: Implementacija

595

#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); float log10(float x); double log10(double x); long double log10(long double x); // stari naziv: <math.h>

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:

Korak 6: Implementacija

597

#include <stdlib.h> #include <iostream.h> #include <time.h> int main() { time_t cajt; srand((unsigned) time(&cajt)); for (int i = 0; i < 10; i++) cout << (rand() % 6 + 1) << endl; return 0; }

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

char *mon_thousands_sep; char char char char *mon_grouping; *positive_sign; *negative_sign; int_frac_digits;

char frac_digits;

Korak 6: Implementacija

601

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.

Korak 6: Implementacija

605

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

Korak 6: Implementacija

607

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

Korak 6: Implementacija

609

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

Korak 6: Implementacija

611

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

Korak 6: Implementacija

613

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

Korak 6: Implementacija

615

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

Korak 6: Implementacija

617

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

Principi objektno orijentiranog dizajna

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

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

619

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

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

Principi objektno orijentiranog dizajna

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

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

621

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

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

Principi objektno orijentiranog dizajna

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

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

623

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

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

Korak 6: Implementacija

625

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

Korak 6: Implementacija

627

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

Principi objektno orijentiranog dizajna

.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

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

629

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

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

631

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

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

Principi objektno orijentiranog dizajna

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

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

Korak 6: Implementacija

633

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

Korak 6: Implementacija

635

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

Principi objektno orijentiranog dizajna

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

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

Korak 6: Implementacija

637

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

Principi objektno orijentiranog dizajna

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

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

Korak 6: Implementacija

639

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

641

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

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