PočetnaHelpdeskMala škola C++ Obrada teksta

Mala škola C++ Obrada teksta


Petlje i grananja

Ukoliko ste pažljivo pročitali početak Škole C++a, a niste imali iskustva s programiranjem prije čitanja članka, sada već vladate osnovnim pojmovima poput varijabli, tipovima istih te načina upotrebe funkcija.
Pri stvaranju vlastitog programa, najčešće je cilj obrada teksta, u bilo kojem obliku. Obrada teksta se može svoditi na jednostavne zadatke poput zamjene određenog dijela teksta drugim tekstom i slično, no može biti potrebna i onda kada to nije intuitivno jasno. Recimo, želimo li napraviti program koji, kada upišemo “2+3” izbaci rezultat 5, a kada upišemo “2*3+0” izbaci 6, već nam je potrebno znanje o obradi teksta i radu sa stringovima. Naime, u navedenom primjeru, programu nisu predani isključivo brojevi (2 i 3) nego i tekst (operatori za zbrajanje i množenje).

Problem je i u tome što želimo programu moći predati proizvoljan broj brojeva i operatora te program mora cijeli unos promatrati kao tekst, dio po dio analizirati i vršiti određene operacije nad unešenim tekstom kako bi “znao” da korisnik želi zbrojiti, odnosno, pomnožiti brojeve. Ovim člankom ćemo postupno i doći do takvog programa koji pokriva velik dio bitnih stvari za programiranje: obrada teksta, pretvorba teksta u brojeve, promjene toka programa te načine provjere ispravnog unosa podataka od strane korisnika. Prvim primjerom dobivamo osnovni kalkulator koji računa samo zbroj ili umnožak dva broja, a kôd možete vidjeti u okviru sa strane.

Novosti u ovom primjeru u odnosu na primjere iz prošlog članka su grananje programa pomoću naredbe “if” te novospomenuti tip podataka “char”. Char predstavlja tip varijable koji može spremiti jedan alfanumerički znak (engl. character). U pozadini, podatak tipa char je spremljen kao broj koji poprima vrijednosti od -128 do 127 ili od 0 do 255, ovisno koristi li se s predznakom ili bez njega. O rasponu vrijednosti nije potrebno voditi brigu, jer char ionako nećemo nikad koristiti kao broj, nego kao znak. No, ipak, dobro je znati da je pozadina iza podatka tipa char brojevnog karaktera, jer će se u nekim slučajevima znakovima lakše moći baratati ako koristimo njihovu brojevnu pozadinsku prirodu.

Svaki znak unešen tipkovnicom može se promatrati kao broj, za svaki znak je rezervirana njegova ekvivalentna brojevna vrijednost, odnosno ASCII vrijednost. Tako npr. veliko slovo “A” ima ASCII vrijednost 65, dok broj 0 ima vrijednost 48. Ove dvije vrijednosti je vrlo korisno zapamtiti, jer se pomoću ove dvije vrijednosti lako dolazi i do ostalih znakova. Recimo, b roj 1 ima vrijednost 49, broj 2 vrijednost 50 i tako sve do broja 9, dok slovo “B” ima vrijednost 66 i dalje se vrijednosti opet povećavaju sve do slova “Z”. Dobro je znati i to da se malo slovo abecede dobiva zbrajanjem broja 32 na ASCII vrijednost velikog slova, pa će tako malo slovo “a” imati vrijednost 97.

U našem primjeru, u sedmoj liniji kôda deklarira se varijabla tipa char i imena “znak”. Ova varijabla predstavlja operator kojeg je korisnik unijeo, dakle, u nju će se pohraniti znak zbrajanja ili množenja.
Osmom linijom kôda korisnik unosi izraz koji želi izračunati. Vidi se da cin prima 3 unosa: prvi i drugi su operandi, dok je drugi operator. Ukoliko korisnik unese “12+3”, cin čita po redu: prva varijabla a je tipa int pa se čita unešeni tekst sve dok se ne dođe do znaka koji ne može predstavljati broj. Tako se čita brojka 1, zatim brojka 2 te se zatim dolazi do znaka “+”, koji nije brojka. Tada, broj 12 se sprema u varijablu a, čitanje se nastavlja od znaka “+”, a pročitani podatak će se spremiti u varijablu znak, koja je tipa char. Pošto varijabla znak može prihvatiti samo jedan znak, čita se “+” i sprema u nju, te naposljetku, broj 3 se sprema u varijablu b.
U devetoj liniji pojavljuje se naredba if. Sintaksa ove naredbe zajednička je sintaksi petlji, kao i sintaksi definicije funkcije utoliko što se sastoji od tri glavna dijela. Prvi dio je sama ključna riječ “if”, koja predstavlja naredbu grananja, drugi dio je onaj unutar oblih zagrada koji sadrži uvjet grananja te treći dio predstavlja blok naredbi unutar vitičastih zagrada koji se izvodi uz zadovoljen uvjet.

U oble zagrade se stavlja logički izraz koji može poprimiti dvije vrijednosti: istinu i laž, true i false, jedinicu i nulu. U slučaju if grananja koje počinje devetom linijom, ako je uvjet u oblim zagradama zadovoljen, izvršavaju se sve naredbe u pridjeljenom bloku koji počinje otvorenom vitičastom zagradom u devetoj liniji, a završava zatvorenom vitičastom zagradom u jedanaestoj liniji kôda. Ovdje je to samo jedna, deseta linija, koja vrši ispis.
U uvjetu se provjerava je li vrijednost varijable znak jednaka znaku ‘+’. Primjetite da je znak zbrajanja stavljen u jednostruke, a ne dvostruke navodnike. Razlog tomu je taj što C++ razlikuje jednostruke i dvostruke navodnike: jednostruki uvijek predstavljaju tip podatka char, dok dvostruki predstavljaju niz znakova ili string. Zbog toga, “+” predstavlja string, dok ‘+’ predstvalja znak.
Operator == ispituje jednakost lijeve i desne strane. Bitno je zapamtiti da se pri ispitivanju uvjeta koristi takav, dvostruki znak jednakosti, jer je on rezerviran upravo za tu zadaću, dok normalan znak jednakosti predstavlja pridruživanje desne strane lijevoj. Ako bismo u uvjet stavili (znak=’+’), varijabli bi se dodijelio znak ‘+’, a uvjet bi bio istinit jer bi operacija dodjeljivanja vrijednosti varijabli bila izvršena. Za ostale provjere, veće, manje, veće/jednako i manje/jednako koriste se klasični operatori: >, = i <=.

Dvanaesta linija kôda sadrži ključnu riječ else. Naredba else uvijek ide u kombinaciji s naredbom if, a predstavlja događaj neispunjavanja uvjeta u prethodnom if-u. U našem slučaju, ako znak nije jednak operatoru zbrajanja (deseta linija nije izvršena), izvršava se blok naredbe else. Ovdje, else ne sadrži blok naredbi, već samo jednu naredbu i to if te pošto sadrži jednu naredbu, ne koriste se vitičaste zagrade. Isti slučaj je i s petnaestom linijom – nije definiran blok naredbe else, nego samo jedan naredbeni redak koji vrši ispis.

Dakle, ukoliko nije učitan znak zbrajanja, vrši se if u dvanaestoj liniji kôda – provjerava se je li možda znak jednak znaku množenja. Ako je, ispisuje se umnožak učitanih brojeva. Ako znak nije niti znak množenja, tada je zadovoljen zadnji else u petnaestoj liniji kôda, nije ispunjen nijedan od dva uvjeta, te se ispisuje poruka o pogrešnom unosu.

Vidimo da se na relativno jednostavan način može napraviti jednostavan kalkulator. No ipak, ovaj kalkulator ima brojne mane. Jedna od njih je da postoje samo dva operatora s kojima radi, a za svaki novi operator dobivamo po tri linije kôda što povećava program. Također, može se vidjeti da su ti if blokovi dosta međusobno slični: svaki od njih vrši provjeru nad istom varijablom te programer mora iste dijelove kôda više puta upisivati. Postavlja se pitanje: postoji li način da se program pojednostavi, a da mu se ujedno poveća funkcionalnost i olakša dodavanje novih operatora? Postoji.

U drugom primjeru koristimo drugi tip grananja koji je kao stvoren za naš kalkulator. Prije svega, valja primjetiti da je osim izbacivanja if-ova tip varijabli a i b postavljen na float, tako da sada možemo računati i s decimalnim brojevima.

Grananje switch-case se koristi onda kada jednoj varijabli ispitujemo vrijednost. Ako varijabla ima određenu vrijednost, izvršava se jedan blok naredbi, ako ima drugu, onda drugi blok naredbi i tako dalje. Ukoliko varijabla nije jednaka niti jednoj od ponuđenih vrijednosti, izvršava se blok naredbi koji je dakle vezan za nepodudarnost varijable sa svima od ponuđenih vrijednosti.

Ovo grananje počinje ključnom riječi “switch”, u primjeru u devetoj liniji kôda. Kao argument u oble zagrade navodi se varijabla koja se ispituje, nakon čega slijedi blok naredbe switch, omeđen vitičastim zagradama. Unutar bloka navode se slučajevi (engl. case) tako da se navede ključna riječ “case”, zatim vrijednost koja se ispituje nakon čega slijedi dvotočka. Blok ove (pod)naredbe počinje dvotočkom, a završava ključnom riječi break. Tako je blok svake naredbe case sadržan od jedne linije kôda: za četiri slučaja imamo četiri bloka i to jedanaestu, četrnaestu, sedamnaestu i dvadesetu liniju kôda. Na kraju imamo i ključnu riječ default, koja predstavlja slučaj kada niti jedan od prethodnih slučajeva nije zadovoljen.
Bitno je znati što ovdje zapravo radi naredba break. Ona, gledano sa strane sintakse, nije niti potrebna, no kad bismo barem jedan break izbacili, program ne bi radio ono što bismo htjeli.

Uzmimo za primjer što se događa ako je učitan znak ‘+’: kod naredbe switch uzima se vrijednost varijable znak. U sljedećoj liniji ispituje se je li vrijednost jednaka znaku ‘+’. Pošto je, vrši se ispis zbroja, a zatim se izvršava naredba break. Naredba break zaustavlja rad cijelog bloka switch naredbe te program nastavlja s radom na naredbi return(0), odnosno, završava s radom.

Kada bismo izbacili break iz prvog bloka vezanog za zbrajanje (dvanaesta linija), nakon ispisa zbroja, program bi nastavio s radom u trinaestoj liniji. Ovdje se javlja jedna zanimljivost: neće se ispitivati jednakost varijable sa znakom ‘-’, nego će se taj blok jednostavno izvršiti te ćemo dobiti ispis i zbroja i razlike. Blokovi će se redom izvršavati sve dok se ne dođe do naredbe break, kada switch završava s radom. Ovo ponašanje jednostavno možete isprobati tako da “zakomentirate” jedan od breakova dodavanjem “//” ispred naredbe break, bez navodnika. Općenito, na ovaj način se stavljaju komentari koje program pri izvršavanju ignorira.

Zbog načina rada breaka, on nije potreban u slučaju “default”. Ovaj slučaj je ionako zadnji, te je break nepotreban, jer svakako switch završava s radom pošto je tu kraj njegovog bloka.
Možda ste primjetili da ako bismo zadržali funkcionalnost prošlog primjera, dakle, podršku za samo dva operatora, broj linija je ostao isti. Međutim, u prvom primjeru smo morali svaki puta navoditi koju varijablu provjeravamo, dok se kod korištenja switcha ona samo jedanput navede. Općenito, zbog toga se switch koristi pri jednostavnim ispitivanjima vrijednosti jedne varijable, dok se if koristi pri ispitivanju složenijih logičkih izraza.

Prethodnim primjerima dobili smo osnovni kalkulator koji računa izraz s dva broja i jednim operatorom. Međutim, korištenim pristupom vrlo je teško dodatno povećati funkcionalnost kalkulatora. Naprimjer, želimo postići da program konstantno traži ponovni unos kako ne bismo morali za svaki izračun ponovno pokretati program. U tom slučaju, takvo beskonačno traženje ponovnog unosa mora se moći prekinuti. Ako želimo prekinuti rad upisom npr. riječi “exit”, program je potrebno u korijenu mijenjati, jer primjeri očekuju unos broja (int odnosno float), a ne unos teksta (stringa). Zbog toga ćemo u sljedećem primjeru pristupiti problemu tako da ćemo učitati tekst koji ćemo onda obrađivati znak po znak te izvršiti potrebne operacije.

Ovakav, novi pristup otvorit će mnoge mogućnosti u proširivanju fukcionalnosti kalkulatora. Za početak, treći primjer neće vršiti matematičku operaciju nego samo učitani tekst pretvoriti u broj i to čitanjem znakova jedan po jedan. Napominjemo da postoje i ugrađene funkcije koje obavljaju ovaj posao, no ovakvim pristupom ćete bolje razumjeti prirodu stringova i znakova u C++-u. Također, čitanjem znak po znak otvara se mogućnost programiranja kalkulatora koji će moći vršiti operacije nad prozvoljnim brojem unesenih operatora i operanada.

Ideja pretvorbe teksta u broj koja se ovdje primjenjuje je sljedeća: učitani tekst, koji se smije zasad sastojati isključivo od znamenaka, čitamo znak po znak i to od zadnjeg prema prvom. Recimo, ako se radi o broju 256, čitamo broj 6, množimo s 1 i dodajemo u varijablu, koja je inicijalno jednaka nuli. Nakon toga čitamo broj 5, množimo ga s 10 i dodajemo rezultat toj istoj varijabli te naposljetku broj 2 množimo sa 100 te 200 dodajemo istoj varijabli. Rezultat ovoga je da je tekst “256” pretvoren u broj (int) vrijednosti 256, jer je 6*1 + 5*10 + 2*100 = 256.

U šestoj liniji deklariramo tri varijable: varijabla i služi kao brojač petlje (o tome više riječi kasnije), u varijablu a spremaju se međurezultati računanja, dok varijabla brojac služi za pamćenje potencije broja 10 kojom ćemo množiti jedinice, desetice, stotice itd. Osmom linijom jednostavno se uneseni broj čita u varijablu tipa string.

Devetom linijom počinje petlja for. Njenim radom se upravlja argumentima unutar oblih zagrada. Argumenti su podijeljeni u tri cjeline, a odvojeni su znakovima točka-zarez. Prvi argument postavlja inicijalne vrijednosti za petlju, drugi argument predstavlja uvjet ostanka u petlji, a treći argument je tzv. korak petlje.

Petlja radi na sljedeći način: Kada program s izvršavanjem dođe do naredbe for, prvo se postavljaju inicijalne vrijednosti, odnosno, prvo se izvršavaju naredbe unutar prvog argumenta. Nakon toga ispituje se uvjet (drugi argument) te ukoliko je uvjet zadovoljen, izvršava se blok petlje (u našem slučaju, deseta linija kôda). Nakon izvršavanja cijelog bloka, program nastavlja s izvođenjem opet kod naredbe for i to na sljedeći način: obavlja se korak petlje, odnosno, izvode se naredbe u trećem argumentu, zatim se ponovno provjerava uvjet te ako je ispunjen, izvršava se blok petlje. Ovaj postupak se ponavlja sve dok se ne dogodi da uvjet nije zadovoljen – tada petlja prestaje s radom, blok naredbi se preskače i program nastavlja s izvođenjem na prvoj liniji iza bloka (u našem slučaju, dvanaesta linija).

Promotrimo sada petlju u našem primjeru. Za početak, treba razjasniti što znači [unos.size()]. Kao što smo spomenuli u prošlom članku, string nije klasični tip podataka, on je klasa objekata. Svaka klasa objekata ima određene metode koje se mogu primjenjivati nad svim objektima te klase. U našem slučaju, imamo objekt imena “unos” koji pripada klasi stringova. Jedna od mnogih metoda klase string je upravo metoda size(), koja kao rezultat vraća duljinu stringa nad kojim se vrši. Metoda se s objektom “spaja” pomoću točke pa tako ustvari izvodimo metodu size() nad stringom unos. Rezultat ove operacije je da unos.size() vraća duljinu stringa unos; ako unos ima vrijednost “256”, unos.size() će biti jednak 3.

Dosad nismo imali potrebu pristupiti određenom članu stringa koji je zapravo skup članova (znakova). Npr. ako imamo tekst “Ovo je tekst” nismo morali pristupiti petom znaku, odnosno slovu ‘j’, jer su se sve obrade svodile na cijeli učitani string. Međutim, sada, kada učitavamo broj, moramo moći pojedinačno pristupiti svakoj znamenci posebno kako bismo mogli dobiti rezultantni broj i to od zadnje znamenke prema prvoj. Članovi se broje, kao i u ostalim programskim jezicima, od indeksa 0, tako da u tekstu “256”, nulti član će imati vrijednost ‘2’, prvi ‘5’ i drugi ‘6’. Pojedini znak u stringu se indeksira pomoću uglatih zagrada, pa ćemo tako uz unos = “256”, znak ‘5’ dobiti pomoću unos[1].

Ako nam se unos sastoji od tri člana, moramo proći sve članove od zadnjeg s indeksom 2, sve do člana s indeksom 0. Inicijalizacijom varijable i u prvom argumentu petlje postižemo upravo računanje indeksa posljednjeg člana: unos.size() vraća broj 3, njega umanjujemo za 1 te se na kraju broj 2 sprema u varijablu i, što je upravo indeks posljednjeg člana učitanog stringa.

Uvjet ostanka petlje (drugi argument) je upravo taj da je indeks veći ili jednak nuli. Naime, dok god nisu obrađeni svi znakovi učitanog niza, računat ćemo spomenute međurezultate. Kada indeks bude jednak nuli, obrađivat će se prva znamenka (u “256” to je ‘2’) te je to zadnja znamenka koja treba biti obrađena.

Kako se mijenja vrijednost indeksa i? U trećem argumentu, naredba — i smanjuje vrijednost varijable i za jedan. Svaki puta nakon izvršavanja cijelog bloka ona se izvodi, a zatim provjerava uvjet – na kraju će varijabla i poprimiti vrijednost -1, što nije veće ili jednako nuli te će petlja završiti s radom.
U trećem argumentu dodana je još jedna naredba, a odmah se vidi i kako se nižu naredbe u prvom i trećem argumentu: pomoću zareza. Naredba brojac *= 10 množit će vrijednost varijable brojac brojem 10 svakim ponovnim ulaskom u petlju. Općenito, matematički izraz oblika [a OPER= b] jednak je izrazu [a = aOPERb], gdje je OPER bilo koji matematički operator. Tako će uz a=3, nakon a+=5, varijabla a sadržavati vrijednost 8, a nakon a*=10 sadržavati vrijednost 30.

Uz unos “256”, prvim ulaskom u petlju, varijabla i ima vrijednost 2, a ima vrijednost 0 i brojac ima vrijednost 1. Varijable brojac i a su inicijalizirane pri deklaraciji, a varijabla i je inicijalizirana prvim ulaskom u petlju.
U desetoj liniji računamo međurezultat. unos[i] prvim prolaskom vraća znak ‘6’, jer je i=2. Spomenuli smo važnost razumijevanja brojevne prirode alfanumeričkih znakova: znak ‘0’ ima vrijednost 48, a znak ‘6’ ima vrijednost 54 (48+6) te je zbog toga [‘6’ – 48 = 6]. Ovime smo izravno znak ‘6’ tipa char pretvorili u broj 6 tipa int. Ova pretvorba se mogla izvesti i na način unos[i] – ‘0’, no odabrana je upotreba ASCII vrijednosti znaka ‘0’ kako bi se intuitivnije moglo shvatiti o čemu se tu radi.

Množenjem broja 6 s varijablom brojac koja zasad ima vrijednost 1 dobivamo broj jedinica u broju, te taj rezultat dodajemo varijabli a. Sada varijabla a sadrži broj 6. Sljedećim prolaskom kroz petlju, varijabli a će se dodati 50 (10*(‘5’-48)) te će imati vrijednost 56 i zadnjim prolaskom dodat će se 200 (100*(‘2’-48)) pa će imat iželjenu vrijednost 256. Nakon toga i poprima vrijednost -1, petlja završava s radom, izvodi se ispis broja u dvanaestoj liniji kôda te program završava s izvođenjem.

Iako treći primjer zasad nema nikakvu upotrebnu vrijednost, principe koji su ovdje korišteni, koristit će se kasnije pri programiranju naprednijeg kalkulatora. Sada je jasno kako obrađivati string znak po znak te kako broj zapisan u string pretvoriti u broj tipa int s kojim onda možemo vršiti matematičke operacije. Četvrti primjer predstavlja napredniji kalkulator koji omogućuje nizanje proizvoljnog broja operatora i operanada, iako i on ima određena ograničenja, a to su: dozvoljen je unos isključivo pozitivnih cjelobrojnih vrijednosti (uključujući nulu) te kalkulator računa redoslijedom unosa, odnosno, ne podržava prioritete množenja i dijeljenja nad zbrajanjem i oduzimanjem.

Četvrti primjer se može učiniti iznimno kompliciranim u odnosu na treći, no on zapravo predstavlja spoj drugog i trećeg primjera uz dodatak novih naredbi. U varijablu duljina spremit će se duljina unešenog niza, a u varijabli rez spremat će se rezultat koji se na kraju ispisuje.

Ideja ovog kalkulatora je sljedeća: Čitamo niz znak po znak, bez spremanja znakova, slijeva nadesno sve dok se ne pročita nešto što nije znamenka. Kroz taj postupak pamti se koliko je znamenaka pročitano kako bi se mogla utvrditi vrijednost varijable brojac. Kada se prođe kroz sve znamenke prije pojave operatora, ponovnim prolazom kroz njih se teksualno zapisani broj pretvara u podatak tipa int. Ukoliko je to prvi broj u nizu, on se sprema u rezultat, a ukoliko nije, provjerava se prethodno učitani operator. Ako je on recimo operator zbrajanja, novoizračunati broj će se pribrojiti rezultatu. Postupak se ponavlja dok se ne dođe do kraja unešenog niza.

U dvanaestoj liniji počinje petlja while. Ona bi se mogla predstaviti kao petlja for koja podržava samo drugi argument, odnosno ispitivanje uvjeta. Petlja while jednostavno izvršava naredbe svog bloka sve dok se ne dogodi da nakon izvršavanja bloka uvjet nije zadovoljen. U našem slučaju, blok petlje while će se izvršavati sve dok je indeks i manji od duljine unešenog teksta. Indeks i mijenja se u bloku unutar određenih naredbi.

Prvom for petljom indeks j postavlja se na vrijednost indeksa i, kako bi se obrađivao samo neobrađeni dio teksta. Npr. u unešenom tekstu “23+45”, a nakon obrade znaka ‘+’, i će imati vrijednost 3, pa će i j imati tu vrijednost te će se obrađivati tekst “45”. Svakim prolaskom kroz tu for petlju, broj se množi s 10 i povećava se indeks j. Petlja ima samo jednu naredbu, if, pa nema potrebe za korištenjem vitičastih zagrada, a blok naredbe if također čini jedna jedina naredba pa se i ovdje mogu zagrade izostaviti. Unutar uvjeta naredbe if provjerava se je li znak znamenka: ako je ASCII vrijednost znaka manja od ‘0’ ILI je veća od ‘9’, onda znak nije znamenka. Operator or je logički operator koji vraća istinu ako je barem jedan od uvjeta zadovoljen.

Dakle, u bilo koja od dva navedena slučaja cijeli izraz unutar uvjeta naredbe if vratit će istinu te će se izvršiti naredba break. Naredba break prisilno prekida izvršavanje hijerarhijski prve petlje u kojoj se nalazi. U našem slučaju, prekida se izvršavanje prve for petlje, dakle, ovaj break nema utjecaja na vanjsku, while petlju niti na drugu for petlju.

Nakon izvršavanja prve for petlje imamo situaciju da varijabla i predstavlja indeks prve znamenke broja unutar unešenog teksta koji se obrađuje, a varijabla j predstavlja indeks prvog znaka iza broja koji se obrađuje. Varijabla j pokazuje na znak jednog od operatora koje kalkulator podržava, a ako je upravo obrađen zadnji broj u unešenom tekstu, onda je j jednak duljini unešenog teksta. Zbog toga je nužan uvjet u trinaestoj liniji (j < duljina), jer kad ne bi bilo tog uvjeta, program bi pokušao pristupiti znaku koji je izvan unešenog teksta (npr. u slučaju unos=”23+45”; unos[5]; pojavit će se greška, jer se pokušava pristupiti nepostojećem članu stringa).

Varijabla brojac je nakon prve petlje također pripremljena: ako je obrađen string “23”, brojac će sadržavati broj 100. Primjetite da nam je potrebno da brojac sadrži broj 10 (23=10*2+1*3), međutim, on sadrži broj 100. Razlog tomu je upravo redoslijed operacija koje se obavljaju pri izvršavanju petlje: prvo se vrše naredbe trećeg argumenta, a zatim ispituje uvjet u drugom argumentu i ako je uvjet zadovoljen, izvršava blok. Zato se brojač tri puta pomnožio brojem 10.

Druga for petlja pretvara tekst u varijablu tipa int. Inicijalno se vrijednost varijable a postavlja na nulu te se brojac dijeli s deset. Petlja se vrti sve dok je i manji od j. Varijablu i svakim prolaskom povećavamo za jedan, a brojac dijelimo s deset. Ovime se vrši prolazak kroz sve znamenke broja koji se obrađuje te se za svaku znamenku računaju jedinice, desetice itd. i spremaju u varijablu a, baš kao i u trećem primjeru. Razlika je u tome što ovdje računamo slijeva nadesno, a ne zdesna nalijevo, kao što je bio slučaj u prošlom primjeru.

Varijabla znak je inicijalno postavljena na vrijednost ‘#’ jer se ta vrijednost neće nikad pojaviti kao matematički operator. Ovime se postiže mogućnost provjere je li učitani broj prvi broj u nizu koji je unešen. Ako je to zaista prvi broj, tada nije učitan niti jedan operator, pa varijabla znak sadrži upravo vrijednost ‘#’. Osamnaestom linijom se vrši provjera: ako varijabla znak sadrži vrijednost ‘#’, u varijablu rez se sprema upravo izračunati broj. U suprotnom, već spomenutim postupkom međurezultat se zbraja, oduzima, množi ili dijeli novoučitanim brojem, ovisno o sadržaju varijable znak. Primjetite da u C++u nije bitno pišu li se naredbe svaka u svoj red ili ne – pomoću znaka točka-zarez C++ “zna” gdje je kraj kojeg naredbenog retka.

Na kraju bloka naredbe while provjerava se je li i manji od duljine niza. Ako je manji, u varijablu znak se sprema i-ti znak iz unešenog teksta, odnosno znak operatora, te se naposljetku varijabla i povećava za jedan, kako bi ponovnim ulaskom u blok while-a indeks i pokazivao na novi broj.
Valja primjetiti i to da nailaskom na operator unutar unešenog teksta ne počinje računanje s istim, već se on sprema u varijablu znak. Razlog je jednostavan: ako imamo izraz “2+15/4”, zbrajanje se može vršiti tek nakon čitanja broja “15”, jer su tek tada zapamćena oba operanda zbrajanja.

Relativno kratkim programom smo dobili relativno dobar kalkulator koji dakako ima i svoje mane. Ipak, unutar okvira Škole mogu se još uvijek napraviti određena poboljšanja. U petom primjeru uvodimo podršku za decimalne i negativne brojeve, dok je rješavanje prioriteta operatora ipak nešto složeniji proces.

Peti primjer podržava negativne i decimalne brojeve. Koncentrirat ćemo se samo na razlike u kôdu u odnosu na četvrti primjer, jer je većina programa jednaka.
Prvo, pošto uvodimo podršku za decimalne brojeve, potrebno je određene varijable deklarirati kao float, odnosno, kao brojeve s pomičnim zarezom. To je učinjeno s varijablom a, koja predstavlja međurezultat, te s varijablom brojac, koja će sada imati vrijednosti i negativnih potencija broja 10 (npr. 0.1, 0.01 itd.). Uvodimo i dodatnu varijablu naziva predznak, pomoću koje pamtimo je li učitani broj negativan ili pozitivan.

Na početku bloka while petlje provjerava se je li učitani znak jednak znaku ‘-’. Ukoliko je, varijabli predznak pridružuje se vrijednost -1, te se indeks i povećava za jedan kako bi se obrada nastavila na znamenkama. Ovime je većina podrške za računanje s negativnim brojevima ostvarena – preostaje samo množenje s predznakom varijable a pri povezivanju iste s ukupnim rezultatom, što je učinjeno u caseovima. Nakon toga preostaje dodjeljivanje broja 1 varijabli predznak, kako bi ponovnim prolaskom kroz blok while petlje predznak bio postavljen na defaultnu, pozitivnu vrijednost.

Prva for petlja je jednaka – indeks se povećava sve dok se ne dođe do nečeg što nije znamenka. Petlja završava s radom i u slučaju nailaska na decimalnu točku. U tom slučaju, uvjet naredbe if u sedamnaestoj liniji kôda je ispunjen, te se vrti novouvedena petlja sve dok se ne naiđe na nešto što nije znamenka. U ovoj novoj petlji ne povećava se vrijednost varijable brojac, jer ako imamo npr. broj 123.45, odgovara nam da je brojač stao pri čitanju znamenke 3, baš kao da se radi o cijelom broju dok se indeks j povećava i dalje. Druga for petlja iz prošlog primjera je praktički jednaka petlji u ovom primjeru u dvadesetoj liniji – razlika je u dodatnom ispitivanju uvjeta pomoću if naredbe u 21. liniji.

Naime, kada se dođe do decimalne točke, ona se mora preskočiti, jer ne predstavlja znamenku, a brojač se mora pomnožiti s deset kako bi se poništilo bezuvjetno dijeljenje s deset pri svakom prolasku kroz petlju. Naredba continue vrši prisilan nastavak rada petlje bez izvršavanja naredbi bloka. Kada se naredba continue izvrši, cijeli blok naredbi vezan za tu petlju se preskače, a program nastavlja s radom na početku petlje, u našem slučaju u dvadesetoj liniji. Tada se obavlja sve isto kao da je cijeli blok izvršen: izvršava se treći argument i ispituje uvjet u drugom argumentu.

Ovime ste imali priliku naučiti kako se ponašaju stringovi u C++u, kako mijenjati tok programa te pretvarati tekstualni zapis broja u varijablu brojevnog tipa. Također je dana ideja o upozorenjima o greškama: naime, kada god provjeravamo neki uvjet koji mora biti ispunjen, uvijek možemo staviti i suprotan uvjet kojem ćemo pridjeliti poruku o grešci. Tako bismo mogli u zadnjem primjeru dodati poruku o grešci za slučaj default unutar casea zbog krivo unesenog operatora. Moglo bi se i prije čitanja znamenke provjeravati je li učitano nešto što nije znamenka te ako je, ispisati poruku da je korisnik unijeo dva operatora zaredom i slično.

Vidi se i koje se sve mogućnosti otvaraju ako se ne precizira tip unesenih podataka. Da smo ostali pri prvom pristupu unosa na način da se odmah pri samom unosu podaci spremaju u varijable tipa int ili float, bilo bi vrlo komplicirano ili čak i nemoguće postići čitanje proizvoljnog broja operatora i operanada unutar jednog retka, barem s obzirom na predznanje koje tekst podrazumijeva.

Vrlo je bitno paziti na specifične ili granične slučajeve koji se javljaju pri radu, ne samo ovog, nego bilo kojeg programa. Tako u petom primjeru imamo specifične slučajeve kada se čita prva ili zadnja znamenka: prvi je riješen inicijalizacijom varijable znak, dok je drugi riješen čestim provjerama indeksa u odnosu na duljinu niza.

U petom primjeru je vrlo lako postići spomenutu mogućnost beskonačnog upisa: glavna while petlja i njen blok se stavi u dodatnu while petlju u kojoj ispitujemo npr. je li varijabla unos nejednaka tekstu “exit”. Dodatno, na kraju bloka novouvedene petlje bi se dodale linije getline(cin, unos); i duljina = unos.size();, a na početku bloka inicijalizirale bi se varijable i, rez, znak i predznak.
Svakako preporučamo pokretanje petog primjera s dodanim ispisima varijabli u pojedinim dijelovima kôda kako biste vidjeli kad koja varijabla ima koju vrijednost. Ovakvom analizom možete riješiti sve nedoumice glede ponašanja petlji i slično.
Funkcije i rad s datotekama

Za rješavanje nekih osnovnih zadataka u bilo kojem programskom jeziku, pri učenju osnova istih, dovoljne su mogućnosti korisnikovog unosa podataka, obrade te ispisa na ekran. Međutim, i kod takvih relativno jednostavnih programa zna se pojaviti potreba za trajnom pohranom ili pak učitavanjem prethodno spremljenih podataka. Navedeno se može postići jedino korištenjem funkcija za rad s datotekama uz vrlo dobro poznavanje rada s tekstom, odnosno stringovima.

Pri pohrani podataka mogu se javiti različiti problemi na koje je potrebno obratiti pažnju. Za početak, ako će biti potrebno nekim programom čitati te podatke, valja dobro pripaziti na format upisa u datoteku. Recimo, ako pohranjujemo nekakvu tablicu u datoteku, potrebno je osmisliti organizaciju iste; kako će se odvajati pojedine cjeline tablice, stupci, retci, mogu li retci imati različit broj ćelija, hoće li tablica imati nekakvo zaglavlje itd. U slučaju da će program čitati ovakvu tablicu iz datoteke, tablica mora na neki način biti konzistentno formatirana, odnosno imati nekakav vlastiti standard zapisa.

Pošto su pohranjeni podaci isključivo tekstualnog karaktera, svi zapisani podaci su, gledano sa stajališta programskog jezika, su tipa string. Zbog toga, pri čitanju podataka, valja paziti na potrebne pretvorbe koje se moraju obaviti, jer npr. zapisani broj -25.6 je zapravo tekst “-25.6” te ne možemo prije pretvorbe u tip podatka float vršiti nikakve računske operacije nad njim.

U članku ćemo kroz primjere napraviti program koji će predstavljati jednostavan tekst editor, pomoću kojeg ćemo moći vršiti neke operacije nad cijelim tekstom postojeće datoteke, kao naprimjer uklanjanje višestrukih razmaka, postavljanje velikog početnog slova u rečenicama, brojanje riječi itd. Kako bi program bio lijepo organiziran, koristit ćemo se ručno definiranim funkcijama. Za početak, prvi primjer će jednostavno čitati sadržaj postojeće datoteke i prikazivati na na ekran.

Imamo učitana dva modula: prvi je iostream koji bi vam trebao biti otprije poznat. U jednom od prethodnih članaka spomenuto je značenje samog imena ovog modula: Input Output STREAM, odnosno, ulazno-izlazni tok (podataka). Intuitivno bi vam moglo biti jasno čemu onda služi drugi modul – tok datoteka, odnosno, File STREAM. Zaista, C++ na vrlo sličan način barata ovim tokovima podataka, jer i ulazno-izlazni tok i tok datoteke zapravo pripadaju istoj nadklasi te samim time imaju i određeni skup zajedničkih metoda i funkcija koje je moguće primjenjivati nad tim tokovima. Dakle, zaključujemo: modul fstream je potreban za rad s datotekama te je određeni skup metoda i funkcija zajednički za obje vrste toka.

Prve tri linije kôda glavnog dijela programa su jasne: deklariraju se potrebne varijable, ispisuje se poruka korisniku te se učitava naziv datoteke koju će program učitati. Četvrta linija je novost.
U četvrtoj liniji funkcije main() javlja se sintaksa koja vam je možda dosad bila nepoznata. Naime, laički i netočno bi moglo bi se reći da se ovdje deklarira varijabla naziva “datoteka”. Međutim, ovdje se ustvari instancira objekt klase ifstream, naziva datoteka te se njegovom konstruktoru predaje kao argument ulaz.data().

Objekte možemo zamisliti kao strukture podataka koje mogu sadržavati funkcije, odnosno, metode. Jedna od takvih tvorevina je i objekt klase string, o kojem smo pisali u prethodnim tekstovima. Zasada je dovoljno znati par osnovnih stvari o objektima: često se deklarira (točnije, instancira) na drukčiji način u odnosu na varijable, može imati konstruktor (koji vrši određene radnje pri samom stvaranju objekta) te podržava metode (koje su vezane za cijelu klasu objekata, a djeluju samo na određeni objekt).

Objekt klase ifstream predstavlja ulazni tok podataka iz datoteke. Dakle, u ovaj objekt učitavamo sadržaj cijele datoteke, te kasnije baratamo tim sadržajem. Bitno je naglasiti da je ifstream isključivo ulazni tok pa se datoteka učitana pomoću ovog objekta koristi samo za čitanje, ne i za pisanje. Iz sigurnosnih razloga, ukoliko je datoteku zaista potrebno samo čitati, dobro je učitati upravo pomoću ove klase, jer se time onemogućuje slučajno pisanje i eventualno brisanje sadržaja datoteke.

Kao što smo već spomenuli, konstruktoru objekta klase ifstream i naziva “datoteka” predajemo kao argument ulaz.data(). Ovaj argument nije ništa drugo nego tekst kojeg je korisnik upisao. Međutim, konstruktor prima kao argument tip podatka char[], odnosno, niz znakova (realizacija stringa u C-u), a varijabla ulaz je objekt klase string. Zbog toga, ugrađenom metodom.data() potrebno je pretvoriti string u niz znakova, što smo ovdje i učinili.
Ako se dogodio problem pri čitanju datoteke, valja obavijestiti korisnika o grešci. Metoda.good() provjerava upravo to – ako se ne može učitati sadržaj datoteke, metoda vraća false (laž), a ako može – true. Ukoliko datoteka nije valjana, ispisuje se poruka o grešci te program pomoću [return(0);] završava s radom.

Uvjet while petlje je upravo ista ta metoda. Primijetite da za pravilan rad programa naredba if nije niti potrebna, jer svakako, ako datoteka nije valjana, blok while petlje će se jednostavno preskočiti, odnosno, program neće niti pokušavati čitati sadržaj datoteke. Međutim, tada korisnik ne bi dobio poruku o grešci te ne bi mogao znati je li datoteka možda valjana i prazna u slučaju da program ne da nikakav ispis.
Čemu takav uvjet while petlje? Naime, tokovi podataka, bilo ulazni, izlazni ili datotečni, u C++-u funkcioniraju tako da objekt toka podataka sadrži tzv. interni pokazivač (engl. internal pointer) pomoću kojeg se čitaju ili upisuju podaci. Recimo, upisom tri znaka u nekakav tok, pokazivač će se pomaknuti za tri mjesta te tako omogućiti upis novog podatka bez da se prethodna tri znaka prebrišu. Upis će se jednostavno nastaviti na poziciji na kojoj se nalazi taj interni pokazivač.

Slična stvar se događa ovdje – prvim ulaskom u petlju pokazivač se nalazi na početku sadržaja datoteke. Čitanjem dijelova sadržaja datoteke, pokazivač se adekvatno pomiče prema kraju datoteke. Naposljetku, kada je cijeli sadržaj pročitan, pokazivač se nalazi na samom kraju sadržaja, te pokušajem daljnjeg čitanja ono neće biti uspješno. Upravo zbog toga, [datoteka.good()] vratit će false te će petlja završiti s radom.

Sadržaj se čita pomoću otprije poznate funkcije getline(). Upravo je ona jedna od funkcija koje su zajedničke svim vrstama tokova podataka. Prvi argument je tok iz kojeg se čita jedna linija, a drugi argument je varijabla u koju će se ta linija spremiti.
Nakon čitanja svake linije ona se jednostavno pomoću cout-a ispisuje na ekran. Kada pokazivač dođe do kraja sadržaja te petlja završi s radom, pomoću metode.close() zatvara se tok podataka vezan za objekt nad kojim se izvršava. Iako nije nužno, dobra je praksa svaki otvoreni tok naposljetku i zatvoriti, jer se time osigurava “čišćenje smeća” koje je stvoreno tokom izvršavanja programa.

Već ovim početkom vidi se da rad s datotekama nije kompliciran u C++-u. Dapače, vrlo je jednostavan, upravo zbog pristupa datotekama pomoću “tokova” čime se postiže intuitivnost i jednostavnost rada s datotekama, uz pretpostavku prethodnog poznavanja rada sa stringovima.
Krajnji tekst editor kojeg ćemo kroz primjer napraviti, bit će podijeljen u četiri glavne cjeline: unos naziva ulazne i izlazne daoteke te opcija izmjene, učitavanje datoteke, obrada iste te upisivanje rezultata u izlaznu datoteku. Pošto vidimo da za kvalitetno učitavanje datoteke ipak nije dovoljno samo instanciranje objekta datotečnog toka, zasad ćemo definirati funkciju za učitavanje datoteke koje ćemo u daljnjem razvoju aplikacije koristiti.

Drugi primjer obavlja identičnu zadaću kao prvi primjer, odnosno, rezultat izvršavanja oba programa će biti jednak. Iako drugi primjer sadrži više linija kôda i djeluje kompliciranije (što i jest) od prvog primjera, postoje mnoge prednosti drugog primjera u odnosu na prvi. Prva prednost je što je sada učitavanje datoteke jasno odvojeno od obrade, a druga, manje vidljiva je ta što sadržaj pohranjujemo, a nakon toga ispisujemo.

Dvije su novosti u kôdu: definicija i korištenje vlastito definirane funkcije te upotreba objekta klase vector.
Funkcija predstavlja potprogram, koji prima argumente, vrši nekakve operacije te vraća rezultat. Jednom definirana, može se pozivati prouzvoljan broj puta u proizvoljnom dijelu cjelokupnog programa – bitno je samo da je mjesto pozivanja nakon mjesta definicije funkcije.

Definicija funkcije počinje headerom, kojim se definira tip podatka koji funkcija vraća kao rezultat, ime funkcije te argumenti koje funkcija podržava. Definicija naše novodefinirane funkcije za učitavanje počinje u sedmoj liniji kôda. Funkcija vraća tip podatka vector (malo kasnije će biti riječi o ovom tipu podatka), naziv joj je “ucitaj datoteku” te podržava jedan argument tipa string koji je nazvan “putanja”. Naziv argumenta se koristi unutar same funkcije – argument predan funkciji pri njenom pozivu ovdje će se “preslikati” u varijablu putanja čiji je doseg (engl. scope) samo unutar same funkcije. Dakle, kad bismo u glavnom dijelu programa definirali varijablu istog imena, ona ne bi imala utjecaja na ovu, internu varijablu putanja koja je vezana samo za tijelo funkcije ucitajDatoteku(), kao ni u obrnutom slučaju.

Nakratko se prebacimo na glavni dio programa. U drugoj liniji funkcije main() deklarira se varijabla spomenutog tipa vector i imena “sadrzaj”. U šestoj liniji, poziva se funkcija ucitajDatoteku(), kojoj se kao argument predaje varijabla ulaz koja je tipa string. Funkcija nakon obrade vraća rezultat koji se znakom jednakosti sprema u varijablu naziva “sadrzaj”. Vidi se da se tipovi podataka poklapaju, što je i nužno za pravilan rad programa. Naime, varijabla ulaz je jednakog tipa podatka kao argument putanja u definiciji funkcije, a povratna vrijednost funkcije je jednakog tipa kao varijabla sadrzaj.

Vratimo se na definiciju funkcije ucitajDatoteku(). Osim headera, definicija se sastoji od tijela funkcije, koje je omeđeno vitičastim zagradama. Sve što je unutar njih izvodi se isključivo pozivom te funkcije. Varijable definirane unutar tijela funkcije vidljive su samo toj funkciji, pa tako sve varijable definirane u funkciji main() (u glavnom programu) vidljive su samo njoj. Upravo je to razlog slanja argumenta, te definiranju novih varijabli unutar funkcije ucitajDatoteku().

Tip podatka vector predstavlja klasu objekata te pripada C++-ovom posebnom skupu klasa zvanim “STL Containers”. Sve klase unutar ovog skupa s jedne strane imaju mnogo toga zajedničkog, a s druge strane svaka je klasa prilagođena nekoj specifičnoj upotrebi. Zasad nam je potrebna samo ova klasa, vector, koja predstavlja niz ili red podataka. Instanciranjem se stvara niz, u koji se onda posebnim metodama mogu dodavati ili uklanjati elementi, a indeksiranjem je moguće pristupiti bilo kojem elementu koji je dodan. Postoji još mnoštvo metoda koje vector podržava, no one zasad nisu bitne.

Kao i svi ostale vrste objekata koje pripadaju STL Containerima, tako se i kod ove vrste instanciranjem objekta navodi kojeg će tipa biti varijable ili objekti koji se dodaju u objekt. Sintaksa instanciranja vectora jasno se vidi u prvoj liniji tijela funkcije ucitajDatoteku(): navodi se tip containera (vector) nakon čega se unutar trokutastih zagrada upisuje tip podataka koje će vector sadržavati te naposljetku ime samog vectora. Kaže se “instancirali smo vektor stringova naziva rezultat”.

Nakon instanciranja ovog objekta, deklarirana je varijabla naziva “linija” u koju će se spremati svaka učitana linija iz datoteke. Ustvari, cijelo tijelo ove funkcije je vrlo slično funkciji main() iz prošlog primjera. Razlika je što se linije ne ispisuju na ekran, nego se spremaju u vektor stringova. Metoda.push_back() vezana za klasu vector dodaje vektoru argument koji joj je predan. Tako prvim ulaskom u petlju, vektor je prazan, učitava se prva linija u varijablu linija te se ista dodaje vektoru rezultat. Sada rezultat sadrži jedan član (string). Nakon drugog prolaska, dodat će se još jedan član pa će sadržavati dva člana itd.
Prethodni opis dodavanja članova može se činiti suvišan – jasno je što znači dodati član. Međutim, valja primijetiti da je vector takva klasa koja omogućuje dodavanje proizvoljnog broja članova, bez prethodno definirane veličine vektora. Dinamički se alocira memorija te će ovaj objekt zauzimati točno onoliku količinu memorije kolika je potrebna za pravilan rad – ni više ni manje.

Ako se datoteka ne uspije učitati, petlja se neće nijednom izvršiti, a funkcija će vratiti kao rezultat prazan vektor, odnosno, objekt vector s nula članova. U glavnom dijelu programa koristi se upravo ovaj slučaj za hvatanje pogreške.

Glavni dio programa, funkcija main(), počinje jednako kao i u prošlom primjeru s tim da se instancira objekt za sadržaj te se deklarira varijabla naziva “i” za brojač petlje. Tip podatka unsigned predstavlja cijeli broj bez predznaka, dakle, ne podržava negativne brojeve. Program bi normalno radio i u slučaju da je upotrebljen tip int, no kompajler bi kasnije prijavio upozorenje (ne grešku) pri usporedbi varijable i s veličinom vektora. Naime, kasnije upotrebljena metoda.size() kao rezultat vraća tip podatka unsigned.

U varijablu sadrzaj pohranjuje se vektor koji sadrži sve linije učitane datoteke, nakon čega se iteracijom kroz petlju ispisuje svaki redak posebno. Uvjet ostanka u petlji je da je brojač manji od veličine vektora. Metoda.size() vraća broj članova u vektoru, pa ako vektor ima recimo 5 članova, varijabla i će poprimati vrijednosti 0, 1, 2, 3 i 4, što su upravo indeksi članova unutar vektora.

Pošto se prolaskom kroz petlju izvršava samo jedna linija kôda, nije potrebno stavljati vitičaste zagrade za blok petlje. U toj jedinoj liniji ispisuje se i-ti član vektora, odnosno, i-ta linija učitane datoteke. Na kraju se provjerava veličina vektora – ako je jednaka nuli, ona pri ispitivanju uvjeta predstavlja false, te je uvjet zadnjeg if-a ispunjen i ispisuje se poruka o grešci. U tom slučaju neće se niti izvršiti prethodna for petlja, jer već prvim ulaskom u petlju, i nije manji od veličine vektora (0=0)
Kako to da učitavanjem prazne datoteke program ne prijavljuje grešku? Ako je datoteka prazna, u funkciji ucitajDatoteku(), datoteka će “biti good”, a u vektor će se upisati jedan prazni string. Vektor će dakle imati jedan član, nebitno je to što je taj član prazan string, no on ipak postoji. U glavnom dijelu programa će se dakle normalno obaviti prvi ulazak u petlju, ispisati taj prazni string, a uvjet zadnjeg if-a neće biti zadovoljen.
U sljedećem primjeru je definirana funkcija za ispis, kao i prva funkcija za preradu teksta, koja izbacuje višestruke razmake. Za ispitivanje primjera korištena je datoteka naziva “datoteka3”, čiji sadržaj možete vidjeti u screenshotu izvršavanja trećeg prijera, kao i izgled izlazne, preuređene datoteke.

Prva, minorna promjena u programu je izmjenjena klasa objekta koji se koristi za pohranu sadržaja datoteke. Ne koristi se više vector, nego deque, koji također pripada STL Containerima. Jedina bitna razlika u odnosu na vector, zbog koje je uveden deque, je mogućnost jednostavnog uklanjanja prvog elementa. Vector je optimiziran za rad sa zadnjim elementom pa ne posjeduje posebnu metodu za uklanjanje i dodavanje elementa na početak niza. Deque pak predstavlja takozvani red, koji posjeduje metode i za rad s prednjim i za rad sa zadnjim elementom. Upotreba deque-a je ujedno jedina promjena u funkciji ucitajDatoteku pa tu funkciju nećemo više posebno razmatrati.

Lako se uočava da je građa funkcije pohraniDatoteku() zapravo vrlo slična prvoj funkciji – razlikuje ih suprotna zadaća. Ova funkcija prima dva argumenta – prvi je sadržaj u već poznatom formatu, dok je drugi putanja za izlaznu datoteku. Funkcija kao rezultat vraća podatak tipa bool; ako je upis uspješan, vraća true, inače vraća false. Na početku je instanciran, u ovom slučaju, objekt klase ofstream (Output File STREAM) koji predstavlja izlaznu datoteku. Već u sljedećem retku ispituje se valjanost toka, ako tok nije valjan, funkcija završava s radom i vraća false.

U suprotnom, nastavlja se izvođenje redaka funkcije – pokreće se while petlja čiji je uvjet da objekt sadrzaj nije prazan. Naime, metoda.empty() vraća true ako je objekt prazan, inače vraća false. Ulaskom u petlju, pomoću operatora “<<” prvi član ulaznog deque-a (vraća ga metoda.front()) upisuje se u izlaznu datoteku iza kojeg slijedi oznaka kraja retka. Naime, prethodnim upisivanjem datoteke u objekt pomoću funkcije getline() brišu se znakovi za novi red te ih stoga moramo na ovaj način upisati u izlaznu datoteku. Metoda pop_front() uklanja prvi član deque-a. Ukoliko ste upoznati s nizovima u C-u, sigurno znate koliko je komplicirano vađenje člana iz niza, a koji nije zadnji – kod STL Containera ovakve radnje se svode na upotrebu jedne jedine metode. Nakon upisa, datoteka se zatvara i funkcija vraća true.

U ovoj funkciji se opet jasno može vidjeti kako mnoge klase u C++-u, koje posjeduju neka logično zajednička svojstva, dijele iste metode, funkcije i operatore. Korištenje izlazne datoteke je s jedne strane identično korištenju ulazne: dijele iste metode te se na isti način instanciraju. S druge strane, operator “<<” koji je ranije korišten u radu sa stringovima jednako se ponaša u slučaju datoteka. Vidi se i u prethodnoj funkciji ucitajDatoteku() koliko je jednostavno jedan STL Container pretvoriti u drugi, jer svi oni dijele velik skup zajedničkih metoda.

Zadaća novouvedene funkcije izbaciVisestruke() kao argument prima sadržaj (deque stringova), a vraća sadržaj u kojem su izbačeni višestruki razmaci. Za obavljanje ove operacije potrebno je iterirati po svim retcima, te za svaki redak proći kroz sve znakove. Ukoliko se naiđe na dva razmaka, jedan za drugim, oni se zamjenjuju jednim razmakom.
Varijablom i prolazi se kroz sve retke na već korišteni način – vanjskom for petljom varijabla i poprima vrijednosti od 0 do broja redaka, isključivo. U unutarnjoj petlji na sličan način se varijabla j mijenja od 0 do broja znakova u retku, no umanjenom za jedan. Varijabla sadrzaj[i] je i-ti član deque-a sadrzaj, pa je ona string koji predstavlja jedan redak. Na njemu se primjenjuje metoda.size(). Nije potrebno paziti da se ne pređe duljina stringa, jer se dohvatom nepostojećeg člana neće dogoditi greška – on jednostavno neće zadovoljiti provjere find-a i replace-a.

Naredba if ispituje je li na tom mjestu dvostruki razmak. To se postiže upotrebljenom metodom.substr() koja ima izuzetno velik raspon načina rada. U ovom načinu rada, gdje su joj predana dva broja kao argumenti, kao rezultat vraća dio stringa nad kojim se metoda izvršava, a dio je određen indeksom i duljinom; tako bi recimo [“abcdef”.substr(3,2)] vratila string “de”, jer rezanje počinje na članu s indeksom 3 (“d”), a isječak je duljine dva znaka. Ukoliko je pronađen dvostruki razmak, prvom linijom bloka if naredbe on se zamjenjuje jednostrukim razmakom pomoću metode.replace() koja kao argumente, uz indeks i duljinu isječka, prima i string kojim će se mijenjati isječak unutar stringa nad kojim se metoda vrši. Nakon ove izmjene mora se indeks j umanjiti za jedan, jer bi se u suprotnom preskočio jedan znak. Na kraju funkcije jednostavno se vraća izmjenjeni sadržaj.

Glavni dio programa je sada pojednostavljen, te su još bolje odvojene ranije spomenute cjeline. Na početku je deklaracija potrebnih varijabli i instanciranje objekta, zatim slijede upisi naziva ulazne i izlazne datoteke. Nakon toga je učitavanje datoteke u objekt i provjera ispravnosti datoteke, zatim obrada datoteke pomoću novouvedene funkcije te naposljetku upis objekta u datoteku, koji je direktno stavljen u uvjet naredbe if.
Vidi se kako se vrlo lako i efikasno može postići modularnost programa. Uvođenjem funkcija dobiva se mogućnost jednostavnog određivanja redoslijeda obavljanja pojedinih operacija, kao i dodavanje uvjeta te uključivanje i isključivanje obrada.

U četvrtom primjeru dodane su tri funkcije za obradu teksta. Glavni program samo nadopunite pozivima te tri funkcije, te dodajte definicije funkcija ispred definicije funkcije main(). Uvedene su funkcije za izbacivanje praznih redaka, izbacivanje vodećih razmaka iz redaka te kapitaliziranje rečenica (prvo slovo se postavlja velikim, ostala malim).
Funkcija izbaciPrazne() izbacuje prazne retke, pri čemu se praznim retcima smatraju i oni koji su ispunjeni samo prazninama. Glavna petlja s brojačem i, prolazi kroz retke sadržaja, a unutarnja kroz znakove jednog retka. Specifičnost upotrebe unutarnje petlje u ovom slučaju je što se iza headera petlje nalazi točka-zarez te petlja nema blok.

Ovaj način izvođenja petlje može se predočiti petljom koja ima prazan blok, dakle, normalno se inicijalizira varijabla, provjerava uvjet te izvršava naredba predana kao treći argument (korak petlje). U ovom slučaju, petlja se vrti dok god je j-ti član retka manji od duljine istog, a da je ujedno i jednak razmaku. Nakon izvršavanja petlje, varijabla j će imati vrijednost indeksa prvog znaka u retku koji nije jednak razmaku. Naredbom if provjerava se je li j jednak duljini retka. Ukoliko je, to znači da su svi znakovi u retku jednaki razmacima ili da je redak prazan te se briše taj redak is deque-a. Zbog specifičnosti deque-a, metodi erase mora se predati iterator koji pokazuje na član koji je potrebno izbrisati. Dovoljno je znati da metoda.begin() daje iterator na prvi član containera (deque-a) te se pribrajanjem varijable i na taj iterator dobiva upravo i-ti član containera. Kao što smo i prije imali slučaj, uklanjanjem člana niza kroz koji se iterira, potrebno je smanjiti indeks za jedan kako se ne bi preskočio sljedeći član niza.

Izbacivanje vodećih razmaka u retku je izvedeno na vrlo jednostavan način. U funkciji izbaciVodece() iterira se opet po svim retcima te se za svaki redak vrti petlja while čiji je uvjet da je nulti član retka jednak razmaku. Jedina naredba koja se izvršava pod petljom je brisanje upravo tog nultog člana, odnosno razmaka. Metoda.erase(), kada se primjenjuje na stingu, počinje brisanje na indeksu koji je zadan prvim argumentom i briše odsječak stringa duljine koja je zadana drugim argumentom.

Dakle, briše se jedan jedini i to nulti član.
Kao što se vidi iz funkcije prvoVelikoSlovo(), kapitaliziranje rečenica je relativno složena zadaća. Za početak, imamo dvije varijable: varijabla znak služi kao zastavica koja pamti je li zadnje učitan znak jednak nekom od znakova točke, upitnika ili uskličnika, dok varijabla znakovi služi upravo za određivanje znakova koji predstavljaju kraj rečenice. Koriste se dvije petlje, jednako kao u funkciji izbaciVisestruke(). U unutarnjoj petlji, prvo se provjerava sljedeće: je li aktivni znak iz sadržaja pronađen u stringu znakovi. Naime, metoda.find() traži unutar stringa nad kojim se vrši ista prvo pojavljivanje znaka koji je predan kao argument. U slučaju pronalaska vraća indeks pojave, a inače vraća objekt tipa string::pos. Uvjet jednostavno ispituje je li metoda vratila nešto što nije string::pos, dakle, je li aktivni znak točka, uskličnik ili upitnik. Ako je, postavlja se zastavica znak u true.

Postoji čitav niz ugrađenih funkcija za ispitivanje pripadnosti podatka tipa char nekoj klasi znakova, od kojih su u ovoj funkciji tri iskorištene. Jedna od njih je upravo u sljedećem ispitivanju uvjeta, a to je funkcija isaplha(), koja vraća istinu ako je ispitivani znak slovo. Ako nije slovo, preskače se blok petlje, a program nastavlja izvođenje na headeru petlje. Ova provjera je nužna kako se ne bi eventualni brojevi u tekstu kapitalizirali te kako bi se preskočili razmaci i svi ostali specijalni znakovi.

U sljedećem ispitivanju provjerava se je li postavljena zastavica znak. Ako je postavljena zastavica znak, znači da je aktivni znak upravo prvo slovo iza točke, upitnika ili uskličnika, a ako je aktivni znak prvo slovo u sadržaju, on je ujedno i početak rečenice i svakako će se kapitalizirati, jer je varijabla znak inicijalno postavljena u true. U bloku ove if naredbe, varijabla znak se postavlja u false nakon čega se vrši provjera je li aktivni znak malo slovo. Ako je, od njegove ASCII vrijednoti oduzima se 32 (prisjetimo se iz jednog od prethodnih tekstova da malo slovo uvijek ima za 32 veću ASCII vrijednost od velikog slova). Ovdje je naredba continue nužna, kako se ne bi u sljedećem if-bloku upravo postavljeno veliko slovo vratilo natrag u malo.

Program je sada u završnoj fazi razvoja. Sljedećim primjerom dodaju se jednostavne funkcije za brojanje znakova i riječi te se modificira funkcija za izbacivanje vodećih razmaka – funkcija je preimenovana u izbaciVodecePratece te izbacuje i razmake na kraju svakog retka. Potpuni program možete vidjeti u okviru sa strane.
U spomenutu preimenovanu funkciju dodana je while petlja slična postojećoj – razlika je što ova, nova petlja, iterira od zadnjeg člana prema prvom. Potrebno je provjeravati i duljinu retka, jer ako je jednaka nuli, program bi prijavio grešku u zbog pokušaja pristupa članu s indeksom -1.

Funkcija brojZnakova() vraća broj znakova u sadržaju, bez prelazaka u novi red. Jednostavno se za svaki redak računa veličina tog retka te se pridodaje varijabli rezultat, koja je inicijalno jednaka nuli.

U funkciji brojRijeci() sad se jasno vidi prednost primjene funkcija. Pomoću već ostvarenih funkcija, iz sadržaja se izbacuju svi suvišni razmaci i prazni retci kako bi se na jednostavan način prebrojavanjem razmaka mogle izbrojati riječi. Dvostrukom petljom se ide po svim članovima te se za svaki pronađeni razmak na rezultat dodaje 1. Nakon obrade svakog retka rezultat se uvećava za 1, jer npr. uz redak [prva druga treća] izbrojana su dva razmaka, a u retku se nalaze tri riječi, dakle, broj riječi jednak je broju razmaka uvećanom za jedan.

U glavnom dijelu programa dodani su pozivi funkcija za brojanje riječi i znakova te ispis njihovih rezultata. Također su sve funkcije za obradu teksta dodane u else blokove kako bi se izbjeglo nepotrebno računanje s neispravnim podacima.

Kroz članak ste vidjeli kako je u osnovi jednostavno baratati datotekama – jednom omogućen pristup čitanju ili pisanju u datoteku, lako je manipulirati sadržajem iste, odnosno, u slučaju čitanja, kopijom sadržaja. U odnosu na prethodne tekstove, tek smo ovim dijelom Škole načeli pravu prirodu C++-a, programskog jezika s odlično riješenim objektno orijentiranim pristupom.

Vidi se i kako je jednostavno nadograđivati program kad je kvalitetno moduliran. Na krajnji program lako ćete moći dodati funkcije npr. za brojanje redaka, brojanje znakova bez razmaka, ispis rezultata brojanja u datoteku i sve drugo što vam padne napamet.

Infobox (facts): Prvi primjer
Primjer 1

#include <iostream>
using namespace std;
int main(void){
[tab]int a,b;
[tab]char znak;
[tab]cin >> a >> znak >> b;
[tab]if(znak == ‘+’){
[tab][tab]cout << a + b << endl;
[tab][tab]}
[tab]else if(znak == ‘*’){
[tab][tab]cout << a * b << endl;
[tab][tab]}
[tab]else cout << “Neispravan unos.” << endl;
[tab]return(0);
[tab]}

Infobox (facts): Drugi primjer
Primjer 2

#include <iostream>
using namespace std;
int main(void){
[tab]float a,b;
[tab]char znak;
[tab]cin >> a >> znak >> b;
[tab]switch(znak){
[tab][tab]case ‘+’:
[tab][tab][tab]cout << a + b << endl;
[tab][tab][tab]break;
[tab][tab]case ‘-’:
[tab][tab][tab]cout << a – b << endl;
[tab][tab][tab]break;
[tab][tab]case ‘*’:
[tab][tab][tab]cout << a * b << endl;
[tab][tab][tab]break;
[tab][tab]case ‘/’:
[tab][tab][tab]cout << a/b << endl;
[tab][tab][tab]break;
[tab][tab]default:
[tab][tab][tab]cout << “Neispravan unos.” << endl;
[tab][tab]}
[tab]return(0);
[tab]}

Infobox (facts): Treći primjer
Primjer 3

#include <iostream>
using namespace std;
int main(void){
[tab]int i, a = 0, brojac = 1;
[tab]string unos;
[tab]getline(cin, unos);
[tab]for(i = unos.size() – 1; i >= 0; — i, brojac *= 10){
[tab][tab]a += brojac * (unos[i] – 48);
[tab][tab]}
[tab]cout << a << endl;
[tab]return(0);
[tab]}

Infobox (facts): Četvrti primjer
Primjer 4

#include <iostream>
using namespace std;
int main(void){
[tab]int i = 0, j, duljina, a, brojac;
[tab]float rez = 0;
[tab]string unos, broj;
[tab]char znak = ‘#’;
[tab]getline(cin, unos);
[tab]duljina = unos.size();
[tab]while(i < duljina){
[tab][tab]for(j = i, brojac = 1; j < duljina; ++ j, brojac *= 10)
[tab][tab][tab]if(unos[j] < ‘0’ or unos[j] > ‘9’)
[tab][tab][tab][tab]break;
[tab][tab]for(a = 0, brojac /= 10; i < j; ++ i, brojac /= 10)
[tab][tab][tab]a += brojac * (unos[i] – ‘0’);
[tab][tab]switch(znak){
[tab][tab][tab]case ‘#’: rez = a; break;
[tab][tab][tab]case ‘+’: rez += a; break;
[tab][tab][tab]case ‘-’: rez -= a; break;
[tab][tab][tab]case ‘*’: rez *= a; break;
[tab][tab][tab]case ‘/’: rez /= a;
[tab][tab][tab]}
[tab][tab]if(i < duljina) znak = unos[i];
[tab][tab]++ i;
[tab][tab]}
[tab]cout << rez << endl;
[tab]return(0);
[tab]}

Infobox (facts): Peti primjer
Primjer 5

#include <iostream>
using namespace std;
int main(void){
[tab]int i = 0, j, duljina;
[tab]float rez = 0, a, brojac;
[tab]string unos, broj;
[tab]char znak = ‘#’;
[tab]int predznak = 1;
[tab]getline(cin, unos);
[tab]duljina = unos.size();
[tab]while(i < duljina){
[tab][tab]if(unos[i] == ‘-’){ predznak = -1; ++ i; }
[tab][tab]for(j = i, brojac = 1; j < duljina; ++ j, brojac *= 10)
[tab][tab][tab]if(unos[j] < ‘0’ or unos[j] > ‘9’) break;
[tab][tab]if(unos[j] == ‘.’)
[tab][tab][tab]for(++ j; j < duljina; ++ j)
[tab][tab][tab][tab]if(unos[j] < ‘0’ or unos[j] > ‘9’) break;
[tab][tab]for(a = 0, brojac /= 10; i < j; ++ i, brojac /= 10){
[tab][tab][tab]if(unos[i] == ‘.’){
[tab][tab][tab][tab]brojac *= 10;
[tab][tab][tab][tab]continue;
[tab][tab][tab][tab]}
[tab][tab][tab]a += brojac * (unos[i] – ‘0’);
[tab][tab][tab]}
[tab][tab]switch(znak){
[tab][tab][tab]case ‘#’: rez = predznak * a; break;
[tab][tab][tab]case ‘+’: rez += predznak * a; break;
[tab][tab][tab]case ‘-’: rez -= predznak * a; break;
[tab][tab][tab]case ‘*’: rez *= predznak * a; break;
[tab][tab][tab]case ‘/’: rez /= predznak * a;
[tab][tab][tab]}
[tab][tab]if(i < duljina) znak = unos[i];
[tab][tab]++ i;
[tab][tab]predznak = 1;
[tab][tab]}
[tab]cout << rez << endl;
[tab]return(0);
[tab]}

 

Autor: Josip Užarević


RELATED ARTICLES

Komentiraj

Please enter your comment!
Please enter your name here

- Advertisment -

Most Popular